Skip to content

Commit 74a44e0

Browse files
author
Andy
authored
Support semantic classification of alias (#20012)
1 parent 46c0edc commit 74a44e0

File tree

7 files changed

+137
-124
lines changed

7 files changed

+137
-124
lines changed

src/compiler/binder.ts

+54-43
Original file line numberDiff line numberDiff line change
@@ -16,52 +16,63 @@ namespace ts {
1616
referenced: boolean;
1717
}
1818

19-
export function getModuleInstanceState(node: Node): ModuleInstanceState {
19+
export function getModuleInstanceState(node: ModuleDeclaration): ModuleInstanceState {
20+
return node.body ? getModuleInstanceStateWorker(node.body) : ModuleInstanceState.Instantiated;
21+
}
22+
23+
function getModuleInstanceStateWorker(node: Node): ModuleInstanceState {
2024
// A module is uninstantiated if it contains only
21-
// 1. interface declarations, type alias declarations
22-
if (node.kind === SyntaxKind.InterfaceDeclaration || node.kind === SyntaxKind.TypeAliasDeclaration) {
23-
return ModuleInstanceState.NonInstantiated;
24-
}
25-
// 2. const enum declarations
26-
else if (isConstEnumDeclaration(node)) {
27-
return ModuleInstanceState.ConstEnumOnly;
28-
}
29-
// 3. non-exported import declarations
30-
else if ((node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration) && !(hasModifier(node, ModifierFlags.Export))) {
31-
return ModuleInstanceState.NonInstantiated;
32-
}
33-
// 4. other uninstantiated module declarations.
34-
else if (node.kind === SyntaxKind.ModuleBlock) {
35-
let state = ModuleInstanceState.NonInstantiated;
36-
forEachChild(node, n => {
37-
switch (getModuleInstanceState(n)) {
38-
case ModuleInstanceState.NonInstantiated:
39-
// child is non-instantiated - continue searching
40-
return false;
41-
case ModuleInstanceState.ConstEnumOnly:
42-
// child is const enum only - record state and continue searching
43-
state = ModuleInstanceState.ConstEnumOnly;
44-
return false;
45-
case ModuleInstanceState.Instantiated:
46-
// child is instantiated - record state and stop
47-
state = ModuleInstanceState.Instantiated;
48-
return true;
25+
switch (node.kind) {
26+
// 1. interface declarations, type alias declarations
27+
case SyntaxKind.InterfaceDeclaration:
28+
case SyntaxKind.TypeAliasDeclaration:
29+
return ModuleInstanceState.NonInstantiated;
30+
// 2. const enum declarations
31+
case SyntaxKind.EnumDeclaration:
32+
if (isConst(node)) {
33+
return ModuleInstanceState.ConstEnumOnly;
34+
}
35+
break;
36+
// 3. non-exported import declarations
37+
case SyntaxKind.ImportDeclaration:
38+
case SyntaxKind.ImportEqualsDeclaration:
39+
if (!(hasModifier(node, ModifierFlags.Export))) {
40+
return ModuleInstanceState.NonInstantiated;
41+
}
42+
break;
43+
// 4. other uninstantiated module declarations.
44+
case SyntaxKind.ModuleBlock: {
45+
let state = ModuleInstanceState.NonInstantiated;
46+
forEachChild(node, n => {
47+
const childState = getModuleInstanceStateWorker(n);
48+
switch (childState) {
49+
case ModuleInstanceState.NonInstantiated:
50+
// child is non-instantiated - continue searching
51+
return;
52+
case ModuleInstanceState.ConstEnumOnly:
53+
// child is const enum only - record state and continue searching
54+
state = ModuleInstanceState.ConstEnumOnly;
55+
return;
56+
case ModuleInstanceState.Instantiated:
57+
// child is instantiated - record state and stop
58+
state = ModuleInstanceState.Instantiated;
59+
return true;
60+
default:
61+
Debug.assertNever(childState);
62+
}
63+
});
64+
return state;
65+
}
66+
case SyntaxKind.ModuleDeclaration:
67+
return getModuleInstanceState(node as ModuleDeclaration);
68+
case SyntaxKind.Identifier:
69+
// Only jsdoc typedef definition can exist in jsdoc namespace, and it should
70+
// be considered the same as type alias
71+
if ((<Identifier>node).isInJSDocNamespace) {
72+
return ModuleInstanceState.NonInstantiated;
4973
}
50-
});
51-
return state;
52-
}
53-
else if (node.kind === SyntaxKind.ModuleDeclaration) {
54-
const body = (<ModuleDeclaration>node).body;
55-
return body ? getModuleInstanceState(body) : ModuleInstanceState.Instantiated;
56-
}
57-
// Only jsdoc typedef definition can exist in jsdoc namespace, and it should
58-
// be considered the same as type alias
59-
else if (node.kind === SyntaxKind.Identifier && (<Identifier>node).isInJSDocNamespace) {
60-
return ModuleInstanceState.NonInstantiated;
61-
}
62-
else {
63-
return ModuleInstanceState.Instantiated;
6474
}
75+
return ModuleInstanceState.Instantiated;
6576
}
6677

6778
const enum ContainerFlags {

src/compiler/checker.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -20173,7 +20173,7 @@ namespace ts {
2017320173
case SyntaxKind.JSDocTypedefTag:
2017420174
return DeclarationSpaces.ExportType;
2017520175
case SyntaxKind.ModuleDeclaration:
20176-
return isAmbientModule(d) || getModuleInstanceState(d) !== ModuleInstanceState.NonInstantiated
20176+
return isAmbientModule(d as ModuleDeclaration) || getModuleInstanceState(d as ModuleDeclaration) !== ModuleInstanceState.NonInstantiated
2017720177
? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue
2017820178
: DeclarationSpaces.ExportNamespace;
2017920179
case SyntaxKind.ClassDeclaration:
@@ -21121,7 +21121,7 @@ namespace ts {
2112121121
}
2112221122

2112321123
// Uninstantiated modules shouldnt do this check
21124-
if (node.kind === SyntaxKind.ModuleDeclaration && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) {
21124+
if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) {
2112521125
return;
2112621126
}
2112721127

@@ -21140,7 +21140,7 @@ namespace ts {
2114021140
}
2114121141

2114221142
// Uninstantiated modules shouldnt do this check
21143-
if (node.kind === SyntaxKind.ModuleDeclaration && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) {
21143+
if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) {
2114421144
return;
2114521145
}
2114621146

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3107,7 +3107,7 @@ namespace ts {
31073107
/* @internal */
31083108
// The set of things we consider semantically classifiable. Used to speed up the LS during
31093109
// classification.
3110-
Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module,
3110+
Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module | Alias,
31113111

31123112
/* @internal */
31133113
LateBindingContainer = Class | Interface | TypeLiteral | ObjectLiteral,

src/services/breakpoints.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ namespace ts.BreakpointResolver {
183183

184184
case SyntaxKind.ModuleDeclaration:
185185
// span on complete module if it is instantiated
186-
if (getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) {
186+
if (getModuleInstanceState(node as ModuleDeclaration) !== ModuleInstanceState.Instantiated) {
187187
return undefined;
188188
}
189189
// falls through
@@ -471,7 +471,7 @@ namespace ts.BreakpointResolver {
471471
function spanInBlock(block: Block): TextSpan {
472472
switch (block.parent.kind) {
473473
case SyntaxKind.ModuleDeclaration:
474-
if (getModuleInstanceState(block.parent) !== ModuleInstanceState.Instantiated) {
474+
if (getModuleInstanceState(block.parent as ModuleDeclaration) !== ModuleInstanceState.Instantiated) {
475475
return undefined;
476476
}
477477
// falls through
@@ -581,7 +581,7 @@ namespace ts.BreakpointResolver {
581581
switch (node.parent.kind) {
582582
case SyntaxKind.ModuleBlock:
583583
// If this is not an instantiated module block, no bp span
584-
if (getModuleInstanceState(node.parent.parent) !== ModuleInstanceState.Instantiated) {
584+
if (getModuleInstanceState(node.parent.parent as ModuleDeclaration) !== ModuleInstanceState.Instantiated) {
585585
return undefined;
586586
}
587587
// falls through

src/services/classifier.ts

+55-73
Original file line numberDiff line numberDiff line change
@@ -488,89 +488,71 @@ namespace ts {
488488

489489
/* @internal */
490490
export function getEncodedSemanticClassifications(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFile: SourceFile, classifiableNames: UnderscoreEscapedMap<true>, span: TextSpan): Classifications {
491-
const result: number[] = [];
492-
processNode(sourceFile);
493-
494-
return { spans: result, endOfLineState: EndOfLineState.None };
495-
496-
function pushClassification(start: number, length: number, type: ClassificationType) {
497-
result.push(start);
498-
result.push(length);
499-
result.push(type);
500-
}
501-
502-
function classifySymbol(symbol: Symbol, meaningAtPosition: SemanticMeaning): ClassificationType {
503-
const flags = symbol.getFlags();
504-
if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) {
491+
const spans: number[] = [];
492+
sourceFile.forEachChild(function cb(node: Node): void {
493+
// Only walk into nodes that intersect the requested span.
494+
if (!node || !textSpanIntersectsWith(span, node.pos, node.getFullWidth())) {
505495
return;
506496
}
507497

508-
if (flags & SymbolFlags.Class) {
509-
return ClassificationType.className;
510-
}
511-
else if (flags & SymbolFlags.Enum) {
512-
return ClassificationType.enumName;
513-
}
514-
else if (flags & SymbolFlags.TypeAlias) {
515-
return ClassificationType.typeAliasName;
516-
}
517-
else if (meaningAtPosition & SemanticMeaning.Type) {
518-
if (flags & SymbolFlags.Interface) {
519-
return ClassificationType.interfaceName;
520-
}
521-
else if (flags & SymbolFlags.TypeParameter) {
522-
return ClassificationType.typeParameterName;
523-
}
524-
}
525-
else if (flags & SymbolFlags.Module) {
526-
// Only classify a module as such if
527-
// - It appears in a namespace context.
528-
// - There exists a module declaration which actually impacts the value side.
529-
if (meaningAtPosition & SemanticMeaning.Namespace ||
530-
(meaningAtPosition & SemanticMeaning.Value && hasValueSideModule(symbol))) {
531-
return ClassificationType.moduleName;
498+
checkForClassificationCancellation(cancellationToken, node.kind);
499+
// Only bother calling into the typechecker if this is an identifier that
500+
// could possibly resolve to a type name. This makes classification run
501+
// in a third of the time it would normally take.
502+
if (isIdentifier(node) && !nodeIsMissing(node) && classifiableNames.has(node.escapedText)) {
503+
const symbol = typeChecker.getSymbolAtLocation(node);
504+
const type = symbol && classifySymbol(symbol, getMeaningFromLocation(node), typeChecker);
505+
if (type) {
506+
pushClassification(node.getStart(sourceFile), node.getEnd(), type);
532507
}
533508
}
534509

535-
return undefined;
510+
node.forEachChild(cb);
511+
});
512+
return { spans, endOfLineState: EndOfLineState.None };
536513

537-
/**
538-
* Returns true if there exists a module that introduces entities on the value side.
539-
*/
540-
function hasValueSideModule(symbol: Symbol): boolean {
541-
return forEach(symbol.declarations, declaration => {
542-
return declaration.kind === SyntaxKind.ModuleDeclaration &&
543-
getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated;
544-
});
545-
}
514+
function pushClassification(start: number, end: number, type: ClassificationType): void {
515+
spans.push(start);
516+
spans.push(end - start);
517+
spans.push(type);
546518
}
519+
}
547520

548-
function processNode(node: Node) {
549-
// Only walk into nodes that intersect the requested span.
550-
if (node && textSpanIntersectsWith(span, node.getFullStart(), node.getFullWidth())) {
551-
const kind = node.kind;
552-
checkForClassificationCancellation(cancellationToken, kind);
553-
554-
if (kind === SyntaxKind.Identifier && !nodeIsMissing(node)) {
555-
const identifier = <Identifier>node;
556-
557-
// Only bother calling into the typechecker if this is an identifier that
558-
// could possibly resolve to a type name. This makes classification run
559-
// in a third of the time it would normally take.
560-
if (classifiableNames.has(identifier.escapedText)) {
561-
const symbol = typeChecker.getSymbolAtLocation(node);
562-
if (symbol) {
563-
const type = classifySymbol(symbol, getMeaningFromLocation(node));
564-
if (type) {
565-
pushClassification(node.getStart(), node.getWidth(), type);
566-
}
567-
}
568-
}
569-
}
570-
571-
forEachChild(node, processNode);
572-
}
521+
function classifySymbol(symbol: Symbol, meaningAtPosition: SemanticMeaning, checker: TypeChecker): ClassificationType | undefined {
522+
const flags = symbol.getFlags();
523+
if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) {
524+
return undefined;
525+
}
526+
else if (flags & SymbolFlags.Class) {
527+
return ClassificationType.className;
573528
}
529+
else if (flags & SymbolFlags.Enum) {
530+
return ClassificationType.enumName;
531+
}
532+
else if (flags & SymbolFlags.TypeAlias) {
533+
return ClassificationType.typeAliasName;
534+
}
535+
else if (flags & SymbolFlags.Module) {
536+
// Only classify a module as such if
537+
// - It appears in a namespace context.
538+
// - There exists a module declaration which actually impacts the value side.
539+
return meaningAtPosition & SemanticMeaning.Namespace || meaningAtPosition & SemanticMeaning.Value && hasValueSideModule(symbol) ? ClassificationType.moduleName : undefined;
540+
}
541+
else if (flags & SymbolFlags.Alias) {
542+
return classifySymbol(checker.getAliasedSymbol(symbol), meaningAtPosition, checker);
543+
}
544+
else if (meaningAtPosition & SemanticMeaning.Type) {
545+
return flags & SymbolFlags.Interface ? ClassificationType.interfaceName : flags & SymbolFlags.TypeParameter ? ClassificationType.typeParameterName : undefined;
546+
}
547+
else {
548+
return undefined;
549+
}
550+
}
551+
552+
/** Returns true if there exists a module that introduces entities on the value side. */
553+
function hasValueSideModule(symbol: Symbol): boolean {
554+
return some(symbol.declarations, declaration =>
555+
isModuleDeclaration(declaration) && getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated);
574556
}
575557

576558
function getClassificationTypeName(type: ClassificationType): ClassificationTypeNames {

src/services/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ namespace ts {
6060
if (isAmbientModule(<ModuleDeclaration>node)) {
6161
return SemanticMeaning.Namespace | SemanticMeaning.Value;
6262
}
63-
else if (getModuleInstanceState(node) === ModuleInstanceState.Instantiated) {
63+
else if (getModuleInstanceState(node as ModuleDeclaration) === ModuleInstanceState.Instantiated) {
6464
return SemanticMeaning.Namespace | SemanticMeaning.Value;
6565
}
6666
else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @Filename: /a.ts
4+
////export type x = number;
5+
////export class y {};
6+
7+
// @Filename: /b.ts
8+
////import { /*0*/x, /*1*/y } from "./a";
9+
////const v: /*2*/x = /*3*/y;
10+
11+
goTo.file("/b.ts");
12+
13+
const [m0, m1, m2, m3] = test.markers();
14+
const c = classification;
15+
verify.semanticClassificationsAre(
16+
c.typeAliasName("x", m0.position),
17+
c.className("y", m1.position),
18+
c.typeAliasName("x", m2.position),
19+
c.className("y", m3.position),
20+
);

0 commit comments

Comments
 (0)