Skip to content

In getPropertySymbolsFromContextualType, use union discriminant to filter types #25914

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

Merged
1 commit merged into from
Jul 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Symbol> {
const contextualType = checker.getContextualType(<ObjectLiteralExpression>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
Expand Down
17 changes: 9 additions & 8 deletions src/services/goToDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand All @@ -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);
}
Expand Down
59 changes: 39 additions & 20 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 = <ObjectLiteralExpression | JsxAttributes>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<Symbol> {
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 */
Expand Down
26 changes: 17 additions & 9 deletions tests/cases/fourslash/findAllRefsUnionProperty.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
/// <reference path='fourslash.ts'/>

////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|];
////} else {
//// 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]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// <reference path='fourslash.ts'/>

////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"],
});
2 changes: 1 addition & 1 deletion tests/cases/fourslash/jsxGenericQuickInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
//// var c = <Component items={[0, 1, 2]} render/*2*/Item={it/*3*/em => item.toFixed()}
verify.quickInfoAt("0", "(property) Props<number>.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<number>.renderItem: (item: number) => string");
verify.quickInfoAt("3", "(parameter) item: number");
34 changes: 34 additions & 0 deletions tests/cases/fourslash/quickInfoUnion_discriminated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// <reference path='fourslash.ts'/>

// @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",
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
10 changes: 5 additions & 5 deletions tests/cases/fourslash/tsxQuickInfo4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
//// }

//// function _buildMainButton({ onClick, children, className }: ButtonProps): JSX.Element {
//// return(<button className={className} onClick={onClick}>{ children || 'MAIN BUTTON'}</button>);
//// return(<button className={className} onClick={onClick}>{ children || 'MAIN BUTTON'}</button>);
//// }

//// declare function buildMainLink({ to, children, className }: LinkProps): JSX.Element;
Expand All @@ -39,17 +39,17 @@
//// );
//// }

//// function buildSomeElement2(): JSX.Element {
//// function buildSomeElement2(): JSX.Element {
//// return (
//// <MainB/*3*/utton onC/*4*/lick={()=>{}}>GO</MainButton>;
//// );
//// }
//// let componenet = <MainButton onClick={()=>{}} ext/*5*/ra-prop>GO</MainButton>;
//// let componenet = <MainButton onClick={()=>{}} ext/*5*/ra-prop>GO</MainButton>;

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"
});