diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index bab6ca7b360..9c7756d0d30 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2080,8 +2080,8 @@ namespace ts { if (isInJavaScriptFile(node) && file.commonJsModuleIndicator && isModuleExportsPropertyAccessExpression(node as PropertyAccessExpression) && - !lookupSymbolForNameWorker(container, "module" as __String)) { - declareSymbol(container.locals!, /*parent*/ undefined, (node as PropertyAccessExpression).expression as Identifier, + !lookupSymbolForNameWorker(blockScopeContainer, "module" as __String)) { + declareSymbol(file.locals!, /*parent*/ undefined, (node as PropertyAccessExpression).expression as Identifier, SymbolFlags.FunctionScopedVariable | SymbolFlags.ModuleExports, SymbolFlags.FunctionScopedVariableExcludes); } break; diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 57de68ab02b..29720ff8532 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16122,7 +16122,14 @@ namespace ts { const { left, operatorToken, right } = binaryExpression; switch (operatorToken.kind) { case SyntaxKind.EqualsToken: - return node === right && isContextSensitiveAssignment(binaryExpression) ? getTypeOfExpression(left) : undefined; + if (node !== right) { + return undefined; + } + const contextSensitive = getIsContextSensitiveAssignmentOrContextType(binaryExpression); + if (!contextSensitive) { + return undefined; + } + return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive; case SyntaxKind.BarBarToken: // When an || expression has a contextual type, the operands are contextually typed by that type. When an || // expression has no contextual type, the right operand is contextually typed by the type of the left operand, @@ -16140,7 +16147,7 @@ namespace ts { // In an assignment expression, the right operand is contextually typed by the type of the left operand. // Don't do this for special property assignments unless there is a type tag on the assignment, to avoid circularity from checking the right operand. - function isContextSensitiveAssignment(binaryExpression: BinaryExpression): boolean { + function getIsContextSensitiveAssignmentOrContextType(binaryExpression: BinaryExpression): boolean | Type { const kind = getSpecialPropertyAssignmentKind(binaryExpression); switch (kind) { case SpecialPropertyAssignmentKind.None: @@ -16159,29 +16166,44 @@ namespace ts { if (!decl) { return false; } - if (isInJavaScriptFile(decl)) { - return !!getJSDocTypeTag(decl); + const lhs = binaryExpression.left as PropertyAccessExpression; + const overallAnnotation = getEffectiveTypeAnnotationNode(decl); + if (overallAnnotation) { + return getTypeFromTypeNode(overallAnnotation); } - else if (isIdentifier((binaryExpression.left as PropertyAccessExpression).expression)) { - const id = (binaryExpression.left as PropertyAccessExpression).expression as Identifier; + else if (isIdentifier(lhs.expression)) { + const id = lhs.expression; const parentSymbol = resolveName(id, id.escapedText, SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true); - return !isFunctionSymbol(parentSymbol); + if (parentSymbol && isFunctionSymbol(parentSymbol)) { + const annotated = getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration); + if (annotated) { + const type = getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), lhs.name.escapedText); + return type || false; + } + return false; + } } - return true; + return !isInJavaScriptFile(decl); } + case SpecialPropertyAssignmentKind.ModuleExports: case SpecialPropertyAssignmentKind.ThisProperty: - if (!binaryExpression.symbol || - binaryExpression.symbol.valueDeclaration && !!getJSDocTypeTag(binaryExpression.symbol.valueDeclaration)) { - return true; + if (!binaryExpression.symbol) return true; + if (binaryExpression.symbol.valueDeclaration) { + const annotated = getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration); + if (annotated) { + const type = getTypeFromTypeNode(annotated); + if (type) { + return type; + } + } } + if (kind === SpecialPropertyAssignmentKind.ModuleExports) return false; const thisAccess = binaryExpression.left as PropertyAccessExpression; if (!isObjectLiteralMethod(getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) { return false; } const thisType = checkThisExpression(thisAccess.expression); - return thisType && !!getPropertyOfType(thisType, thisAccess.name.escapedText); - case SpecialPropertyAssignmentKind.ModuleExports: - return !binaryExpression.symbol || binaryExpression.symbol.valueDeclaration && !!getJSDocTypeTag(binaryExpression.symbol.valueDeclaration); + return thisType && getTypeOfPropertyOfContextualType(thisType, thisAccess.name.escapedText) || false; default: return Debug.assertNever(kind); } diff --git a/tests/baselines/reference/expandoFunctionContextualTypes.js b/tests/baselines/reference/expandoFunctionContextualTypes.js new file mode 100644 index 00000000000..541adf963e4 --- /dev/null +++ b/tests/baselines/reference/expandoFunctionContextualTypes.js @@ -0,0 +1,22 @@ +//// [expandoFunctionContextualTypes.ts] +interface MyComponentProps { + color: "red" | "blue" +} + +interface StatelessComponent

{ + (): any; + defaultProps?: Partial

; +} + +const MyComponent: StatelessComponent = () => null as any; + +MyComponent.defaultProps = { + color: "red" +}; + + +//// [expandoFunctionContextualTypes.js] +var MyComponent = function () { return null; }; +MyComponent.defaultProps = { + color: "red" +}; diff --git a/tests/baselines/reference/expandoFunctionContextualTypes.symbols b/tests/baselines/reference/expandoFunctionContextualTypes.symbols new file mode 100644 index 00000000000..cea0647c762 --- /dev/null +++ b/tests/baselines/reference/expandoFunctionContextualTypes.symbols @@ -0,0 +1,34 @@ +=== tests/cases/compiler/expandoFunctionContextualTypes.ts === +interface MyComponentProps { +>MyComponentProps : Symbol(MyComponentProps, Decl(expandoFunctionContextualTypes.ts, 0, 0)) + + color: "red" | "blue" +>color : Symbol(MyComponentProps.color, Decl(expandoFunctionContextualTypes.ts, 0, 28)) +} + +interface StatelessComponent

{ +>StatelessComponent : Symbol(StatelessComponent, Decl(expandoFunctionContextualTypes.ts, 2, 1)) +>P : Symbol(P, Decl(expandoFunctionContextualTypes.ts, 4, 29)) + + (): any; + defaultProps?: Partial

; +>defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>P : Symbol(P, Decl(expandoFunctionContextualTypes.ts, 4, 29)) +} + +const MyComponent: StatelessComponent = () => null as any; +>MyComponent : Symbol(MyComponent, Decl(expandoFunctionContextualTypes.ts, 9, 5)) +>StatelessComponent : Symbol(StatelessComponent, Decl(expandoFunctionContextualTypes.ts, 2, 1)) +>MyComponentProps : Symbol(MyComponentProps, Decl(expandoFunctionContextualTypes.ts, 0, 0)) + +MyComponent.defaultProps = { +>MyComponent.defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12)) +>MyComponent : Symbol(MyComponent, Decl(expandoFunctionContextualTypes.ts, 9, 5)) +>defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12)) + + color: "red" +>color : Symbol(color, Decl(expandoFunctionContextualTypes.ts, 11, 28)) + +}; + diff --git a/tests/baselines/reference/expandoFunctionContextualTypes.types b/tests/baselines/reference/expandoFunctionContextualTypes.types new file mode 100644 index 00000000000..e1eca92659c --- /dev/null +++ b/tests/baselines/reference/expandoFunctionContextualTypes.types @@ -0,0 +1,31 @@ +=== tests/cases/compiler/expandoFunctionContextualTypes.ts === +interface MyComponentProps { + color: "red" | "blue" +>color : "red" | "blue" +} + +interface StatelessComponent

{ + (): any; + defaultProps?: Partial

; +>defaultProps : Partial

+} + +const MyComponent: StatelessComponent = () => null as any; +>MyComponent : StatelessComponent +>() => null as any : { (): any; defaultProps: { color: "red"; }; } +>null as any : any +>null : null + +MyComponent.defaultProps = { +>MyComponent.defaultProps = { color: "red"} : { color: "red"; } +>MyComponent.defaultProps : Partial +>MyComponent : StatelessComponent +>defaultProps : Partial +>{ color: "red"} : { color: "red"; } + + color: "red" +>color : "red" +>"red" : "red" + +}; + diff --git a/tests/baselines/reference/expandoFunctionContextualTypesJs.symbols b/tests/baselines/reference/expandoFunctionContextualTypesJs.symbols new file mode 100644 index 00000000000..5ef7acd5366 --- /dev/null +++ b/tests/baselines/reference/expandoFunctionContextualTypesJs.symbols @@ -0,0 +1,86 @@ +=== tests/cases/compiler/input.js === +/** @typedef {{ color: "red" | "blue" }} MyComponentProps */ + +/** + * @template P + * @typedef {{ (): any; defaultProps?: Partial

}} StatelessComponent */ + + /** + * @type {StatelessComponent} + */ +const MyComponent = () => /* @type {any} */(null); +>MyComponent : Symbol(MyComponent, Decl(input.js, 9, 5)) + +MyComponent.defaultProps = { +>MyComponent.defaultProps : Symbol(defaultProps, Decl(input.js, 4, 23)) +>MyComponent : Symbol(MyComponent, Decl(input.js, 9, 5)) +>defaultProps : Symbol(defaultProps, Decl(input.js, 4, 23)) + + color: "red" +>color : Symbol(color, Decl(input.js, 11, 28)) + +}; + +const MyComponent2 = () => null; +>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5)) + +/** + * @type {MyComponentProps} + */ +MyComponent2.defaultProps = { +>MyComponent2.defaultProps : Symbol(MyComponent2.defaultProps, Decl(input.js, 15, 32)) +>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5)) +>defaultProps : Symbol(MyComponent2.defaultProps, Decl(input.js, 15, 32)) + + color: "red" +>color : Symbol(color, Decl(input.js, 20, 29)) +} + +/** + * @type {StatelessComponent} + */ +const check = MyComponent2; +>check : Symbol(check, Decl(input.js, 27, 5)) +>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5)) + +/** + * + * @param {{ props: MyComponentProps }} p + */ +function expectLiteral(p) {} +>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27)) +>p : Symbol(p, Decl(input.js, 33, 23)) + +function foo() { +>foo : Symbol(foo, Decl(input.js, 33, 28)) + + /** + * @type {MyComponentProps} + */ + this.props = { color: "red" }; +>props : Symbol(foo.props, Decl(input.js, 35, 16)) +>color : Symbol(color, Decl(input.js, 39, 18)) + + expectLiteral(this); +>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27)) +} + +/** + * @type {MyComponentProps} + */ +module.exports = { +>module.exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0)) +>module : Symbol(export=, Decl(input.js, 42, 1)) +>exports : Symbol(export=, Decl(input.js, 42, 1)) + + color: "red" +>color : Symbol(color, Decl(input.js, 47, 18)) +} + +expectLiteral({ props: module.exports }); +>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27)) +>props : Symbol(props, Decl(input.js, 51, 15)) +>module.exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0)) +>module : Symbol(module, Decl(input.js, 42, 1), Decl(input.js, 51, 22)) +>exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0)) + diff --git a/tests/baselines/reference/expandoFunctionContextualTypesJs.types b/tests/baselines/reference/expandoFunctionContextualTypesJs.types new file mode 100644 index 00000000000..081c57c0dac --- /dev/null +++ b/tests/baselines/reference/expandoFunctionContextualTypesJs.types @@ -0,0 +1,109 @@ +=== tests/cases/compiler/input.js === +/** @typedef {{ color: "red" | "blue" }} MyComponentProps */ + +/** + * @template P + * @typedef {{ (): any; defaultProps?: Partial

}} StatelessComponent */ + + /** + * @type {StatelessComponent} + */ +const MyComponent = () => /* @type {any} */(null); +>MyComponent : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; } +>() => /* @type {any} */(null) : { (): any; defaultProps: { color: "red"; }; } +>(null) : null +>null : null + +MyComponent.defaultProps = { +>MyComponent.defaultProps = { color: "red"} : { color: "red"; } +>MyComponent.defaultProps : Partial<{ color: "red" | "blue"; }> +>MyComponent : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; } +>defaultProps : Partial<{ color: "red" | "blue"; }> +>{ color: "red"} : { color: "red"; } + + color: "red" +>color : "red" +>"red" : "red" + +}; + +const MyComponent2 = () => null; +>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; } +>() => null : { (): any; defaultProps: { color: "red" | "blue"; }; } +>null : null + +/** + * @type {MyComponentProps} + */ +MyComponent2.defaultProps = { +>MyComponent2.defaultProps = { color: "red"} : { color: "red"; } +>MyComponent2.defaultProps : { color: "red" | "blue"; } +>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; } +>defaultProps : { color: "red" | "blue"; } +>{ color: "red"} : { color: "red"; } + + color: "red" +>color : "red" +>"red" : "red" +} + +/** + * @type {StatelessComponent} + */ +const check = MyComponent2; +>check : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; } +>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; } + +/** + * + * @param {{ props: MyComponentProps }} p + */ +function expectLiteral(p) {} +>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void +>p : { props: { color: "red" | "blue"; }; } + +function foo() { +>foo : typeof foo + + /** + * @type {MyComponentProps} + */ + this.props = { color: "red" }; +>this.props = { color: "red" } : { color: "red"; } +>this.props : any +>this : any +>props : any +>{ color: "red" } : { color: "red"; } +>color : "red" +>"red" : "red" + + expectLiteral(this); +>expectLiteral(this) : void +>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void +>this : any +} + +/** + * @type {MyComponentProps} + */ +module.exports = { +>module.exports = { color: "red"} : { color: "red" | "blue"; } +>module.exports : { color: "red" | "blue"; } +>module : { "tests/cases/compiler/input": { color: "red" | "blue"; }; } +>exports : { color: "red" | "blue"; } +>{ color: "red"} : { color: "red"; } + + color: "red" +>color : "red" +>"red" : "red" +} + +expectLiteral({ props: module.exports }); +>expectLiteral({ props: module.exports }) : void +>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void +>{ props: module.exports } : { props: { color: "red" | "blue"; }; } +>props : { color: "red" | "blue"; } +>module.exports : { color: "red" | "blue"; } +>module : { "tests/cases/compiler/input": { color: "red" | "blue"; }; } +>exports : { color: "red" | "blue"; } + diff --git a/tests/baselines/reference/moduleExportAssignment2.symbols b/tests/baselines/reference/moduleExportAssignment2.symbols index 25e80647d89..eaafdbefb1f 100644 --- a/tests/baselines/reference/moduleExportAssignment2.symbols +++ b/tests/baselines/reference/moduleExportAssignment2.symbols @@ -9,7 +9,7 @@ var npm = module.exports = function (tree) { module.exports.asReadInstalled = function (tree) { >module.exports.asReadInstalled : Symbol(asReadInstalled, Decl(npm.js, 1, 1)) >module.exports : Symbol(asReadInstalled, Decl(npm.js, 1, 1)) ->module : Symbol(module, Decl(npm.js, 0, 9)) +>module : Symbol(module, Decl(npm.js, 0, 9), Decl(npm.js, 3, 13)) >exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0)) >asReadInstalled : Symbol(asReadInstalled, Decl(npm.js, 1, 1)) >tree : Symbol(tree, Decl(npm.js, 2, 43)) @@ -20,7 +20,7 @@ module.exports.asReadInstalled = function (tree) { module.exports(tree) >module.exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0)) ->module : Symbol(module, Decl(npm.js, 3, 13)) +>module : Symbol(module, Decl(npm.js, 0, 9), Decl(npm.js, 3, 13)) >exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0)) >tree : Symbol(tree, Decl(npm.js, 2, 43)) } diff --git a/tests/cases/compiler/expandoFunctionContextualTypes.ts b/tests/cases/compiler/expandoFunctionContextualTypes.ts new file mode 100644 index 00000000000..ae624549252 --- /dev/null +++ b/tests/cases/compiler/expandoFunctionContextualTypes.ts @@ -0,0 +1,14 @@ +interface MyComponentProps { + color: "red" | "blue" +} + +interface StatelessComponent

{ + (): any; + defaultProps?: Partial

; +} + +const MyComponent: StatelessComponent = () => null as any; + +MyComponent.defaultProps = { + color: "red" +}; diff --git a/tests/cases/compiler/expandoFunctionContextualTypesJs.ts b/tests/cases/compiler/expandoFunctionContextualTypesJs.ts new file mode 100644 index 00000000000..e69f3dc79c4 --- /dev/null +++ b/tests/cases/compiler/expandoFunctionContextualTypesJs.ts @@ -0,0 +1,57 @@ +// @allowJs: true +// @checkJs: true +// @noEmit: true +// @filename: input.js + +/** @typedef {{ color: "red" | "blue" }} MyComponentProps */ + +/** + * @template P + * @typedef {{ (): any; defaultProps?: Partial

}} StatelessComponent */ + + /** + * @type {StatelessComponent} + */ +const MyComponent = () => /* @type {any} */(null); + +MyComponent.defaultProps = { + color: "red" +}; + +const MyComponent2 = () => null; + +/** + * @type {MyComponentProps} + */ +MyComponent2.defaultProps = { + color: "red" +} + +/** + * @type {StatelessComponent} + */ +const check = MyComponent2; + +/** + * + * @param {{ props: MyComponentProps }} p + */ +function expectLiteral(p) {} + +function foo() { + /** + * @type {MyComponentProps} + */ + this.props = { color: "red" }; + + expectLiteral(this); +} + +/** + * @type {MyComponentProps} + */ +module.exports = { + color: "red" +} + +expectLiteral({ props: module.exports });