Skip to content

Commit ab529c6

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(compiler-cli): support resolving the symbol of let declaration (#56199)
Updates the symbol builder to handle resolving the symbol of a let declaration. PR Close #56199
1 parent 28e76d7 commit ab529c6

File tree

9 files changed

+187
-19
lines changed

9 files changed

+187
-19
lines changed

packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
TmplAstElement,
11+
TmplAstLetDeclaration,
1112
TmplAstReference,
1213
TmplAstTemplate,
1314
TmplAstVariable,
@@ -31,6 +32,7 @@ export enum SymbolKind {
3132
Expression,
3233
DomBinding,
3334
Pipe,
35+
LetDeclaration,
3436
}
3537

3638
/**
@@ -46,7 +48,8 @@ export type Symbol =
4648
| DirectiveSymbol
4749
| TemplateSymbol
4850
| DomBindingSymbol
49-
| PipeSymbol;
51+
| PipeSymbol
52+
| LetDeclarationSymbol;
5053

5154
/**
5255
* A `Symbol` which declares a new named entity in the template scope.
@@ -242,6 +245,36 @@ export interface VariableSymbol {
242245
initializerLocation: TcbLocation;
243246
}
244247

248+
/**
249+
* A representation of an `@let` declaration in a component template.
250+
*/
251+
export interface LetDeclarationSymbol {
252+
kind: SymbolKind.LetDeclaration;
253+
254+
/** The `ts.Type` of the entity. */
255+
tsType: ts.Type;
256+
257+
/**
258+
* The `ts.Symbol` for the declaration.
259+
*
260+
* This will be `null` if the symbol could not be resolved using the type checker.
261+
*/
262+
tsSymbol: ts.Symbol | null;
263+
264+
/** The node in the `TemplateAst` where the `@let` is declared. */
265+
declaration: TmplAstLetDeclaration;
266+
267+
/**
268+
* The location in the shim file for the identifier of the `@let` declaration.
269+
*/
270+
localVarLocation: TcbLocation;
271+
272+
/**
273+
* The location in the shim file of the `@let` declaration's initializer expression.
274+
*/
275+
initializerLocation: TcbLocation;
276+
}
277+
245278
/**
246279
* A representation of an element in a component template.
247280
*/

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import {
1515
ParseSourceSpan,
1616
PropertyRead,
1717
SafePropertyRead,
18+
TemplateEntity,
1819
TmplAstElement,
1920
TmplAstNode,
20-
TmplAstReference,
2121
TmplAstTemplate,
2222
TmplAstTextAttribute,
23-
TmplAstVariable,
2423
WrappedNodeExpr,
2524
} from '@angular/compiler';
2625
import ts from 'typescript';
@@ -437,10 +436,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
437436
this.isComplete = false;
438437
}
439438

440-
getExpressionTarget(
441-
expression: AST,
442-
clazz: ts.ClassDeclaration,
443-
): TmplAstReference | TmplAstVariable | null {
439+
getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TemplateEntity | null {
444440
return (
445441
this.getLatestComponentState(clazz).data?.boundTarget.getExpressionTarget(expression) || null
446442
);

packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
TmplAstBoundAttribute,
1919
TmplAstBoundEvent,
2020
TmplAstElement,
21+
TmplAstLetDeclaration,
2122
TmplAstNode,
2223
TmplAstReference,
2324
TmplAstTemplate,
@@ -39,6 +40,7 @@ import {
3940
ElementSymbol,
4041
ExpressionSymbol,
4142
InputBindingSymbol,
43+
LetDeclarationSymbol,
4244
OutputBindingSymbol,
4345
PipeSymbol,
4446
ReferenceSymbol,
@@ -81,7 +83,9 @@ export class SymbolBuilder {
8183
) {}
8284

8385
getSymbol(node: TmplAstTemplate | TmplAstElement): TemplateSymbol | ElementSymbol | null;
84-
getSymbol(node: TmplAstReference | TmplAstVariable): ReferenceSymbol | VariableSymbol | null;
86+
getSymbol(
87+
node: TmplAstReference | TmplAstVariable | TmplAstLetDeclaration,
88+
): ReferenceSymbol | VariableSymbol | LetDeclarationSymbol | null;
8589
getSymbol(node: AST | TmplAstNode): Symbol | null;
8690
getSymbol(node: AST | TmplAstNode): Symbol | null {
8791
if (this.symbolCache.has(node)) {
@@ -101,6 +105,8 @@ export class SymbolBuilder {
101105
symbol = this.getSymbolOfAstTemplate(node);
102106
} else if (node instanceof TmplAstVariable) {
103107
symbol = this.getSymbolOfVariable(node);
108+
} else if (node instanceof TmplAstLetDeclaration) {
109+
symbol = this.getSymbolOfLetDeclaration(node);
104110
} else if (node instanceof TmplAstReference) {
105111
symbol = this.getSymbolOfReference(node);
106112
} else if (node instanceof BindingPipe) {
@@ -620,6 +626,36 @@ export class SymbolBuilder {
620626
}
621627
}
622628

629+
private getSymbolOfLetDeclaration(decl: TmplAstLetDeclaration): LetDeclarationSymbol | null {
630+
const node = findFirstMatchingNode(this.typeCheckBlock, {
631+
withSpan: decl.sourceSpan,
632+
filter: ts.isVariableDeclaration,
633+
});
634+
635+
if (node === null) {
636+
return null;
637+
}
638+
639+
const nodeValueSymbol = this.getSymbolOfTsNode(node.initializer!);
640+
641+
if (nodeValueSymbol === null) {
642+
return null;
643+
}
644+
645+
return {
646+
tsType: nodeValueSymbol.tsType,
647+
tsSymbol: nodeValueSymbol.tsSymbol,
648+
initializerLocation: nodeValueSymbol.tcbLocation,
649+
kind: SymbolKind.LetDeclaration,
650+
declaration: decl,
651+
localVarLocation: {
652+
tcbPath: this.tcbPath,
653+
isShimFile: this.tcbIsShim,
654+
positionInFile: this.getTcbPositionForNode(node.name),
655+
},
656+
};
657+
}
658+
623659
private getSymbolOfPipe(expression: BindingPipe): PipeSymbol | null {
624660
const methodAccess = findFirstMatchingNode(this.typeCheckBlock, {
625661
withSpan: expression.nameSpan,
@@ -660,7 +696,7 @@ export class SymbolBuilder {
660696

661697
private getSymbolOfTemplateExpression(
662698
expression: AST,
663-
): VariableSymbol | ReferenceSymbol | ExpressionSymbol | null {
699+
): VariableSymbol | ReferenceSymbol | ExpressionSymbol | LetDeclarationSymbol | null {
664700
if (expression instanceof ASTWithSource) {
665701
expression = expression.ast;
666702
}

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,9 +1983,9 @@ describe('type check blocks', () => {
19831983
{{sum}}
19841984
`);
19851985

1986-
expect(result).toContain('const _t1 = 1;');
1987-
expect(result).toContain('const _t2 = 2;');
1988-
expect(result).toContain('const _t3 = (_t1) + (_t2);');
1986+
expect(result).toContain('const _t1 = (1);');
1987+
expect(result).toContain('const _t2 = (2);');
1988+
expect(result).toContain('const _t3 = ((_t1) + (_t2));');
19891989
expect(result).toContain('"" + (_t3);');
19901990
});
19911991

@@ -1995,7 +1995,7 @@ describe('type check blocks', () => {
19951995
<button (click)="doStuff(value)"></button>
19961996
`);
19971997

1998-
expect(result).toContain('const _t1 = 1;');
1998+
expect(result).toContain('const _t1 = (1);');
19991999
expect(result).toContain('var _t2 = document.createElement("button");');
20002000
expect(result).toContain(
20012001
'_t2.addEventListener("click", ($event): any => { (this).doStuff(_t1); });',

packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ import {
2020
TmplAstNode,
2121
TmplAstReference,
2222
TmplAstTemplate,
23+
AST,
24+
LiteralArray,
25+
LiteralMap,
26+
TmplAstIfBlock,
27+
TmplAstLetDeclaration,
28+
ParseTemplateOptions,
2329
} from '@angular/compiler';
24-
import {AST, LiteralArray, LiteralMap, TmplAstIfBlock} from '@angular/compiler/src/compiler';
2530
import ts from 'typescript';
2631

2732
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
@@ -33,6 +38,7 @@ import {
3338
ElementSymbol,
3439
ExpressionSymbol,
3540
InputBindingSymbol,
41+
LetDeclarationSymbol,
3642
OutputBindingSymbol,
3743
PipeSymbol,
3844
ReferenceSymbol,
@@ -1947,6 +1953,86 @@ runInEachFileSystem(() => {
19471953
});
19481954
});
19491955

1956+
describe('let declarations', () => {
1957+
let templateTypeChecker: TemplateTypeChecker;
1958+
let cmp: ClassDeclaration<ts.ClassDeclaration>;
1959+
let ast: TmplAstNode[];
1960+
let program: ts.Program;
1961+
1962+
beforeEach(() => {
1963+
const fileName = absoluteFrom('/main.ts');
1964+
const dirFile = absoluteFrom('/dir.ts');
1965+
const templateString = `
1966+
@let message = 'The value is ' + value;
1967+
<div [dir]="message"></div>
1968+
`;
1969+
const testValues = setup(
1970+
[
1971+
{
1972+
fileName,
1973+
templates: {'Cmp': templateString},
1974+
source: `
1975+
export class Cmp {
1976+
value = 1;
1977+
}
1978+
`,
1979+
declarations: [
1980+
{
1981+
name: 'TestDir',
1982+
selector: '[dir]',
1983+
file: dirFile,
1984+
type: 'directive',
1985+
exportAs: ['dir'],
1986+
inputs: {dir: 'dir'},
1987+
},
1988+
],
1989+
},
1990+
{
1991+
fileName: dirFile,
1992+
source: `export class TestDir {dir: any;}`,
1993+
templates: {},
1994+
},
1995+
],
1996+
undefined,
1997+
{
1998+
enableLetSyntax: true,
1999+
},
2000+
);
2001+
templateTypeChecker = testValues.templateTypeChecker;
2002+
program = testValues.program;
2003+
const sf = getSourceFileOrError(testValues.program, fileName);
2004+
cmp = getClass(sf, 'Cmp');
2005+
ast = templateTypeChecker.getTemplate(cmp)!;
2006+
});
2007+
2008+
it('should get symbol of a let declaration at the declaration location', () => {
2009+
const symbol = templateTypeChecker.getSymbolOfNode(ast[0] as TmplAstLetDeclaration, cmp)!;
2010+
assertLetDeclarationSymbol(symbol);
2011+
expect(program.getTypeChecker().typeToString(symbol.tsType!)).toBe('string');
2012+
expect(symbol.declaration.name).toBe('message');
2013+
});
2014+
2015+
it('should get symbol of a let declaration at a usage site', () => {
2016+
const symbol = templateTypeChecker.getSymbolOfNode(
2017+
(ast[1] as TmplAstElement).inputs[0].value,
2018+
cmp,
2019+
)!;
2020+
assertLetDeclarationSymbol(symbol);
2021+
expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('string');
2022+
expect(symbol.declaration.name).toEqual('message');
2023+
2024+
// Ensure we can map the shim locations back to the template
2025+
const initializerMapping = templateTypeChecker.getTemplateMappingAtTcbLocation(
2026+
symbol.initializerLocation,
2027+
)!;
2028+
expect(initializerMapping.span.toString()).toEqual(`'The value is ' + value`);
2029+
const localVarMapping = templateTypeChecker.getTemplateMappingAtTcbLocation(
2030+
symbol.localVarLocation,
2031+
)!;
2032+
expect(localVarMapping.span.toString()).toEqual('message');
2033+
});
2034+
});
2035+
19502036
it('elements with generic directives', () => {
19512037
const fileName = absoluteFrom('/main.ts');
19522038
const dirFile = absoluteFrom('/dir.ts');
@@ -2093,9 +2179,18 @@ function assertDomBindingSymbol(tSymbol: Symbol): asserts tSymbol is DomBindingS
20932179
expect(tSymbol.kind).toEqual(SymbolKind.DomBinding);
20942180
}
20952181

2096-
export function setup(targets: TypeCheckingTarget[], config?: Partial<TypeCheckingConfig>) {
2182+
function assertLetDeclarationSymbol(tSymbol: Symbol): asserts tSymbol is LetDeclarationSymbol {
2183+
expect(tSymbol.kind).toEqual(SymbolKind.LetDeclaration);
2184+
}
2185+
2186+
export function setup(
2187+
targets: TypeCheckingTarget[],
2188+
config?: Partial<TypeCheckingConfig>,
2189+
parseOptions?: ParseTemplateOptions,
2190+
) {
20972191
return baseTestSetup(targets, {
20982192
inlining: false,
20992193
config: {...config, enableTemplateTypeChecker: true, useInlineTypeConstructors: false},
2194+
parseOptions,
21002195
});
21012196
}

packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ export function setup(
493493
config?: Partial<TypeCheckingConfig>;
494494
options?: ts.CompilerOptions;
495495
inlining?: boolean;
496+
parseOptions?: ParseTemplateOptions;
496497
} = {},
497498
): {
498499
templateTypeChecker: TemplateTypeChecker;
@@ -594,7 +595,7 @@ export function setup(
594595
const template = target.templates[className];
595596
const templateUrl = `${className}.html`;
596597
const templateFile = new ParseSourceFile(template, templateUrl);
597-
const {nodes, errors} = parseTemplate(template, templateUrl);
598+
const {nodes, errors} = parseTemplate(template, templateUrl, overrides.parseOptions);
598599
if (errors !== null) {
599600
throw new Error('Template parse errors: \n' + errors.join('\n'));
600601
}

packages/language-service/src/completions.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
TmplAstTextAttribute as TextAttribute,
3434
TmplAstUnknownBlock as UnknownBlock,
3535
TmplAstVariable,
36+
TmplAstLetDeclaration,
3637
} from '@angular/compiler';
3738
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
3839
import {
@@ -628,8 +629,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
628629

629630
/**
630631
* Get the `ts.Symbol` of a specific completion for a property expression in a global context
631-
* (e.g.
632-
* `{{y|}}`).
632+
* (e.g. `{{y|}}`).
633633
*/
634634
private getGlobalPropertyExpressionCompletionSymbol(
635635
this: PropertyExpressionCompletionBuilder,
@@ -645,7 +645,8 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
645645
}
646646
const {componentContext, templateContext} = completions;
647647
if (templateContext.has(entryName)) {
648-
const node: TmplAstReference | TmplAstVariable = templateContext.get(entryName)!.node;
648+
const node: TmplAstReference | TmplAstVariable | TmplAstLetDeclaration =
649+
templateContext.get(entryName)!.node;
649650
const symbol = this.templateTypeChecker.getSymbolOfNode(
650651
node,
651652
this.component,

packages/language-service/src/definitions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export class DefinitionBuilder {
111111
component,
112112
}: DefinitionMeta & TemplateInfo): readonly ts.DefinitionInfo[] | undefined {
113113
switch (symbol.kind) {
114+
case SymbolKind.LetDeclaration:
115+
// TODO(crisbeto): will integrate let declarations in a future commit.
116+
return undefined;
114117
case SymbolKind.Directive:
115118
case SymbolKind.Element:
116119
case SymbolKind.Template:

packages/language-service/src/quick_info.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ export class QuickInfoBuilder {
8484

8585
private getQuickInfoForSymbol(symbol: Symbol): ts.QuickInfo | undefined {
8686
switch (symbol.kind) {
87+
case SymbolKind.LetDeclaration:
88+
// TODO(crisbeto): will integrate let declarations in a future commit.
89+
return undefined;
8790
case SymbolKind.Input:
8891
case SymbolKind.Output:
8992
return this.getQuickInfoForBindingSymbol(symbol);

0 commit comments

Comments
 (0)