diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 131dbc38cf74c..5d905f930c6ac 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -507,6 +507,7 @@ namespace ts { }, getAugmentedPropertiesOfType, getRootSymbols, + getSymbolOfExpando, getContextualType: (nodeIn: Expression, contextFlags?: ContextFlags) => { const node = getParseTreeNode(nodeIn, isExpression); if (!node) { @@ -8728,9 +8729,9 @@ namespace ts { let links = getSymbolLinks(symbol); const originalLinks = links; if (!links.type) { - const jsDeclaration = symbol.valueDeclaration && getDeclarationOfExpando(symbol.valueDeclaration); - if (jsDeclaration) { - const merged = mergeJSSymbols(symbol, getSymbolOfNode(jsDeclaration)); + const expando = symbol.valueDeclaration && getSymbolOfExpando(symbol.valueDeclaration, /*allowDeclaration*/ false); + if (expando) { + const merged = mergeJSSymbols(symbol, expando); if (merged) { // note:we overwrite links because we just cloned the symbol symbol = links = merged; @@ -24828,7 +24829,7 @@ namespace ts { const exprType = checkJsxAttribute(attributeDecl, checkMode); objectFlags |= getObjectFlags(exprType) & ObjectFlags.PropagatingFlags; - const attributeSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient | member.flags, member.escapedName); + const attributeSymbol = createSymbol(SymbolFlags.Property | member.flags, member.escapedName); attributeSymbol.declarations = member.declarations; attributeSymbol.parent = member.parent; if (member.valueDeclaration) { @@ -24887,7 +24888,7 @@ namespace ts { const contextualType = getApparentTypeOfContextualType(openingLikeElement.attributes); const childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName); // If there are children in the body of JSX element, create dummy attribute "children" with the union of children types so that it will pass the attribute checking process - const childrenPropSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, jsxChildrenPropertyName); + const childrenPropSymbol = createSymbol(SymbolFlags.Property, jsxChildrenPropertyName); childrenPropSymbol.type = childrenTypes.length === 1 ? childrenTypes[0] : childrenContextualType && forEachType(childrenContextualType, isTupleLikeType) ? createTupleType(childrenTypes) : createArrayType(getUnionType(childrenTypes)); @@ -28076,15 +28077,59 @@ namespace ts { } function getAssignedClassSymbol(decl: Declaration): Symbol | undefined { - const assignmentSymbol = decl && decl.parent && - (isFunctionDeclaration(decl) && getSymbolOfNode(decl) || - isBinaryExpression(decl.parent) && getSymbolOfNode(decl.parent.left) || - isVariableDeclaration(decl.parent) && getSymbolOfNode(decl.parent)); - const prototype = assignmentSymbol && assignmentSymbol.exports && assignmentSymbol.exports.get("prototype" as __String); - const init = prototype && prototype.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); + const assignmentSymbol = decl && getSymbolOfExpando(decl, /*allowDeclaration*/ true); + const prototype = assignmentSymbol?.exports?.get("prototype" as __String); + const init = prototype?.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); return init ? getSymbolOfNode(init) : undefined; } + function getSymbolOfExpando(node: Node, allowDeclaration: boolean): Symbol | undefined { + if (!node.parent) { + return undefined; + } + let name: Expression | BindingName | undefined; + let decl: Node | undefined; + if (isVariableDeclaration(node.parent) && node.parent.initializer === node) { + if (!isInJSFile(node) && !isVarConst(node.parent)) { + return undefined; + } + name = node.parent.name; + decl = node.parent; + } + else if (isBinaryExpression(node.parent)) { + const parentNode = node.parent; + const parentNodeOperator = node.parent.operatorToken.kind; + if (parentNodeOperator === SyntaxKind.EqualsToken && (allowDeclaration || parentNode.right === node)) { + name = parentNode.left; + decl = name; + } + else if (parentNodeOperator === SyntaxKind.BarBarToken || parentNodeOperator === SyntaxKind.QuestionQuestionToken) { + if (isVariableDeclaration(parentNode.parent) && parentNode.parent.initializer === parentNode) { + name = parentNode.parent.name; + decl = parentNode.parent; + } + else if (isBinaryExpression(parentNode.parent) && parentNode.parent.operatorToken.kind === SyntaxKind.EqualsToken && (allowDeclaration || parentNode.parent.right === parentNode)) { + name = parentNode.parent.left; + decl = name; + } + + if (!name || !isBindableStaticNameExpression(name) || !isSameEntityName(name, parentNode.left)) { + return undefined; + } + } + } + else if (allowDeclaration && isFunctionDeclaration(node)) { + name = node.name; + decl = node; + } + + if (!decl || !name || (!allowDeclaration && !getExpandoInitializer(node, isPrototypeAccess(name)))) { + return undefined; + } + return getSymbolOfNode(decl); + } + + function getAssignedJSPrototype(node: Node) { if (!node.parent) { return false; @@ -28161,14 +28206,11 @@ namespace ts { } if (isInJSFile(node)) { - const decl = getDeclarationOfExpando(node); - if (decl) { - const jsSymbol = getSymbolOfNode(decl); - if (jsSymbol?.exports?.size) { - const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined); - jsAssignmentType.objectFlags |= ObjectFlags.JSLiteral; - return getIntersectionType([returnType, jsAssignmentType]); - } + const jsSymbol = getSymbolOfExpando(node, /*allowDeclaration*/ false); + if (jsSymbol?.exports?.size) { + const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined); + jsAssignmentType.objectFlags |= ObjectFlags.JSLiteral; + return getIntersectionType([returnType, jsAssignmentType]); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a584cf7a0994e..dbc916cd56756 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4004,6 +4004,7 @@ namespace ts { getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): readonly Symbol[]; + getSymbolOfExpando(node: Node, allowDeclaration: boolean): Symbol | undefined; getContextualType(node: Expression): Type | undefined; /* @internal */ getContextualType(node: Expression, contextFlags?: ContextFlags): Type | undefined; // eslint-disable-line @typescript-eslint/unified-signatures /* @internal */ getContextualTypeForObjectLiteralElement(element: ObjectLiteralElementLike): Type | undefined; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 51ba41c5ca7fa..95f81b987aaba 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1937,48 +1937,6 @@ namespace ts { return getSourceTextOfNodeFromSourceFile(sourceFile, str).charCodeAt(0) === CharacterCodes.doubleQuote; } - export function getDeclarationOfExpando(node: Node): Node | undefined { - if (!node.parent) { - return undefined; - } - let name: Expression | BindingName | undefined; - let decl: Node | undefined; - if (isVariableDeclaration(node.parent) && node.parent.initializer === node) { - if (!isInJSFile(node) && !isVarConst(node.parent)) { - return undefined; - } - name = node.parent.name; - decl = node.parent; - } - else if (isBinaryExpression(node.parent)) { - const parentNode = node.parent; - const parentNodeOperator = node.parent.operatorToken.kind; - if (parentNodeOperator === SyntaxKind.EqualsToken && parentNode.right === node) { - name = parentNode.left; - decl = name; - } - else if (parentNodeOperator === SyntaxKind.BarBarToken || parentNodeOperator === SyntaxKind.QuestionQuestionToken) { - if (isVariableDeclaration(parentNode.parent) && parentNode.parent.initializer === parentNode) { - name = parentNode.parent.name; - decl = parentNode.parent; - } - else if (isBinaryExpression(parentNode.parent) && parentNode.parent.operatorToken.kind === SyntaxKind.EqualsToken && parentNode.parent.right === parentNode) { - name = parentNode.parent.left; - decl = name; - } - - if (!name || !isBindableStaticNameExpression(name) || !isSameEntityName(name, parentNode.left)) { - return undefined; - } - } - } - - if (!name || !getExpandoInitializer(node, isPrototypeAccess(name))) { - return undefined; - } - return decl; - } - export function isAssignmentDeclaration(decl: Declaration) { return isBinaryExpression(decl) || isAccessExpression(decl) || isIdentifier(decl) || isCallExpression(decl); } @@ -2098,7 +2056,7 @@ namespace ts { * var min = window.min || {} * my.app = self.my.app || class { } */ - function isSameEntityName(name: Expression, initializer: Expression): boolean { + export function isSameEntityName(name: Expression, initializer: Expression): boolean { if (isPropertyNameLiteral(name) && isPropertyNameLiteral(initializer)) { return getTextOfIdentifierOrLiteral(name) === getTextOfIdentifierOrLiteral(initializer); } diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index a8c50322522a1..8d557d4edcb2d 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -37,7 +37,7 @@ namespace ts { function check(node: Node) { if (isJsFile) { - if (canBeConvertedToClass(node)) { + if (canBeConvertedToClass(node, checker)) { diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); } } @@ -190,14 +190,13 @@ namespace ts { return `${exp.pos.toString()}:${exp.end.toString()}`; } - function canBeConvertedToClass(node: Node): boolean { + function canBeConvertedToClass(node: Node, checker: TypeChecker): boolean { if (node.kind === SyntaxKind.FunctionExpression) { if (isVariableDeclaration(node.parent) && node.symbol.members?.size) { return true; } - const decl = getDeclarationOfExpando(node); - const symbol = decl?.symbol; + const symbol = checker.getSymbolOfExpando(node, /*allowDeclaration*/ false); return !!(symbol && (symbol.exports?.size || symbol.members?.size)); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a4f5a9a74dec2..15653ecacf258 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2207,6 +2207,7 @@ declare namespace ts { getFullyQualifiedName(symbol: Symbol): string; getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): readonly Symbol[]; + getSymbolOfExpando(node: Node, allowDeclaration: boolean): Symbol | undefined; getContextualType(node: Expression): Type | undefined; /** * returns unknownSignature in the case of an error. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 5bff8c8059e60..26cdf6ba46056 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2207,6 +2207,7 @@ declare namespace ts { getFullyQualifiedName(symbol: Symbol): string; getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): readonly Symbol[]; + getSymbolOfExpando(node: Node, allowDeclaration: boolean): Symbol | undefined; getContextualType(node: Expression): Type | undefined; /** * returns unknownSignature in the case of an error. diff --git a/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.symbols b/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.symbols new file mode 100644 index 0000000000000..dbe951dcf54f1 --- /dev/null +++ b/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.symbols @@ -0,0 +1,33 @@ +=== tests/cases/conformance/salsa/bug39167.js === +var test = {}; +>test : Symbol(test, Decl(bug39167.js, 0, 3), Decl(bug39167.js, 0, 14), Decl(bug39167.js, 2, 18)) + +test.K = test.K || +>test.K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>test : Symbol(test, Decl(bug39167.js, 0, 3), Decl(bug39167.js, 0, 14), Decl(bug39167.js, 2, 18)) +>K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>test.K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>test : Symbol(test, Decl(bug39167.js, 0, 3), Decl(bug39167.js, 0, 14), Decl(bug39167.js, 2, 18)) +>K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) + + function () {} + +test.K.prototype = { +>test.K.prototype : Symbol(test.K.prototype, Decl(bug39167.js, 2, 18)) +>test.K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>test : Symbol(test, Decl(bug39167.js, 0, 3), Decl(bug39167.js, 0, 14), Decl(bug39167.js, 2, 18)) +>K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>prototype : Symbol(test.K.prototype, Decl(bug39167.js, 2, 18)) + + add() {} +>add : Symbol(add, Decl(bug39167.js, 4, 20)) + +}; + +new test.K().add; +>new test.K().add : Symbol(add, Decl(bug39167.js, 4, 20)) +>test.K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>test : Symbol(test, Decl(bug39167.js, 0, 3), Decl(bug39167.js, 0, 14), Decl(bug39167.js, 2, 18)) +>K : Symbol(test.K, Decl(bug39167.js, 0, 14), Decl(bug39167.js, 4, 5)) +>add : Symbol(add, Decl(bug39167.js, 4, 20)) + diff --git a/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.types b/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.types new file mode 100644 index 0000000000000..81f4e5bc64129 --- /dev/null +++ b/tests/baselines/reference/defaultPropertyAssignedClassWithPrototype.types @@ -0,0 +1,40 @@ +=== tests/cases/conformance/salsa/bug39167.js === +var test = {}; +>test : typeof test +>{} : {} + +test.K = test.K || +>test.K = test.K || function () {} : typeof K +>test.K : typeof K +>test : typeof test +>K : typeof K +>test.K || function () {} : typeof K +>test.K : typeof K +>test : typeof test +>K : typeof K + + function () {} +>function () {} : typeof K + +test.K.prototype = { +>test.K.prototype = { add() {}} : { add(): void; } +>test.K.prototype : { add(): void; } +>test.K : typeof K +>test : typeof test +>K : typeof K +>prototype : { add(): void; } +>{ add() {}} : { add(): void; } + + add() {} +>add : () => void + +}; + +new test.K().add; +>new test.K().add : () => void +>new test.K() : K +>test.K : typeof K +>test : typeof test +>K : typeof K +>add : () => void + diff --git a/tests/cases/conformance/salsa/defaultPropertyAssignedClassWithPrototype.ts b/tests/cases/conformance/salsa/defaultPropertyAssignedClassWithPrototype.ts new file mode 100644 index 0000000000000..a4876b7cab3bf --- /dev/null +++ b/tests/cases/conformance/salsa/defaultPropertyAssignedClassWithPrototype.ts @@ -0,0 +1,13 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @Filename: bug39167.js +var test = {}; +test.K = test.K || + function () {} + +test.K.prototype = { + add() {} +}; + +new test.K().add;