diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index a24012cd4e3c1..81da5a0d82fa1 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1394,7 +1394,8 @@ namespace ts.FindAllReferences.Core { // If the location is in a context sensitive location (i.e. in an object literal) try // to get a contextual type for it, and add the property symbol from the contextual // type to the search set - const res = firstDefined(getPropertySymbolsFromContextualType(containingObjectLiteralElement, checker), fromRoot); + const contextualType = checker.getContextualType(containingObjectLiteralElement.parent); + const res = contextualType && firstDefined(getPropertySymbolsFromContextualType(containingObjectLiteralElement, checker, contextualType, /*unionSymbolOk*/ true), fromRoot); if (res) return res; // If the location is name of property symbol from object literal destructuring pattern @@ -1462,17 +1463,6 @@ namespace ts.FindAllReferences.Core { !(search.parents && !search.parents.some(parent => explicitlyInheritsFrom(rootSymbol.parent!, parent, state.inheritsFromCache, checker)))); } - /** Gets all symbols for one property. Does not get symbols for every property. */ - function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker): ReadonlyArray { - const contextualType = checker.getContextualType(node.parent); - if (!contextualType) return emptyArray; - const name = getNameFromPropertyName(node.name); - if (!name) return emptyArray; - const symbol = contextualType.getProperty(name); - return symbol ? [symbol] : - contextualType.isUnion() ? mapDefined(contextualType.types, t => t.getProperty(name)) : emptyArray; - } - /** * Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations * of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index 1d0112630108a..628989b006e09 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -69,11 +69,9 @@ namespace ts.GoToDefinition { if (isPropertyName(node) && isBindingElement(parent) && isObjectBindingPattern(parent.parent) && (node === (parent.propertyName || parent.name))) { const type = typeChecker.getTypeAtLocation(parent.parent); - if (type) { - const propSymbols = getPropertySymbolsFromType(type, node); - if (propSymbols) { - return flatMap(propSymbols, propSymbol => getDefinitionFromSymbol(typeChecker, propSymbol, node)); - } + const propSymbols = getPropertySymbolsFromType(type, node); + if (propSymbols) { + return flatMap(propSymbols, propSymbol => getDefinitionFromSymbol(typeChecker, propSymbol, node)); } } @@ -87,9 +85,12 @@ namespace ts.GoToDefinition { // function Foo(arg: Props) {} // Foo( { pr/*1*/op1: 10, prop2: true }) const element = getContainingObjectLiteralElement(node); - if (element && typeChecker.getContextualType(element.parent as Expression)) { - return flatMap(getPropertySymbolsFromContextualType(typeChecker, element), propertySymbol => - getDefinitionFromSymbol(typeChecker, propertySymbol, node)); + if (element) { + const contextualType = element && typeChecker.getContextualType(element.parent); + if (contextualType) { + return flatMap(getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false), propertySymbol => + getDefinitionFromSymbol(typeChecker, propertySymbol, node)); + } } return getDefinitionFromSymbol(typeChecker, symbol, node); } diff --git a/src/services/services.ts b/src/services/services.ts index ad39eb84a3445..d02804ca1c73c 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1489,19 +1489,6 @@ namespace ts { } } - function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined { - if ((isIdentifier(node) || isStringLiteral(node)) - && isPropertyAssignment(node.parent) - && node.parent.name === node) { - const type = checker.getContextualType(node.parent.parent); - const property = type && checker.getPropertyOfType(type, getTextOfIdentifierOrLiteral(node)); - if (property) { - return property; - } - } - return checker.getSymbolAtLocation(node); - } - /// Goto definition function getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] | undefined { synchronizeHostData(); @@ -2187,28 +2174,60 @@ namespace ts { */ /* @internal */ export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElementWithName | undefined { + const element = getContainingObjectLiteralElementWorker(node); + return element && (isObjectLiteralExpression(element.parent) || isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined; + } + function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined { switch (node.kind) { case SyntaxKind.StringLiteral: case SyntaxKind.NumericLiteral: if (node.parent.kind === SyntaxKind.ComputedPropertyName) { - return isObjectLiteralElement(node.parent.parent) ? node.parent.parent as ObjectLiteralElementWithName : undefined; + return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; } // falls through case SyntaxKind.Identifier: return isObjectLiteralElement(node.parent) && (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) && - node.parent.name === node ? node.parent as ObjectLiteralElementWithName : undefined; + node.parent.name === node ? node.parent : undefined; } return undefined; } + /* @internal */ - export type ObjectLiteralElementWithName = ObjectLiteralElement & { name: PropertyName }; + export type ObjectLiteralElementWithName = ObjectLiteralElement & { name: PropertyName; parent: ObjectLiteralExpression | JsxAttributes }; + + function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined { + const object = getContainingObjectLiteralElement(node); + if (object) { + const contextualType = checker.getContextualType(object.parent); + const properties = contextualType && getPropertySymbolsFromContextualType(object, checker, contextualType, /*unionSymbolOk*/ false); + if (properties && properties.length === 1) { + return first(properties); + } + } + return checker.getSymbolAtLocation(node); + } + /** Gets all symbols for one property. Does not get symbols for every property. */ /* @internal */ - export function getPropertySymbolsFromContextualType(typeChecker: TypeChecker, node: ObjectLiteralElement): Symbol[] { - const objectLiteral = node.parent; - const contextualType = typeChecker.getContextualType(objectLiteral)!; // TODO: GH#18217 - return getPropertySymbolsFromType(contextualType, node.name!)!; // TODO: GH#18217 + export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker, contextualType: Type, unionSymbolOk: boolean): ReadonlyArray { + const name = getNameFromPropertyName(node.name); + if (!name) return emptyArray; + if (!contextualType.isUnion()) { + const symbol = contextualType.getProperty(name); + return symbol ? [symbol] : emptyArray; + } + + const discriminatedPropertySymbols = mapDefined(contextualType.types, t => isObjectLiteralExpression(node.parent) && checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent) ? undefined : t.getProperty(name)); + if (unionSymbolOk && (discriminatedPropertySymbols.length === 0 || discriminatedPropertySymbols.length === contextualType.types.length)) { + const symbol = contextualType.getProperty(name); + if (symbol) return [symbol]; + } + if (discriminatedPropertySymbols.length === 0) { + // Bad discriminant -- do again without discriminating + return mapDefined(contextualType.types, t => t.getProperty(name)); + } + return discriminatedPropertySymbols; } /* @internal */ diff --git a/tests/cases/fourslash/findAllRefsUnionProperty.ts b/tests/cases/fourslash/findAllRefsUnionProperty.ts index 0be705bf6b52c..97b8972c457fb 100644 --- a/tests/cases/fourslash/findAllRefsUnionProperty.ts +++ b/tests/cases/fourslash/findAllRefsUnionProperty.ts @@ -1,8 +1,12 @@ /// ////type T = -//// | { [|{| "isWriteAccess": true, "isDefinition": true |}type|]: "a" } -//// | { [|{| "isWriteAccess": true, "isDefinition": true |}type|]: "b" }; +//// | { [|{| "isWriteAccess": true, "isDefinition": true |}type|]: "a", [|{| "isWriteAccess": true, "isDefinition": true |}prop|]: number } +//// | { [|{| "isWriteAccess": true, "isDefinition": true |}type|]: "b", [|{| "isWriteAccess": true, "isDefinition": true |}prop|]: string }; +////const tt: T = { +//// [|{| "isWriteAccess": true, "isDefinition": true |}type|]: "a", +//// [|{| "isWriteAccess": true, "isDefinition": true |}prop|]: 0, +////}; ////declare const t: T; ////if (t.[|type|] === "a") { //// t.[|type|]; @@ -10,10 +14,14 @@ //// t.[|type|]; ////} -const ranges = test.ranges(); -const [r0, r1, r2, r3, r4] = ranges; -verify.referenceGroups(ranges, [ - { definition: { text: '(property) type: "a"', range: r0 }, ranges: [r0, r3] }, - { definition: { text: '(property) type: "b"', range: r1 }, ranges: [r1, r4] }, - { definition: { text: '(property) type: "a" | "b"', range: r0 }, ranges: [r2] }, -]); +const [t0, p0, t1, p1, t2, p2, t3, t4, t5] = test.ranges(); + +const a = { definition: { text: '(property) type: "a"', range: t0 }, ranges: [t0, t2, t4] }; +const b = { definition: { text: '(property) type: "b"', range: t1 }, ranges: [t1, t5] }; +const ab = { definition: { text: '(property) type: "a" | "b"', range: t0 }, ranges: [t3] }; +verify.referenceGroups([t0, t1, t3, t4, t5], [a, b, ab]); +verify.referenceGroups(t2, [a, ab]); + +const p = { definition: "(property) prop: number", ranges: [p0, p2] }; +verify.referenceGroups([p0, p1], [p, { definition: "(property) prop: string", ranges: [p1] }]); +verify.referenceGroups(p2, [p]); diff --git a/tests/cases/fourslash/goToDefinitionUnionTypeProperty_discriminated.ts b/tests/cases/fourslash/goToDefinitionUnionTypeProperty_discriminated.ts new file mode 100644 index 0000000000000..535c22c89307b --- /dev/null +++ b/tests/cases/fourslash/goToDefinitionUnionTypeProperty_discriminated.ts @@ -0,0 +1,29 @@ +/// + +////type U = A | B; +//// +////interface A { +//// /*aKind*/kind: "a"; +//// /*aProp*/prop: number; +////}; +//// +////interface B { +//// /*bKind*/kind: "b"; +//// /*bProp*/prop: string; +////} +//// +////const u: U = { +//// [|/*kind*/kind|]: "a", +//// [|/*prop*/prop|]: 0, +////}; +////const u2: U = { +//// [|/*kindBogus*/kind|]: "bogus", +//// [|/*propBogus*/prop|]: 0, +////}; + +verify.goToDefinition({ + kind: "aKind", + prop: "aProp", + kindBogus: ["aKind", "bKind"], + propBogus: ["aProp", "bProp"], +}); diff --git a/tests/cases/fourslash/jsxGenericQuickInfo.tsx b/tests/cases/fourslash/jsxGenericQuickInfo.tsx index 898c7c17790a0..359701d5c1757 100644 --- a/tests/cases/fourslash/jsxGenericQuickInfo.tsx +++ b/tests/cases/fourslash/jsxGenericQuickInfo.tsx @@ -18,5 +18,5 @@ //// var c = item.toFixed()} verify.quickInfoAt("0", "(property) Props.renderItem: (item: number) => string"); verify.quickInfoAt("1", "(parameter) item: number"); -verify.quickInfoAt("2", "(JSX attribute) renderItem: (item: number) => string"); +verify.quickInfoAt("2", "(JSX attribute) Props.renderItem: (item: number) => string"); verify.quickInfoAt("3", "(parameter) item: number"); diff --git a/tests/cases/fourslash/quickInfoUnion_discriminated.ts b/tests/cases/fourslash/quickInfoUnion_discriminated.ts new file mode 100644 index 0000000000000..65b7cb53a0d83 --- /dev/null +++ b/tests/cases/fourslash/quickInfoUnion_discriminated.ts @@ -0,0 +1,34 @@ +/// + +// @Filename: quickInfoJsDocTags.ts +////type U = A | B; +//// +////interface A { +//// /** Kind A */ +//// kind: "a"; +//// /** Prop A */ +//// prop: number; +////} +//// +////interface B { +//// /** Kind B */ +//// kind: "b"; +//// /** Prop B */ +//// prop: string; +////} +//// +////const u: U = { +//// /*uKind*/kind: "a", +//// /*uProp*/prop: 0, +////} +////const u2: U = { +//// /*u2Kind*/kind: "bogus", +//// /*u2Prop*/prop: 1, +////}; + +verify.quickInfos({ + uKind: ['(property) A.kind: "a"', "Kind A "], + uProp: ["(property) A.prop: number", "Prop A "], + u2Kind: '(property) kind: "bogus"', + u2Prop: "(property) prop: number", +}); diff --git a/tests/cases/fourslash/referencesForContextuallyTypedUnionProperties.ts b/tests/cases/fourslash/referencesForContextuallyTypedUnionProperties.ts index d955ac0b57266..9ddd31f2fbb95 100644 --- a/tests/cases/fourslash/referencesForContextuallyTypedUnionProperties.ts +++ b/tests/cases/fourslash/referencesForContextuallyTypedUnionProperties.ts @@ -34,8 +34,7 @@ ////var u1 = { a: 0, b: 0, common: "" }; ////var u2 = { b: 0, common: 0 }; -const all = test.ranges(); -const [aCommon, bCommon, ...unionRefs] = all; +const [aCommon, bCommon, ...unionRefs] = test.ranges(); verify.referenceGroups(aCommon, [ { definition: "(property) A.common: string", ranges: [aCommon] }, { definition: "(property) common: string | number", ranges: unionRefs }, diff --git a/tests/cases/fourslash/tsxQuickInfo4.ts b/tests/cases/fourslash/tsxQuickInfo4.ts index d9f566571aafd..a72e610449603 100644 --- a/tests/cases/fourslash/tsxQuickInfo4.ts +++ b/tests/cases/fourslash/tsxQuickInfo4.ts @@ -28,7 +28,7 @@ //// } //// function _buildMainButton({ onClick, children, className }: ButtonProps): JSX.Element { -//// return(); +//// return(); //// } //// declare function buildMainLink({ to, children, className }: LinkProps): JSX.Element; @@ -39,17 +39,17 @@ //// ); //// } -//// function buildSomeElement2(): JSX.Element { +//// function buildSomeElement2(): JSX.Element { //// return ( //// {}}>GO; //// ); //// } -//// let componenet = {}} ext/*5*/ra-prop>GO; +//// let componenet = {}} ext/*5*/ra-prop>GO; verify.quickInfos({ 1: "function MainButton(linkProps: LinkProps): any (+1 overload)", - 2: "(JSX attribute) to: string", + 2: "(JSX attribute) LinkProps.to: string", 3: "function MainButton(buttonProps: ButtonProps): any (+1 overload)", - 4: "(JSX attribute) onClick: () => void", + 4: "(method) ButtonProps.onClick(event?: any): void", 5: "(JSX attribute) extra-prop: true" });