-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Support declaration emit for late bound element accesses assigned to functions in both TS and JS #36593
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
Support declaration emit for late bound element accesses assigned to functions in both TS and JS #36593
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3057,9 +3057,20 @@ namespace ts { | |
function resolveExternalModuleSymbol(moduleSymbol: Symbol | undefined, dontResolveAlias?: boolean): Symbol | undefined; | ||
function resolveExternalModuleSymbol(moduleSymbol: Symbol, dontResolveAlias?: boolean): Symbol { | ||
if (moduleSymbol) { | ||
const links = getSymbolLinks(moduleSymbol); | ||
if (!dontResolveAlias) { | ||
links.resolvedExternalModuleSymbol = "circular"; | ||
} | ||
const exportEquals = resolveSymbol(moduleSymbol.exports!.get(InternalSymbolName.ExportEquals), dontResolveAlias); | ||
const exported = getCommonJsExportEquals(getMergedSymbol(exportEquals), getMergedSymbol(moduleSymbol)); | ||
return getMergedSymbol(exported) || moduleSymbol; | ||
const result = getMergedSymbol(exported) || moduleSymbol; | ||
if (!dontResolveAlias) { | ||
// we "cache" this result, but because both the input _and_ the output are mergeable, literally every call could need | ||
// a different result, assuming the inputs and outputs get merged with. So instead we just use this as a flag that | ||
// export assignment resolution has occured on this symbol at least once. (because it might happen many times!) | ||
links.resolvedExternalModuleSymbol = result; | ||
} | ||
return result; | ||
} | ||
return undefined!; | ||
} | ||
|
@@ -5531,7 +5542,7 @@ namespace ts { | |
function serializeSymbolWorker(symbol: Symbol, isPrivate: boolean, propertyAsAlias: boolean) { | ||
const symbolName = unescapeLeadingUnderscores(symbol.escapedName); | ||
const isDefault = symbol.escapedName === InternalSymbolName.Default; | ||
if (isStringANonContextualKeyword(symbolName) && !isDefault) { | ||
if ((isStringANonContextualKeyword(symbolName) || !isIdentifierText(symbolName, languageVersion)) && !isDefault && symbol.escapedName !== InternalSymbolName.ExportEquals) { | ||
// Oh no. We cannot use this symbol's name as it's name... It's likely some jsdoc had an invalid name like `export` or `default` :( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: |
||
context.encounteredError = true; | ||
// TODO: Issue error via symbol tracker? | ||
|
@@ -5682,7 +5693,8 @@ namespace ts { | |
} | ||
|
||
function getNamespaceMembersForSerialization(symbol: Symbol) { | ||
return !symbol.exports ? [] : filter(arrayFrom((symbol.exports).values()), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); | ||
const exports = getExportsOfSymbol(symbol); | ||
return !exports ? [] : filter(arrayFrom((exports).values()), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); | ||
} | ||
|
||
function isTypeOnlyNamespace(symbol: Symbol) { | ||
|
@@ -8884,6 +8896,17 @@ namespace ts { | |
function getResolvedMembersOrExportsOfSymbol(symbol: Symbol, resolutionKind: MembersOrExportsResolutionKind): UnderscoreEscapedMap<Symbol> { | ||
const links = getSymbolLinks(symbol); | ||
if (!links[resolutionKind]) { | ||
// We _must_ resolve any possible commonjs export assignments _before_ merging in late bound members, as cjs export assignments may cause | ||
// things to be merged (?!?) into this symbol; moreover, we _can't_ resolve those export assignments if we're already resolving the containing | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you give an example test of this? I think I can imagine it, but I may be thinking of something different. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. function Foo() {
}
const lateBound = "field";
Foo[lateBound] = 12;
Foo.A = function() {}
/**
* @typedef {number | string} FooProp
*/
module.exports = Foo.A; The export assignment is the A prop of Foo, which means we need the full list of Foo's exports to check if A is a member (and what it is), which means we need to do a cjs merge to merge the typedefs into the exports, which means we need to know the target of Foo.A, which means we need the A member of Foo, which means we need Foo's exports, which means.... Yeah, the process is inherently circular. This peek short circuits it, since symbol merging makes all the partial results layer together in the end anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I’m missing how we get to here... did you mean to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nope. I did not.
The "top level module" is defined by a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ohhh, yep, I get it now. Thanks 👍 |
||
// module (as then we'll issue a circularity error) | ||
Comment on lines
+8899
to
+8901
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I haven’t approved this PR is because I don’t really understand this (or really, why changes in alias resolution were involved generally), so I was hoping someone else would. But since nobody else has come by, if you want to expound on this, I’ll take a closer look. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree; it seems like this is needed to support checking, not necessarily declaration emit, since it skips late-bound symbols anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The checking changed because the declaration emit made it evident it was wrong. We were using |
||
const p = symbol.valueDeclaration?.parent; | ||
if (p && isSourceFile(p) && p.symbol && !getSymbolLinks(p.symbol).resolvedExternalModuleSymbol) { | ||
const exported = resolveExternalModuleSymbol(p.symbol); | ||
const targetLinks = exported && getSymbolLinks(exported); | ||
if (targetLinks && targetLinks[resolutionKind]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that works |
||
return links[resolutionKind] = targetLinks[resolutionKind]!; | ||
} | ||
} | ||
const isStatic = resolutionKind === MembersOrExportsResolutionKind.resolvedExports; | ||
const earlySymbols = !isStatic ? symbol.members : | ||
symbol.flags & SymbolFlags.Module ? getExportsOfModuleWorker(symbol) : | ||
|
@@ -35082,8 +35105,8 @@ namespace ts { | |
} | ||
} | ||
|
||
function createTypeOfDeclaration(declarationIn: AccessorDeclaration | VariableLikeDeclaration | PropertyAccessExpression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker, addUndefined?: boolean) { | ||
const declaration = getParseTreeNode(declarationIn, isVariableLikeOrAccessor); | ||
function createTypeOfDeclaration(declarationIn: AccessorDeclaration | VariableLikeDeclaration | PropertyAccessExpression | ElementAccessExpression | BinaryExpression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker, addUndefined?: boolean) { | ||
const declaration = getParseTreeNode(declarationIn, isPossiblyPropertyLikeDeclaration); | ||
if (!declaration) { | ||
return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1157,13 +1157,20 @@ namespace ts { | |
fakespace.locals = createSymbolTable(props); | ||
fakespace.symbol = props[0].parent!; | ||
const declarations = mapDefined(props, p => { | ||
if (!isPropertyAccessExpression(p.valueDeclaration)) { | ||
return undefined; // TODO GH#33569: Handle element access expressions that created late bound names (rather than silently omitting them) | ||
if (!isPropertyAccessExpression(p.valueDeclaration) && !isElementAccessExpression(p.valueDeclaration) && !isBinaryExpression(p.valueDeclaration)) { | ||
return undefined; | ||
} | ||
if (hasDynamicName(p.valueDeclaration) && !resolver.isLateBound(getParseTreeNode(p.valueDeclaration) as Declaration)) { | ||
return undefined; | ||
} | ||
const name = unescapeLeadingUnderscores(p.escapedName); | ||
if (!isIdentifierText(name, ScriptTarget.ES3)) { | ||
return undefined; // TODO: Rather than quietly eliding (as is current behavior), maybe we should issue errors? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issuing errors would essentially conflict with this PR, though, right? It seems like this PR's intent is to elide. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issuing errors would be a breaking change, since our past behavior with late bound stuff has always been to elide what we couldn't emit. Still, it means we're quietly omitting type information from the declaration file. Usually that'd warrant an error in any other scenario, imo. |
||
} | ||
getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(p.valueDeclaration); | ||
const type = resolver.createTypeOfDeclaration(p.valueDeclaration, fakespace, declarationEmitNodeBuilderFlags, symbolTracker); | ||
getSymbolAccessibilityDiagnostic = oldDiag; | ||
const varDecl = createVariableDeclaration(unescapeLeadingUnderscores(p.escapedName), type, /*initializer*/ undefined); | ||
const varDecl = createVariableDeclaration(name, type, /*initializer*/ undefined); | ||
return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([varDecl])); | ||
}); | ||
const namespaceDecl = createModuleDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name!, createModuleBlock(declarations), NodeFlags.Namespace); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,9 @@ namespace ts { | |
| TypeAliasDeclaration | ||
| ConstructorDeclaration | ||
| IndexSignatureDeclaration | ||
| PropertyAccessExpression; | ||
| PropertyAccessExpression | ||
| ElementAccessExpression | ||
| BinaryExpression; | ||
|
||
export function canProduceDiagnostics(node: Node): node is DeclarationDiagnosticProducing { | ||
return isVariableDeclaration(node) || | ||
|
@@ -48,7 +50,9 @@ namespace ts { | |
isTypeAliasDeclaration(node) || | ||
isConstructorDeclaration(node) || | ||
isIndexSignatureDeclaration(node) || | ||
isPropertyAccessExpression(node); | ||
isPropertyAccessExpression(node) || | ||
isElementAccessExpression(node) || | ||
isBinaryExpression(node); | ||
} | ||
|
||
export function createGetSymbolAccessibilityDiagnosticForNodeName(node: DeclarationDiagnosticProducing) { | ||
|
@@ -125,7 +129,7 @@ namespace ts { | |
} | ||
|
||
export function createGetSymbolAccessibilityDiagnosticForNode(node: DeclarationDiagnosticProducing): (symbolAccessibilityResult: SymbolAccessibilityResult) => SymbolAccessibilityDiagnostic | undefined { | ||
if (isVariableDeclaration(node) || isPropertyDeclaration(node) || isPropertySignature(node) || isPropertyAccessExpression(node) || isBindingElement(node) || isConstructorDeclaration(node)) { | ||
if (isVariableDeclaration(node) || isPropertyDeclaration(node) || isPropertySignature(node) || isPropertyAccessExpression(node) || isElementAccessExpression(node) || isBinaryExpression(node) || isBindingElement(node) || isConstructorDeclaration(node)) { | ||
return getVariableDeclarationTypeVisibilityError; | ||
} | ||
else if (isSetAccessor(node) || isGetAccessor(node)) { | ||
|
@@ -166,7 +170,7 @@ namespace ts { | |
} | ||
// This check is to ensure we don't report error on constructor parameter property as that error would be reported during parameter emit | ||
// The only exception here is if the constructor was marked as private. we are not emitting the constructor parameters at all. | ||
else if (node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.PropertySignature || | ||
else if (node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.ElementAccessExpression || node.kind === SyntaxKind.BinaryExpression || node.kind === SyntaxKind.PropertySignature || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would be nice to break these long lines |
||
(node.kind === SyntaxKind.Parameter && hasModifier(node.parent, ModifierFlags.Private))) { | ||
// TODO(jfreeman): Deal with computed properties in error reporting. | ||
if (hasModifier(node, ModifierFlags.Static)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
//// [lateBoundElementAccessAssignmentDeclarations.ts] | ||
export function foo() {} | ||
foo.bar = 12; | ||
const _private = Symbol(); | ||
foo[_private] = "ok"; | ||
const strMem = "strMemName"; | ||
foo[strMem] = "ok"; | ||
const dashStrMem = "dashed-str-mem"; | ||
foo[dashStrMem] = "ok"; | ||
const numMem = 42; | ||
foo[numMem] = "ok"; | ||
|
||
const x: string = foo[_private]; | ||
const y: string = foo[strMem]; | ||
const z: string = foo[numMem]; | ||
const a: string = foo[dashStrMem]; | ||
|
||
//// [lateBoundElementAccessAssignmentDeclarations.js] | ||
export function foo() { } | ||
foo.bar = 12; | ||
const _private = Symbol(); | ||
foo[_private] = "ok"; | ||
const strMem = "strMemName"; | ||
foo[strMem] = "ok"; | ||
const dashStrMem = "dashed-str-mem"; | ||
foo[dashStrMem] = "ok"; | ||
const numMem = 42; | ||
foo[numMem] = "ok"; | ||
const x = foo[_private]; | ||
const y = foo[strMem]; | ||
const z = foo[numMem]; | ||
const a = foo[dashStrMem]; | ||
|
||
|
||
//// [lateBoundElementAccessAssignmentDeclarations.d.ts] | ||
export declare function foo(): void; | ||
export declare namespace foo { | ||
var bar: number; | ||
var strMemName: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
=== tests/cases/compiler/lateBoundElementAccessAssignmentDeclarations.ts === | ||
export function foo() {} | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
|
||
foo.bar = 12; | ||
>foo.bar : Symbol(foo.bar, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24)) | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>bar : Symbol(foo.bar, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24)) | ||
|
||
const _private = Symbol(); | ||
>_private : Symbol(_private, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 5)) | ||
>Symbol : Symbol(Symbol, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.symbol.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) | ||
|
||
foo[_private] = "ok"; | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>_private : Symbol(_private, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 5)) | ||
|
||
const strMem = "strMemName"; | ||
>strMem : Symbol(strMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 5)) | ||
|
||
foo[strMem] = "ok"; | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>strMem : Symbol(strMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 5)) | ||
|
||
const dashStrMem = "dashed-str-mem"; | ||
>dashStrMem : Symbol(dashStrMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 5)) | ||
|
||
foo[dashStrMem] = "ok"; | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>dashStrMem : Symbol(dashStrMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 5)) | ||
|
||
const numMem = 42; | ||
>numMem : Symbol(numMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 8, 5)) | ||
|
||
foo[numMem] = "ok"; | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>numMem : Symbol(numMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 8, 5)) | ||
|
||
const x: string = foo[_private]; | ||
>x : Symbol(x, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 11, 5)) | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>_private : Symbol(_private, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 5)) | ||
|
||
const y: string = foo[strMem]; | ||
>y : Symbol(y, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 12, 5)) | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>strMem : Symbol(strMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 5)) | ||
|
||
const z: string = foo[numMem]; | ||
>z : Symbol(z, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 13, 5)) | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>numMem : Symbol(numMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 8, 5)) | ||
|
||
const a: string = foo[dashStrMem]; | ||
>a : Symbol(a, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 14, 5)) | ||
>foo : Symbol(foo, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 0), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 0, 24), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 2, 26), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 4, 28), Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 36) ... and 1 more) | ||
>dashStrMem : Symbol(dashStrMem, Decl(lateBoundElementAccessAssignmentDeclarations.ts, 6, 5)) | ||
|
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.
so this is basically just circularity detection? why do you need an initial value
"circular"
then? I think I'm misunderstanding your comments.typo: occurred
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.
So I have a value to flag before the result is ready to handle the reentrant case.