From 02a8ec608ce4ebaf429f5855cc09dd8a72bac00b Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 14 Nov 2017 07:20:25 -0800 Subject: [PATCH] Support semantic classification of alias --- src/compiler/binder.ts | 97 +++++++------ src/compiler/checker.ts | 6 +- src/compiler/types.ts | 2 +- src/services/breakpoints.ts | 6 +- src/services/classifier.ts | 128 ++++++++---------- src/services/utilities.ts | 2 +- .../fourslash/semanticClassificationAlias.ts | 20 +++ 7 files changed, 137 insertions(+), 124 deletions(-) create mode 100644 tests/cases/fourslash/semanticClassificationAlias.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index a0146740678df..cd99f75155147 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -16,52 +16,63 @@ namespace ts { referenced: boolean; } - export function getModuleInstanceState(node: Node): ModuleInstanceState { + export function getModuleInstanceState(node: ModuleDeclaration): ModuleInstanceState { + return node.body ? getModuleInstanceStateWorker(node.body) : ModuleInstanceState.Instantiated; + } + + function getModuleInstanceStateWorker(node: Node): ModuleInstanceState { // A module is uninstantiated if it contains only - // 1. interface declarations, type alias declarations - if (node.kind === SyntaxKind.InterfaceDeclaration || node.kind === SyntaxKind.TypeAliasDeclaration) { - return ModuleInstanceState.NonInstantiated; - } - // 2. const enum declarations - else if (isConstEnumDeclaration(node)) { - return ModuleInstanceState.ConstEnumOnly; - } - // 3. non-exported import declarations - else if ((node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration) && !(hasModifier(node, ModifierFlags.Export))) { - return ModuleInstanceState.NonInstantiated; - } - // 4. other uninstantiated module declarations. - else if (node.kind === SyntaxKind.ModuleBlock) { - let state = ModuleInstanceState.NonInstantiated; - forEachChild(node, n => { - switch (getModuleInstanceState(n)) { - case ModuleInstanceState.NonInstantiated: - // child is non-instantiated - continue searching - return false; - case ModuleInstanceState.ConstEnumOnly: - // child is const enum only - record state and continue searching - state = ModuleInstanceState.ConstEnumOnly; - return false; - case ModuleInstanceState.Instantiated: - // child is instantiated - record state and stop - state = ModuleInstanceState.Instantiated; - return true; + switch (node.kind) { + // 1. interface declarations, type alias declarations + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + return ModuleInstanceState.NonInstantiated; + // 2. const enum declarations + case SyntaxKind.EnumDeclaration: + if (isConst(node)) { + return ModuleInstanceState.ConstEnumOnly; + } + break; + // 3. non-exported import declarations + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (!(hasModifier(node, ModifierFlags.Export))) { + return ModuleInstanceState.NonInstantiated; + } + break; + // 4. other uninstantiated module declarations. + case SyntaxKind.ModuleBlock: { + let state = ModuleInstanceState.NonInstantiated; + forEachChild(node, n => { + const childState = getModuleInstanceStateWorker(n); + switch (childState) { + case ModuleInstanceState.NonInstantiated: + // child is non-instantiated - continue searching + return; + case ModuleInstanceState.ConstEnumOnly: + // child is const enum only - record state and continue searching + state = ModuleInstanceState.ConstEnumOnly; + return; + case ModuleInstanceState.Instantiated: + // child is instantiated - record state and stop + state = ModuleInstanceState.Instantiated; + return true; + default: + Debug.assertNever(childState); + } + }); + return state; + } + case SyntaxKind.ModuleDeclaration: + return getModuleInstanceState(node as ModuleDeclaration); + case SyntaxKind.Identifier: + // Only jsdoc typedef definition can exist in jsdoc namespace, and it should + // be considered the same as type alias + if ((node).isInJSDocNamespace) { + return ModuleInstanceState.NonInstantiated; } - }); - return state; - } - else if (node.kind === SyntaxKind.ModuleDeclaration) { - const body = (node).body; - return body ? getModuleInstanceState(body) : ModuleInstanceState.Instantiated; - } - // Only jsdoc typedef definition can exist in jsdoc namespace, and it should - // be considered the same as type alias - else if (node.kind === SyntaxKind.Identifier && (node).isInJSDocNamespace) { - return ModuleInstanceState.NonInstantiated; - } - else { - return ModuleInstanceState.Instantiated; } + return ModuleInstanceState.Instantiated; } const enum ContainerFlags { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0ae9486aa4e19..db67b81cb2a5c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19789,7 +19789,7 @@ namespace ts { case SyntaxKind.JSDocTypedefTag: return DeclarationSpaces.ExportType; case SyntaxKind.ModuleDeclaration: - return isAmbientModule(d) || getModuleInstanceState(d) !== ModuleInstanceState.NonInstantiated + return isAmbientModule(d as ModuleDeclaration) || getModuleInstanceState(d as ModuleDeclaration) !== ModuleInstanceState.NonInstantiated ? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue : DeclarationSpaces.ExportNamespace; case SyntaxKind.ClassDeclaration: @@ -20728,7 +20728,7 @@ namespace ts { } // Uninstantiated modules shouldnt do this check - if (node.kind === SyntaxKind.ModuleDeclaration && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { + if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { return; } @@ -20747,7 +20747,7 @@ namespace ts { } // Uninstantiated modules shouldnt do this check - if (node.kind === SyntaxKind.ModuleDeclaration && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { + if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { return; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3141e3f22d924..50b164721e343 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3076,7 +3076,7 @@ namespace ts { /* @internal */ // The set of things we consider semantically classifiable. Used to speed up the LS during // classification. - Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module, + Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module | Alias, } export interface Symbol { diff --git a/src/services/breakpoints.ts b/src/services/breakpoints.ts index 73732ce87e82d..57dbc5711e7d8 100644 --- a/src/services/breakpoints.ts +++ b/src/services/breakpoints.ts @@ -183,7 +183,7 @@ namespace ts.BreakpointResolver { case SyntaxKind.ModuleDeclaration: // span on complete module if it is instantiated - if (getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { + if (getModuleInstanceState(node as ModuleDeclaration) !== ModuleInstanceState.Instantiated) { return undefined; } // falls through @@ -471,7 +471,7 @@ namespace ts.BreakpointResolver { function spanInBlock(block: Block): TextSpan { switch (block.parent.kind) { case SyntaxKind.ModuleDeclaration: - if (getModuleInstanceState(block.parent) !== ModuleInstanceState.Instantiated) { + if (getModuleInstanceState(block.parent as ModuleDeclaration) !== ModuleInstanceState.Instantiated) { return undefined; } // falls through @@ -581,7 +581,7 @@ namespace ts.BreakpointResolver { switch (node.parent.kind) { case SyntaxKind.ModuleBlock: // If this is not an instantiated module block, no bp span - if (getModuleInstanceState(node.parent.parent) !== ModuleInstanceState.Instantiated) { + if (getModuleInstanceState(node.parent.parent as ModuleDeclaration) !== ModuleInstanceState.Instantiated) { return undefined; } // falls through diff --git a/src/services/classifier.ts b/src/services/classifier.ts index 18eec066ea215..af7d84300e029 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -488,89 +488,71 @@ namespace ts { /* @internal */ export function getEncodedSemanticClassifications(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFile: SourceFile, classifiableNames: UnderscoreEscapedMap, span: TextSpan): Classifications { - const result: number[] = []; - processNode(sourceFile); - - return { spans: result, endOfLineState: EndOfLineState.None }; - - function pushClassification(start: number, length: number, type: ClassificationType) { - result.push(start); - result.push(length); - result.push(type); - } - - function classifySymbol(symbol: Symbol, meaningAtPosition: SemanticMeaning): ClassificationType { - const flags = symbol.getFlags(); - if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) { + const spans: number[] = []; + sourceFile.forEachChild(function cb(node: Node): void { + // Only walk into nodes that intersect the requested span. + if (!node || !textSpanIntersectsWith(span, node.pos, node.getFullWidth())) { return; } - if (flags & SymbolFlags.Class) { - return ClassificationType.className; - } - else if (flags & SymbolFlags.Enum) { - return ClassificationType.enumName; - } - else if (flags & SymbolFlags.TypeAlias) { - return ClassificationType.typeAliasName; - } - else if (meaningAtPosition & SemanticMeaning.Type) { - if (flags & SymbolFlags.Interface) { - return ClassificationType.interfaceName; - } - else if (flags & SymbolFlags.TypeParameter) { - return ClassificationType.typeParameterName; - } - } - else if (flags & SymbolFlags.Module) { - // Only classify a module as such if - // - It appears in a namespace context. - // - There exists a module declaration which actually impacts the value side. - if (meaningAtPosition & SemanticMeaning.Namespace || - (meaningAtPosition & SemanticMeaning.Value && hasValueSideModule(symbol))) { - return ClassificationType.moduleName; + checkForClassificationCancellation(cancellationToken, node.kind); + // Only bother calling into the typechecker if this is an identifier that + // could possibly resolve to a type name. This makes classification run + // in a third of the time it would normally take. + if (isIdentifier(node) && !nodeIsMissing(node) && classifiableNames.has(node.escapedText)) { + const symbol = typeChecker.getSymbolAtLocation(node); + const type = symbol && classifySymbol(symbol, getMeaningFromLocation(node), typeChecker); + if (type) { + pushClassification(node.getStart(sourceFile), node.getEnd(), type); } } - return undefined; + node.forEachChild(cb); + }); + return { spans, endOfLineState: EndOfLineState.None }; - /** - * Returns true if there exists a module that introduces entities on the value side. - */ - function hasValueSideModule(symbol: Symbol): boolean { - return forEach(symbol.declarations, declaration => { - return declaration.kind === SyntaxKind.ModuleDeclaration && - getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated; - }); - } + function pushClassification(start: number, end: number, type: ClassificationType): void { + spans.push(start); + spans.push(end - start); + spans.push(type); } + } - function processNode(node: Node) { - // Only walk into nodes that intersect the requested span. - if (node && textSpanIntersectsWith(span, node.getFullStart(), node.getFullWidth())) { - const kind = node.kind; - checkForClassificationCancellation(cancellationToken, kind); - - if (kind === SyntaxKind.Identifier && !nodeIsMissing(node)) { - const identifier = node; - - // Only bother calling into the typechecker if this is an identifier that - // could possibly resolve to a type name. This makes classification run - // in a third of the time it would normally take. - if (classifiableNames.has(identifier.escapedText)) { - const symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - const type = classifySymbol(symbol, getMeaningFromLocation(node)); - if (type) { - pushClassification(node.getStart(), node.getWidth(), type); - } - } - } - } - - forEachChild(node, processNode); - } + function classifySymbol(symbol: Symbol, meaningAtPosition: SemanticMeaning, checker: TypeChecker): ClassificationType | undefined { + const flags = symbol.getFlags(); + if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) { + return undefined; + } + else if (flags & SymbolFlags.Class) { + return ClassificationType.className; } + else if (flags & SymbolFlags.Enum) { + return ClassificationType.enumName; + } + else if (flags & SymbolFlags.TypeAlias) { + return ClassificationType.typeAliasName; + } + else if (flags & SymbolFlags.Module) { + // Only classify a module as such if + // - It appears in a namespace context. + // - There exists a module declaration which actually impacts the value side. + return meaningAtPosition & SemanticMeaning.Namespace || meaningAtPosition & SemanticMeaning.Value && hasValueSideModule(symbol) ? ClassificationType.moduleName : undefined; + } + else if (flags & SymbolFlags.Alias) { + return classifySymbol(checker.getAliasedSymbol(symbol), meaningAtPosition, checker); + } + else if (meaningAtPosition & SemanticMeaning.Type) { + return flags & SymbolFlags.Interface ? ClassificationType.interfaceName : flags & SymbolFlags.TypeParameter ? ClassificationType.typeParameterName : undefined; + } + else { + return undefined; + } + } + + /** Returns true if there exists a module that introduces entities on the value side. */ + function hasValueSideModule(symbol: Symbol): boolean { + return some(symbol.declarations, declaration => + isModuleDeclaration(declaration) && getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated); } function getClassificationTypeName(type: ClassificationType): ClassificationTypeNames { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index cf71118292a90..fb49b8228a5b0 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -60,7 +60,7 @@ namespace ts { if (isAmbientModule(node)) { return SemanticMeaning.Namespace | SemanticMeaning.Value; } - else if (getModuleInstanceState(node) === ModuleInstanceState.Instantiated) { + else if (getModuleInstanceState(node as ModuleDeclaration) === ModuleInstanceState.Instantiated) { return SemanticMeaning.Namespace | SemanticMeaning.Value; } else { diff --git a/tests/cases/fourslash/semanticClassificationAlias.ts b/tests/cases/fourslash/semanticClassificationAlias.ts new file mode 100644 index 0000000000000..799674a211dae --- /dev/null +++ b/tests/cases/fourslash/semanticClassificationAlias.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////export type x = number; +////export class y {}; + +// @Filename: /b.ts +////import { /*0*/x, /*1*/y } from "./a"; +////const v: /*2*/x = /*3*/y; + +goTo.file("/b.ts"); + +const [m0, m1, m2, m3] = test.markers(); +const c = classification; +verify.semanticClassificationsAre( + c.typeAliasName("x", m0.position), + c.className("y", m1.position), + c.typeAliasName("x", m2.position), + c.className("y", m3.position), +);