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.
There are 7 conditions here -- why put 3 in one if and 4 in another?
There was a problem hiding this comment.
Also, no need to test && originalLocation.parent if we know it's an Identifier.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
Might be neater to make aliased optional.
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.
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.
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.
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.
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.
This seems like a complex way to avoid writing a for loop...
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