Skip to content

Check module.exports #25732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 20, 2018
Merged

Check module.exports #25732

merged 18 commits into from
Jul 20, 2018

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Jul 17, 2018

Create a unique symbol and type for module in each commonjs file. Previously, all references to module used the same symbol, which had type any. Now module has type { exports: typeof import("current-file") }. This module symbol is no longer identified by identity but by CheckFlags.ModuleExports.

A few notes:

  1. I'm not sure that getSourceFileOfNode is the best way to get the symbol of the source file.
  2. module has a fresh anonymous type for each file. This may have to change in order to fix const x = require("m") should create an alias symbol #25533, although the outline of the solution will remain the same.
  3. module.exports assignment is now unconditionally a Special Property Assignment for the purposes of checking. That means assignments to module.exports (1) will not have assignability errors (2) return the type of module.exports, not its initializer. This should work fine because references to module.exports now refer to the source file's type.
  4. In order to make control flow right for merged aliases, getExportSymbolOfValueSymbolIfExported needs to unconditionally get the merged symbol of its return. Previously it only merged symbol.exportSymbol.
  5. Constructs like exports.default = { blah: 1 }; module.exports = exports['default'] mean that getTypeOfFuncClassEnumModule now needs to use push/popTypeResolution to prevent infinite loops.

Fixes #25687
Fixes #25621

sandersn added 6 commits July 16, 2018 14:53
It burns a check flags. Probably necessary, but perhaps not.

I haven't accepted baselines, but they are a bit questionable. I'm not
sure the synthetic type is right, because I expected to see
{ "exports": typeof import("x") } but instead see { "x": typeof
import("x") }.
Conflicts between exports property assignments and exports assignments
should get a union type instead of an error.
@sandersn
Copy link
Member Author

Looks like the build found some lint. I'll fix it in my next commit.

@sandersn
Copy link
Member Author

Notes from user tests:

Good

  1. adonis-framework: Aliases of module.exports are now typed, so we find that Helpers._makePath should have its third parameter optional, but is not annotated as such in its jsdoc, and that Route.route should have it second parameter verb: string | string[] not just string.

  2. assert: property assignments to an alias declaration (var assert = module.exports = ok) are now recognised. The type of this in those property assignments is still not right, but hasn't gotten worse at least.

  3. npmlog: Same as assert, but the difference is much more dramatic. Also strict null checks catches a rogue undefined.

  4. chrome-devtools-frontend: (1) includes a copy of assert (2) now has a contextual type for some assignments that sets this: any. This removes some errors! TODO: Finish later.

Bad

  1. axios: Assignment to the default property (ironically commented as "Support typescript") has an incorrect "variable 'axios' is used before being assigned error"
var axios = {}
module.exports = axios // ok
module.exports.default = axios // should be ok, currently an error
  1. npm: Alias of module.exports isn't callable in 3 different places. (Probably related to const x = require("m") should create an alias symbol #25533).
var npm = module.exports = function (tree) {
}
module.exports.asReadInstalled = function (tree) {
    npm(tree) // should be callable, but isn't
}
  1. npmlog: The alias of module.exports is missing the properties from its initialiser.
var EE = require('events').EventEmitter
var log = exports = module.exports = new EE()
log.on('error', function() { })

Ugly

  1. create-react-app: No more "can't find module 'browserslist'." I have no idea why.
  2. graceful-fs: Duplicate assignability error trying to overwrite fs.close. Maybe because of the special handling in checkBinaryLikeExpression? But this should have fewer assignability checks.

sandersn added 9 commits July 17, 2018 12:46
These currently fail.
This allows exports like `module.exports = new EE` to have properties
added to them.

Still messy, but I'm going to run user tests and look for regressions.
getExportSymbolOfValueSymbolIfExported should always merge its returned
symbol, whether it's symbol.exportSymbol or just symbol.
@sandersn
Copy link
Member Author

I fixed all the user test failures. The only incorrect change is to npm, which uses the same pattern as npmlog, but wrapped in an IIFE. Because the exports assignments aren't at top-level they aren't recognised. Now the error message is wrong as well &mdash it says typeof EventEmitter instead of EventEmitter. However, I don't think it's worth the additional effort to fix until we also make this pattern work inside an IIFE.

@sandersn sandersn requested review from a user and mhegazy July 19, 2018 23:21
@@ -1412,6 +1411,14 @@ namespace ts {
return lastLocation.symbol;
}
}
if (originalLocation && isInJavaScriptFile(originalLocation) && originalLocation.parent) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 7 conditions here -- why put 3 in one if and 4 in another?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, no need to test && originalLocation.parent if we know it's an Identifier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, that's a symptom of the development history. It looks a lot better with isModuleExportsPropertyAccessExpression.

@@ -1412,6 +1411,14 @@ namespace ts {
return lastLocation.symbol;
}
}
if (originalLocation && isInJavaScriptFile(originalLocation) && originalLocation.parent) {
if (isIdentifier(originalLocation) && isPropertyAccessExpression(originalLocation.parent) &&
originalLocation.escapedText === "module" && originalLocation.parent.name.escapedText === "exports") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isModuleExportsPropertyAccessExpression does something similar to this.

if (originalLocation && isInJavaScriptFile(originalLocation) && originalLocation.parent) {
if (isIdentifier(originalLocation) && isPropertyAccessExpression(originalLocation.parent) &&
originalLocation.escapedText === "module" && originalLocation.parent.name.escapedText === "exports") {
const moduleSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "module" as __String, CheckFlags.ModuleExports);
Copy link

@ghost ghost Jul 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the creation should be cached somewhere, since we base several other caches on the symbol. If we see the name module 100 times (not that unlikely in a commonjs module), we'll create 100 different symbols, and we'll also create a new type for each of those symbols.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes! Good catch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent to always binding module in module.exports to a specific symbol, so I just moved it to the binder and moved ModuleExports from CheckFlags to SymbolFlags.

@@ -4710,7 +4711,7 @@ namespace ts {
return undefined;
}

function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol) {
function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol, aliased: Symbol) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function's getting pretty big (over 120 lines), any way you could break it down in a future PR?

@@ -4766,15 +4767,19 @@ namespace ts {
}
else if (!jsDocType && isBinaryExpression(expression)) {
// If we don't have an explicit JSDoc type, get the type from the expression.
let type = getWidenedLiteralType(checkExpressionCached(expression.right));
const isModuleExportsAlias = aliased !== symbol;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be neater to make aliased optional.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Done.

if (!aliased.exports) {
aliased.exports = createSymbolTable();
}
(isModuleExportsAlias ? aliased : symbol).exports!.forEach((s, name) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If !isModuleExportsAlias, symbol the same as aliased. So this should always equal aliased.
Tested with Debug.assert((isModuleExportsAlias ? aliased : symbol) === aliased);.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now aliased || symbol now that aliased is optional instead of sometimes duplicated.

links.type = baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type;
if (symbol.flags & SymbolFlags.ValueModule && symbol.valueDeclaration && isSourceFile(symbol.valueDeclaration) && symbol.valueDeclaration.commonJsModuleIndicator) {
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
return errorType;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not set links.type in this case?
This whole function might be neater if it just called getTypeOfFuncClassEnumModuleUncached which used return x instead of links.type = x. Then we would be sure to be setting the value and not forgetting some control flow path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original reason is that getTypeOfVariableOrParameterOrProperty does. I think that's because you can call getTypeOfSymbol in situations that are speculative, and you may successfully be able to get the type at a later time.

I tried this and nothing broke, even when I changed the equivalent line in getTypeOfVariableOrParameterOrProperty. So it looks like my guess is wrong. I think I will merge this for now and try that refactor separately, for both.

@@ -19295,7 +19322,7 @@ namespace ts {
if (node.expression.kind === SyntaxKind.SuperKeyword) {
const superType = checkSuperExpression(node.expression);
if (isTypeAny(superType)) {
forEach(node.arguments, checkExpresionNoReturn); // Still visit arguments so they get marked for visibility, etc
forEach(node.arguments, checkExpressionNoReturn); // Still visit arguments so they get marked for visibility, etc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a complex way to avoid writing a for loop...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

Previously it was also special, but created during name resolution in
the checker. It made sense when there was only one special symbol for
all files, but now there is one `module` symbol per file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant