-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
More special declaration types in JS #21974
More special declaration types in JS #21974
Conversation
Previously this only worked cross-file because it was a merge. Now it works anywhere, and locally it is actually binding a new property on the object literal symbol.
Plus add tests for them
It is still super hacky
Less polymorphism, up-to-date generalised names and documentation.
This required fixing the predicates and the avoiding of contextual typing loops. This is now done right, in getContextualTypeOfBinaryExpression. The predicates still need work.
Except for adding a newline at the end of file. That's required by Linux, you know!
Turns out merging was incorrect even for non-nested declarations, but tests didn't catch it before.
Also improve assert message in fourslash.
The add-intermediate-container-symbols loop is still quite confusing, but it's not as bad as before.
Makes typingsInstaller compile without adding dependencies
src/compiler/utilities.ts
Outdated
return SpecialPropertyAssignmentKind.ExportsProperty; | ||
} | ||
if (innerPropertyAccess.name.escapedText === "prototype") { | ||
if (lhs.expression.name.escapedText === "prototype") { | ||
return SpecialPropertyAssignmentKind.PrototypeProperty; | ||
} | ||
} |
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 preceding changes in this function are just a refactor, except for the following 3 lines.
else if (name.kind === SyntaxKind.PropertyAccessExpression) { | ||
left = name.expression; | ||
} | ||
else { |
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 else clause can never happen because on line 2004 we just checked that name.kind is QualifiedName or PropertyAccessExpression. So I deleted it.
and module.exports.
Previously, this would assert: ```ts exports.undeclared.n = 1; ``` Because undeclared was never declared in any recognised way. Now it no longer asserts, but does not bind. That's because the full pattern starts with the line `exports = require('./x')` and assumes that x.js declares `undeclared`. I am not sure how to bind this. The new test contains this pattern in case I figure it out.
Outer.Inner = function () {} | ||
Outer.Inner.prototype = { | ||
x: 1, | ||
m() { } |
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.
We should also check/enforce that a __proto__
member in the RHS acts as a base type for the class type (this is also present in the devtools codebase).
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.
Ah, Annex B... 😭
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.
__proto__
was sensible ish in a pre-Object.setPrototypeOf
world. :P
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.
But there was Object.create
:(
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’ll add it to the Big List of JavaScript We Could Do Better. I don’t wa t to put it in this PR though — I have t touched the inheritance code for a while.
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.
K.
src/compiler/binder.ts
Outdated
} | ||
|
||
function forEachIdentifierInEntityName(e: EntityNameExpression, action: (e: Identifier, symbol: Symbol, k: EntityNameExpression) => Symbol): Symbol { | ||
if (isIdentifier(e)) { |
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.
should handle module.exports and exports as a terminal case instead of in the callers
src/compiler/binder.ts
Outdated
declareSymbol(file.symbol.exports, file.symbol, <PropertyAccessExpression>node.left, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None); | ||
const lhs = node.left as PropertyAccessEntityNameExpression; | ||
const symbol = forEachIdentifierInEntityName(lhs.expression, (id, original, e) => { | ||
if (isExportsOrModuleExportsOrAlias(file, e) || (isIdentifier(e) && e.escapedText === "module" && original === undefined)) { |
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 do not think we need to handle "module" in a special way. we probably should make forEachIdentifierInEntityName handle the terminal case correctly being identifier
| exports
| module.exports
, and return the correct symbol in each case.
src/compiler/utilities.ts
Outdated
} | ||
|
||
export function getJavascriptInitializer(initializer: Expression) { | ||
if (isCallExpression(initializer)) { |
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.
we have a function getImmediatelyInvokedFunctionExpression
that is almost the same
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.
It is backwards: You give it a function and it climbs up past parentheses to check with there is a parent that is a CallExpression. This code starts with a CallExpression, climbs down past parentheses, and checks whether the expression is a function. I don't think it's worth undoing the work just to use getImmeidatelyInvokedFunctionExpression
. It might be worthwhile to put this in utilities, except I'm not sure what to call it.
src/compiler/utilities.ts
Outdated
} | ||
if (isObjectLiteralExpression(initializer) && | ||
(initializer.properties.length === 0 || | ||
isBinaryExpression(initializer.parent) && isPropertyAccessExpression(initializer.parent.left) && initializer.parent.left.name.escapedText === "prototype")) { |
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.
kinda weird to be checking up the parent chain here.. can we do it higher up instead, e.g. getDeclaredJavascriptInitializer
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.
We only check up the parent chain because non-empty object literals are allowed for protoype assignments. I think it is a little cleaner to calculate whether this is true and pass it in, although it's wordier, so I'll go with that.
As we discussed, I'll probably remove most restrictions on initializer kind soon anyway, so these functions will probably be simpler soon.
src/compiler/utilities.ts
Outdated
(getJavascriptInitializer(node.parent.right) || getDefaultedJavascriptInitializer(node.parent.left as EntityNameExpression, node.parent.right)); | ||
} | ||
|
||
export function getJavascriptInitializer(initializer: Expression) { |
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.
Please add a comment listing the patterns that his function is supporting.
src/compiler/utilities.ts
Outdated
} | ||
} | ||
|
||
export function getAssignedJavascriptInitializer(node: Node) { |
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.
where else do we need the BarBartoken handling that is not in getDefaultedJavascriptInitializer? can we consolidate this in one place.
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.
Nowhere, it's just for defaulted initializers. I don't think the property access check is strictly needed, so I'll just move the BarBarToken check into getDefaultedJavascriptInitializer.
src/compiler/utilities.ts
Outdated
} | ||
} | ||
|
||
function getDefaultedJavascriptInitializer(name: EntityNameExpression, initializer: Expression) { |
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.
can you add comments on all of these functions.. they have very similar names.
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 we need to handle the crashes you were running into before pushing this one.
1. Add documentation 2. Better organisation of concerns in utility functions 3. Better handling of module.exports and exports in the binder's new code.
I addressed the comments and will take a look at the crashes tomorrow. I have fairly compact repros that should be easy to package as fourslash tests. |
Note that the error location is misleading because it's reported inside the merge step for the js initializer.
when the JS symbol is a JS initializer
Object literals as declarations in JS
Starting in 2.7, Typescript supported empty object literals as namespace declarations in Javascript. Before that, it supported functions and classes as namespace declarations:
This PR adds support for top-level assignments as declarations. A var or const declaration is not required.
IIFEs as namespace declarations
This PR assumes that the IIFE returns a function, class or empty object literal, but does not verify it:
Defaulted declarations
"Defaulted declarations" allow initializers that reference the declared name in the left side of a logical or:
Prototype assignment
You can assign an object literal directly to the prototype property. Invididual prototype assignments still work too:
Nested and merged declarations
Nesting works to any level now, and merges correctly across files. Previously neither was the case. The binder now speculatively creates nested symbols for top-level assignments and then the checker performs an extra merge of symbols with the new JSContainer flag.
Fix function containers
Previously functions weren't marked as a JS container when they didn't have any nested properties. This meant that type resolution didn't work:
Fixes #7632