From 0ca4cb25d68f370ac6bc5013f71f43b96b6f1e7a Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 23 Jan 2017 13:23:26 -0800 Subject: [PATCH] Support find-all-references for default exports --- src/compiler/utilities.ts | 8 +- src/services/findAllReferences.ts | 114 ++++++++++-------- .../fourslash/findAllRefsForDefaultExport.ts | 11 ++ 3 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 tests/cases/fourslash/findAllRefsForDefaultExport.ts diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b7cc0f0de6d..5c63d8bb60c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -395,7 +395,7 @@ namespace ts { function isShorthandAmbientModule(node: Node): boolean { // The only kind of module that can be missing a body is a shorthand ambient module. - return node.kind === SyntaxKind.ModuleDeclaration && (!(node).body); + return node && node.kind === SyntaxKind.ModuleDeclaration && (!(node).body); } export function isBlockScopedContainerTopLevel(node: Node): boolean { @@ -3104,7 +3104,11 @@ namespace ts { } export function getLocalSymbolForExportDefault(symbol: Symbol) { - return symbol && symbol.valueDeclaration && hasModifier(symbol.valueDeclaration, ModifierFlags.Default) ? symbol.valueDeclaration.localSymbol : undefined; + return isExportDefaultSymbol(symbol) ? symbol.valueDeclaration.localSymbol : undefined; + } + + export function isExportDefaultSymbol(symbol: Symbol): boolean { + return symbol && symbol.valueDeclaration && hasModifier(symbol.valueDeclaration, ModifierFlags.Default); } /** Return ".ts", ".d.ts", or ".tsx", if that is the extension. */ diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 3c38c4e7536..83006c66d77 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -15,14 +15,13 @@ namespace ts.FindAllReferences { // `getSymbolAtLocation` normally returns the symbol of the class when given the constructor keyword, // so we have to specify that we want the constructor symbol. - const symbol = typeChecker.getSymbolAtLocation(node); - - if (!implementations && !symbol && node.kind === SyntaxKind.StringLiteral) { - return getReferencesForStringLiteral(node, sourceFiles, typeChecker, cancellationToken); - } + let symbol = typeChecker.getSymbolAtLocation(node); // Could not find a symbol e.g. unknown identifier if (!symbol) { + if (!implementations && node.kind === SyntaxKind.StringLiteral) { + return getReferencesForStringLiteral(node, sourceFiles, typeChecker, cancellationToken); + } // Can't have references to something that we have no symbol for. return undefined; } @@ -34,9 +33,27 @@ namespace ts.FindAllReferences { return undefined; } + const aliasedSymbol = followAliasIfNecessary(symbol, node, typeChecker); + const isShorthandModule = ts.isShorthandAmbientModuleSymbol(aliasedSymbol); + // Don't follow alias for shorthand modules because we lose information that way. + if (!isShorthandModule) { + symbol = aliasedSymbol; + } + // Compute the meaning from the location and the symbol it references const searchMeaning = getIntersectingMeaningFromDeclarations(getMeaningFromLocation(node), declarations); + const result: ReferencedSymbol[] = []; + // Maps from a symbol ID to the ReferencedSymbol entry in 'result'. + const symbolToIndex: number[] = []; + const inheritsFromCache: Map = createMap(); + + // Build the set of symbols to search for, initially it has only the current symbol + const searchSymbols = populateSearchSymbolSet(symbol, node, typeChecker, implementations, isShorthandModule ? aliasedSymbol : undefined); + function isSearchedFor(symbol: Symbol): boolean { + return contains(searchSymbols, symbol); + } + // Get the text to search for. // Note: if this is an external module symbol, the name doesn't include quotes. const declaredName = stripQuotes(getDeclaredName(typeChecker, symbol, node)); @@ -44,25 +61,28 @@ namespace ts.FindAllReferences { // Try to get the smallest valid scope that we can limit our search to; // otherwise we'll need to search globally (i.e. include each file). const scope = getSymbolScope(symbol); - - // Maps from a symbol ID to the ReferencedSymbol entry in 'result'. - const symbolToIndex: number[] = []; - - const result: ReferencedSymbol[] = []; if (scope) { - getReferencesInNode(scope, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex, implementations, typeChecker, cancellationToken); + getRefs(scope, declaredName); } else { - const internedName = getInternedName(symbol, node); + const isDefault = isExportDefaultSymbol(symbol); + const internedName = isDefault ? symbol.valueDeclaration.localSymbol.name : getInternedName(symbol, node); for (const sourceFile of sourceFiles) { cancellationToken.throwIfCancellationRequested(); - if (sourceFileHasName(sourceFile, internedName)) { - getReferencesInNode(sourceFile, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex, implementations, typeChecker, cancellationToken); + const searchName = (isDefault ? getDefaultImportName(symbol, sourceFile, typeChecker) : undefined) || + (sourceFileHasName(sourceFile, internedName) ? declaredName : undefined); + if (searchName !== undefined) { + getRefs(sourceFile, searchName); } } } return result; + + function getRefs(scope: ts.Node, searchName: string): void { + getReferencesInNode(scope, symbol, searchName, node, searchMeaning, findInStrings, findInComments, result, + symbolToIndex, implementations, typeChecker, cancellationToken, isSearchedFor, inheritsFromCache); + } } /** getReferencedSymbols for special node kinds. */ @@ -100,6 +120,23 @@ namespace ts.FindAllReferences { return getNameTable(sourceFile).get(name) !== undefined; } + /** + * Given a symbol, see if any of the imports in a source file reference it. + * Only call this if `symbol` is a default export. + */ + function getDefaultImportName(symbol: Symbol, sourceFile: SourceFile, checker: ts.TypeChecker): string | undefined { + for (const importSpecifier of sourceFile.imports) { + const importDecl = importSpecifier.parent as ts.ImportDeclaration; + Debug.assert(importDecl.moduleSpecifier === importSpecifier); + const defaultName = importDecl.importClause.name; + const defaultReferencedSymbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(defaultName)); + if (symbol === defaultReferencedSymbol) { + return defaultName.text; + } + } + return undefined; + } + function getDefinition(symbol: Symbol, node: Node, typeChecker: TypeChecker): ReferencedSymbolDefinitionInfo { const { displayParts, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, node.getSourceFile(), getContainerNode(node), node); const name = displayParts.map(p => p.text).join(""); @@ -177,11 +214,6 @@ namespace ts.FindAllReferences { return location.text; } - // Try to get the local symbol if we're dealing with an 'export default' - // since that symbol has the "true" name. - const localExportDefaultSymbol = getLocalSymbolForExportDefault(symbol); - symbol = localExportDefaultSymbol || symbol; - return stripQuotes(symbol.name); } @@ -398,7 +430,9 @@ namespace ts.FindAllReferences { symbolToIndex: number[], implementations: boolean, typeChecker: TypeChecker, - cancellationToken: CancellationToken): void { + cancellationToken: CancellationToken, + isSearchedFor: (symbol: Symbol) => boolean, + inheritsFromCache: Map): void { const sourceFile = container.getSourceFile(); @@ -406,12 +440,6 @@ namespace ts.FindAllReferences { const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, searchText, start, container.getEnd(), cancellationToken); const parents = getParentSymbolsOfPropertyAccess(); - const inheritsFromCache: Map = createMap(); - // Build the set of symbols to search for, initially it has only the current symbol - const searchSymbols = populateSearchSymbolSet(searchSymbol, searchLocation, typeChecker, implementations); - function isSearchedFor(symbol: Symbol): boolean { - return contains(searchSymbols, symbol); - } for (const position of possiblePositions) { cancellationToken.throwIfCancellationRequested(); @@ -456,11 +484,11 @@ namespace ts.FindAllReferences { addReferenceToRelatedSymbol(referenceLocation, relatedSymbol); } /* Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment - * has two meaning : property name and property value. Therefore when we do findAllReference at the position where - * an identifier is declared, the language service should return the position of the variable declaration as well as - * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the - * position of property accessing, the referenceEntry of such position will be handled in the first case. - */ + * has two meanings: property name and property value. Therefore when we do findAllReference at the position where + * an identifier is declared, the language service should return the position of the variable declaration as well as + * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the + * position of property accessing, the referenceEntry of such position will be handled in the first case. + */ else if (!(referenceSymbol.flags & SymbolFlags.Transient) && isSearchedFor(shorthandValueSymbol)) { addReferenceToRelatedSymbol(referenceSymbolDeclaration.name, shorthandValueSymbol); } @@ -472,10 +500,10 @@ namespace ts.FindAllReferences { return; /* If we are just looking for implementations and this is a property access expression, we need to get the - * symbol of the local type of the symbol the property is being accessed on. This is because our search - * symbol may have a different parent symbol if the local type's symbol does not declare the property - * being accessed (i.e. it is declared in some parent class or interface) - */ + * symbol of the local type of the symbol the property is being accessed on. This is because our search + * symbol may have a different parent symbol if the local type's symbol does not declare the property + * being accessed (i.e. it is declared in some parent class or interface) + */ function getParentSymbolsOfPropertyAccess(): Symbol[] | undefined { if (implementations) { const propertyAccessExpression = getPropertyAccessExpressionFromRightHandSide(searchLocation); @@ -994,7 +1022,7 @@ namespace ts.FindAllReferences { } } - function populateSearchSymbolSet(symbol: Symbol, location: Node, typeChecker: TypeChecker, implementations: boolean): Symbol[] { + function populateSearchSymbolSet(symbol: Symbol, location: Node, typeChecker: TypeChecker, implementations: boolean, aliasSymbol?: Symbol): Symbol[] { // The search set contains at least the current symbol let result = [symbol]; @@ -1009,18 +1037,6 @@ namespace ts.FindAllReferences { } } - // If the symbol is an alias, add what it aliases to the list - // import {a} from "mod"; - // export {a} - // If the symbol is an alias to default declaration, add what it aliases to the list - // declare "mod" { export default class B { } } - // import B from "mod"; - //// For export specifiers, the exported name can be referring to a local symbol, e.g.: - //// import {a} from "mod"; - //// export {a as somethingElse} - //// We want the *local* declaration of 'a' as declared in the import, - //// *not* as declared within "mod" (or farther) - const aliasSymbol = getAliasSymbolForPropertyNameSymbol(symbol, location, typeChecker); if (aliasSymbol) { result = result.concat(populateSearchSymbolSet(aliasSymbol, location, typeChecker, implementations)); } @@ -1225,7 +1241,7 @@ namespace ts.FindAllReferences { } /** Gets all symbols for one property. Does not get symbols for every property. */ - function getPropertySymbolsFromContextualType(node: ObjectLiteralElement, typeChecker: TypeChecker): Symbol[] { + function getPropertySymbolsFromContextualType(node: ObjectLiteralElement, typeChecker: TypeChecker): Symbol[] | undefined { const objectLiteral = node.parent; const contextualType = typeChecker.getContextualType(objectLiteral); const name = getNameFromObjectLiteralElement(node); diff --git a/tests/cases/fourslash/findAllRefsForDefaultExport.ts b/tests/cases/fourslash/findAllRefsForDefaultExport.ts new file mode 100644 index 00000000000..a27227b907a --- /dev/null +++ b/tests/cases/fourslash/findAllRefsForDefaultExport.ts @@ -0,0 +1,11 @@ +/// + +// @Filename: a.ts +////export default function /*def*/[|f|]() {} + +// @Filename: b.ts +////import [|g|] from "./a"; +/////*ref*/[|g|](); + +verify.rangesReferenceEachOther(); +verify.goToDefinition("ref", "def");