-
Notifications
You must be signed in to change notification settings - Fork 13.2k
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
| 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
| } | ||
| 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
| } | ||
|
|
||
| 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
| 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
| 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
modulein each commonjs file. Previously, all references tomoduleused the same symbol, which had type any. Nowmodulehas type{ exports: typeof import("current-file") }. This module symbol is no longer identified by identity but by CheckFlags.ModuleExports.A few notes:
modulehas 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 thatgetTypeOfFuncClassEnumModulenow needs to use push/popTypeResolution to prevent infinite loops.Fixes #25687
Fixes #25621