-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Check module.exports #25732
Conversation
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.
Looks like the build found some lint. I'll fix it in my next commit. |
Notes from user tests: Good
Bad
var axios = {}
module.exports = axios // ok
module.exports.default = axios // should be ok, currently an error
var npm = module.exports = function (tree) {
}
module.exports.asReadInstalled = function (tree) {
npm(tree) // should be callable, but isn't
}
var EE = require('events').EventEmitter
var log = exports = module.exports = new EE()
log.on('error', function() { }) Ugly
|
These currently fail.
Still quite messy and full of notes
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.
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 |
src/compiler/checker.ts
Outdated
@@ -1412,6 +1411,14 @@ namespace ts { | |||
return lastLocation.symbol; | |||
} | |||
} | |||
if (originalLocation && isInJavaScriptFile(originalLocation) && originalLocation.parent) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
src/compiler/checker.ts
Outdated
@@ -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") { |
There was a problem hiding this comment.
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.
src/compiler/checker.ts
Outdated
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes! Good catch.
There was a problem hiding this comment.
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.
src/compiler/checker.ts
Outdated
@@ -4710,7 +4711,7 @@ namespace ts { | |||
return undefined; | |||
} | |||
|
|||
function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol) { | |||
function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol, aliased: Symbol) { |
There was a problem hiding this comment.
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?
src/compiler/checker.ts
Outdated
@@ -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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Done.
src/compiler/checker.ts
Outdated
if (!aliased.exports) { | ||
aliased.exports = createSymbolTable(); | ||
} | ||
(isModuleExportsAlias ? aliased : symbol).exports!.forEach((s, name) => { |
There was a problem hiding this comment.
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);
.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/compiler/checker.ts
Outdated
@@ -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 |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
Create a unique symbol and type for
module
in each commonjs file. Previously, all references tomodule
used the same symbol, which had type any. Nowmodule
has type{ exports: typeof import("current-file") }
. This module symbol is no longer identified by identity but by CheckFlags.ModuleExports.A few notes:
module
has a fresh anonymous type for each file. This may have to change in order to fixconst x = require("m")
should create an alias symbol #25533, although the outline of the solution will remain the same.symbol.exportSymbol
.exports.default = { blah: 1 }; module.exports = exports['default']
mean thatgetTypeOfFuncClassEnumModule
now needs to use push/popTypeResolution to prevent infinite loops.Fixes #25687
Fixes #25621