feat(compiler-cli): Add ability to get `Symbol` of AST expression in component template (#38618)
Adds support to the `TemplateTypeChecker` to get a `Symbol` of an AST expression in a component template. Not all expressions will have `ts.Symbol`s (e.g. there is no `ts.Symbol` associated with the expression `a + b`, but there are for both the a and b nodes individually). PR Close #38618
This commit is contained in:
parent
cf2e8b99a8
commit
f56ece4fdc
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import {AbsoluteSourceSpan, AST, ASTWithSource, BindingPipe, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../file_system';
|
||||
|
@ -40,7 +40,10 @@ export class SymbolBuilder {
|
|||
return this.getSymbolOfElement(node);
|
||||
} else if (node instanceof TmplAstTemplate) {
|
||||
return this.getSymbolOfAstTemplate(node);
|
||||
} else if (node instanceof AST) {
|
||||
return this.getSymbolOfTemplateExpression(node);
|
||||
}
|
||||
// TODO(atscott): TmplAstContent, TmplAstIcu
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -223,6 +226,54 @@ export class SymbolBuilder {
|
|||
};
|
||||
}
|
||||
|
||||
private getSymbolOfTemplateExpression(expression: AST): ExpressionSymbol|null {
|
||||
if (expression instanceof ASTWithSource) {
|
||||
expression = expression.ast;
|
||||
}
|
||||
|
||||
let node = findFirstMatchingNode(
|
||||
this.typeCheckBlock,
|
||||
{withSpan: expression.sourceSpan, filter: (n: ts.Node): n is ts.Node => true});
|
||||
if (node === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (ts.isParenthesizedExpression(node)) {
|
||||
node = node.expression;
|
||||
}
|
||||
|
||||
// - If we have safe property read ("a?.b") we want to get the Symbol for b, the `whenTrue`
|
||||
// expression.
|
||||
// - If our expression is a pipe binding ("a | test:b:c"), we want the Symbol for the
|
||||
// `transform` on the pipe.
|
||||
// - Otherwise, we retrieve the symbol for the node itself with no special considerations
|
||||
if ((expression instanceof SafePropertyRead || expression instanceof SafeMethodCall) &&
|
||||
ts.isConditionalExpression(node)) {
|
||||
const whenTrueSymbol =
|
||||
(expression instanceof SafeMethodCall && ts.isCallExpression(node.whenTrue)) ?
|
||||
this.getSymbolOfTsNode(node.whenTrue.expression) :
|
||||
this.getSymbolOfTsNode(node.whenTrue);
|
||||
if (whenTrueSymbol === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...whenTrueSymbol,
|
||||
kind: SymbolKind.Expression,
|
||||
// Rather than using the type of only the `whenTrue` part of the expression, we should
|
||||
// still get the type of the whole conditional expression to include `|undefined`.
|
||||
tsType: this.typeChecker.getTypeAtLocation(node)
|
||||
};
|
||||
} else if (expression instanceof BindingPipe && ts.isCallExpression(node)) {
|
||||
// TODO(atscott): Create a PipeSymbol to include symbol for the Pipe class
|
||||
const symbolInfo = this.getSymbolOfTsNode(node.expression);
|
||||
return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression};
|
||||
} else {
|
||||
const symbolInfo = this.getSymbolOfTsNode(node);
|
||||
return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression};
|
||||
}
|
||||
}
|
||||
|
||||
private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo|null {
|
||||
while (ts.isParenthesizedExpression(node)) {
|
||||
node = node.expression;
|
||||
|
|
|
@ -6,18 +6,38 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {TmplAstBoundAttribute, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
import {DirectiveSymbol, ElementSymbol, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig} from '../api';
|
||||
import {DirectiveSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig} from '../api';
|
||||
|
||||
import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('TemplateTypeChecker.getSymbolOfNode', () => {
|
||||
it('should not get a symbol for regular attributes', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div id="helloWorld"></div>`;
|
||||
const {templateTypeChecker, program} = setup(
|
||||
[
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `export class Cmp {}`,
|
||||
},
|
||||
],
|
||||
);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const {attributes} = getAstElements(templateTypeChecker, cmp)[0];
|
||||
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp);
|
||||
expect(symbol).toBeNull();
|
||||
});
|
||||
|
||||
describe('templates', () => {
|
||||
describe('ng-templates', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
|
@ -67,6 +87,394 @@ runInEachFileSystem(() => {
|
|||
expect(symbol.directives[0].tsSymbol.getName()).toBe('TestDir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('structural directives', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||
let templateNode: TmplAstTemplate;
|
||||
let program: ts.Program;
|
||||
|
||||
beforeEach(() => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `
|
||||
<div *ngFor="let user of users; let i = index;">
|
||||
{{user.name}} {{user.streetNumber}}
|
||||
<div [tabIndex]="i"></div>
|
||||
</div>`;
|
||||
const testValues = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
export interface User {
|
||||
name: string;
|
||||
streetNumber: number;
|
||||
}
|
||||
export class Cmp { users: User[]; }
|
||||
`,
|
||||
declarations: [ngForDeclaration()],
|
||||
},
|
||||
ngForTypeCheckTarget(),
|
||||
]);
|
||||
templateTypeChecker = testValues.templateTypeChecker;
|
||||
program = testValues.program;
|
||||
const sf = getSourceFileOrError(testValues.program, fileName);
|
||||
cmp = getClass(sf, 'Cmp');
|
||||
templateNode = getAstTemplates(templateTypeChecker, cmp)[0];
|
||||
});
|
||||
|
||||
it('should retrieve a symbol for an expression inside structural binding', () => {
|
||||
const ngForOfBinding =
|
||||
templateNode.templateAttrs.find(a => a.name === 'ngForOf')! as TmplAstBoundAttribute;
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding.value, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('users');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array<User>');
|
||||
});
|
||||
|
||||
it('should retrieve a symbol for property reads of implicit variable inside structural binding',
|
||||
() => {
|
||||
const boundText =
|
||||
(templateNode.children[0] as TmplAstElement).children[0] as TmplAstBoundText;
|
||||
const interpolation = (boundText.value as ASTWithSource).ast as Interpolation;
|
||||
const namePropRead = interpolation.expressions[0] as PropertyRead;
|
||||
const streetNumberPropRead = interpolation.expressions[1] as PropertyRead;
|
||||
|
||||
const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!;
|
||||
assertExpressionSymbol(nameSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name');
|
||||
expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string');
|
||||
|
||||
const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!;
|
||||
assertExpressionSymbol(streetSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!))
|
||||
.toEqual('streetNumber');
|
||||
expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for expressions', () => {
|
||||
it('should get a symbol for a component property used in an input binding', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div [inputA]="helloWorld"></div>`;
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `export class Cmp {helloWorld?: boolean;}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType))
|
||||
.toEqual('false | true | undefined');
|
||||
});
|
||||
|
||||
it('should get a symbol for properties several levels deep', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div [inputA]="person.address.street"></div>`;
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
interface Address {
|
||||
street: string;
|
||||
}
|
||||
|
||||
interface Person {
|
||||
address: Address;
|
||||
}
|
||||
export class Cmp {person?: Person;}
|
||||
`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const inputNode = nodes[0].inputs[0].value as ASTWithSource;
|
||||
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(inputNode, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('street');
|
||||
expect((symbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration).parent.name!.getText())
|
||||
.toEqual('Address');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('string');
|
||||
|
||||
const personSymbol = templateTypeChecker.getSymbolOfNode(
|
||||
((inputNode.ast as PropertyRead).receiver as PropertyRead).receiver, cmp)!;
|
||||
assertExpressionSymbol(personSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(personSymbol.tsSymbol!)).toEqual('person');
|
||||
expect(program.getTypeChecker().typeToString(personSymbol.tsType))
|
||||
.toEqual('Person | undefined');
|
||||
});
|
||||
|
||||
describe('should get symbols for conditionals', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||
let program: ts.Program;
|
||||
let templateString: string;
|
||||
|
||||
beforeEach(() => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
templateString = `
|
||||
<div [inputA]="person?.address?.street"></div>
|
||||
<div [inputA]="person ? person.address : noPersonError"></div>
|
||||
<div [inputA]="person?.speak()"></div>
|
||||
`;
|
||||
const testValues = setup(
|
||||
[
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
interface Address {
|
||||
street: string;
|
||||
}
|
||||
|
||||
interface Person {
|
||||
address: Address;
|
||||
speak(): string;
|
||||
}
|
||||
export class Cmp {person?: Person; noPersonError = 'no person'}
|
||||
`,
|
||||
},
|
||||
],
|
||||
);
|
||||
templateTypeChecker = testValues.templateTypeChecker;
|
||||
program = testValues.program;
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
cmp = getClass(sf, 'Cmp');
|
||||
});
|
||||
|
||||
it('safe property reads', () => {
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
const safePropertyRead = nodes[0].inputs[0].value as ASTWithSource;
|
||||
const propReadSymbol = templateTypeChecker.getSymbolOfNode(safePropertyRead, cmp)!;
|
||||
assertExpressionSymbol(propReadSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(propReadSymbol.tsSymbol!))
|
||||
.toEqual('street');
|
||||
expect((propReadSymbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||
.parent.name!.getText())
|
||||
.toEqual('Address');
|
||||
expect(program.getTypeChecker().typeToString(propReadSymbol.tsType))
|
||||
.toEqual('string | undefined');
|
||||
});
|
||||
|
||||
it('safe method calls', () => {
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
const safeMethodCall = nodes[2].inputs[0].value as ASTWithSource;
|
||||
const methodCallSymbol = templateTypeChecker.getSymbolOfNode(safeMethodCall, cmp)!;
|
||||
assertExpressionSymbol(methodCallSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(methodCallSymbol.tsSymbol!))
|
||||
.toEqual('speak');
|
||||
expect((methodCallSymbol.tsSymbol!.declarations[0] as ts.PropertyDeclaration)
|
||||
.parent.name!.getText())
|
||||
.toEqual('Person');
|
||||
expect(program.getTypeChecker().typeToString(methodCallSymbol.tsType))
|
||||
.toEqual('string | undefined');
|
||||
});
|
||||
|
||||
it('ternary expressions', () => {
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const ternary = (nodes[1].inputs[0].value as ASTWithSource).ast as Conditional;
|
||||
const ternarySymbol = templateTypeChecker.getSymbolOfNode(ternary, cmp)!;
|
||||
assertExpressionSymbol(ternarySymbol);
|
||||
expect(ternarySymbol.tsSymbol).toBeNull();
|
||||
expect(program.getTypeChecker().typeToString(ternarySymbol.tsType))
|
||||
.toEqual('string | Address');
|
||||
const addrSymbol = templateTypeChecker.getSymbolOfNode(ternary.trueExp, cmp)!;
|
||||
assertExpressionSymbol(addrSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(addrSymbol.tsSymbol!)).toEqual('address');
|
||||
expect(program.getTypeChecker().typeToString(addrSymbol.tsType)).toEqual('Address');
|
||||
|
||||
const noPersonSymbol = templateTypeChecker.getSymbolOfNode(ternary.falseExp, cmp)!;
|
||||
assertExpressionSymbol(noPersonSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(noPersonSymbol.tsSymbol!))
|
||||
.toEqual('noPersonError');
|
||||
expect(program.getTypeChecker().typeToString(noPersonSymbol.tsType)).toEqual('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a symbol for function on a component used in an input binding', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div [inputA]="helloWorld"></div>`;
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
export class Cmp {
|
||||
helloWorld() { return ''; }
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('() => string');
|
||||
});
|
||||
|
||||
it('should get a symbol for binary expressions', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div [inputA]="a + b"></div>`;
|
||||
const {templateTypeChecker, program} = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
export class Cmp {
|
||||
a!: string;
|
||||
b!: number;
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
const sf = getSourceFileOrError(program, fileName);
|
||||
const cmp = getClass(sf, 'Cmp');
|
||||
const nodes = getAstElements(templateTypeChecker, cmp);
|
||||
|
||||
const valueAssignment = nodes[0].inputs[0].value as ASTWithSource;
|
||||
const wholeExprSymbol = templateTypeChecker.getSymbolOfNode(valueAssignment, cmp)!;
|
||||
assertExpressionSymbol(wholeExprSymbol);
|
||||
expect(wholeExprSymbol.tsSymbol).toBeNull();
|
||||
expect(program.getTypeChecker().typeToString(wholeExprSymbol.tsType)).toEqual('string');
|
||||
|
||||
const aSymbol =
|
||||
templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).left, cmp)!;
|
||||
assertExpressionSymbol(aSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toBe('a');
|
||||
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
||||
|
||||
const bSymbol =
|
||||
templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).right, cmp)!;
|
||||
assertExpressionSymbol(bSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toBe('b');
|
||||
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
||||
});
|
||||
|
||||
describe('literals', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||
let interpolation: Interpolation;
|
||||
let program: ts.Program;
|
||||
|
||||
beforeEach(() => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `
|
||||
{{ [1, 2, 3] }}
|
||||
{{ { hello: "world" } }}`;
|
||||
const testValues = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `export class Cmp {}`,
|
||||
},
|
||||
]);
|
||||
templateTypeChecker = testValues.templateTypeChecker;
|
||||
program = testValues.program;
|
||||
const sf = getSourceFileOrError(testValues.program, fileName);
|
||||
cmp = getClass(sf, 'Cmp');
|
||||
interpolation = ((templateTypeChecker.getTemplate(cmp)![0] as TmplAstBoundText).value as
|
||||
ASTWithSource)
|
||||
.ast as Interpolation;
|
||||
});
|
||||
|
||||
it('literal array', () => {
|
||||
const literalArray = interpolation.expressions[0];
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array<number>');
|
||||
});
|
||||
|
||||
it('literal map', () => {
|
||||
const literalMap = interpolation.expressions[1];
|
||||
const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!;
|
||||
assertExpressionSymbol(symbol);
|
||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object');
|
||||
expect(program.getTypeChecker().typeToString(symbol.tsType))
|
||||
.toEqual('{ hello: string; }');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('pipes', () => {
|
||||
let templateTypeChecker: TemplateTypeChecker;
|
||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||
let binding: BindingPipe;
|
||||
let program: ts.Program;
|
||||
|
||||
beforeEach(() => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const templateString = `<div [inputA]="a | test:b:c"></div>`;
|
||||
const testValues = setup([
|
||||
{
|
||||
fileName,
|
||||
templates: {'Cmp': templateString},
|
||||
source: `
|
||||
export class Cmp { a: string; b: number; c: boolean }
|
||||
export class TestPipe {
|
||||
transform(value: string, repeat: number, commaSeparate: boolean): string[] {
|
||||
}
|
||||
}
|
||||
`,
|
||||
declarations: [{
|
||||
type: 'pipe',
|
||||
name: 'TestPipe',
|
||||
pipeName: 'test',
|
||||
}],
|
||||
},
|
||||
]);
|
||||
program = testValues.program;
|
||||
templateTypeChecker = testValues.templateTypeChecker;
|
||||
const sf = getSourceFileOrError(testValues.program, fileName);
|
||||
cmp = getClass(sf, 'Cmp');
|
||||
binding =
|
||||
(getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource).ast as
|
||||
BindingPipe;
|
||||
});
|
||||
|
||||
it('should get symbol for pipe', () => {
|
||||
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
|
||||
assertExpressionSymbol(pipeSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!))
|
||||
.toEqual('transform');
|
||||
expect(
|
||||
(pipeSymbol.tsSymbol!.declarations[0].parent as ts.ClassDeclaration).name!.getText())
|
||||
.toEqual('TestPipe');
|
||||
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType))
|
||||
.toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]');
|
||||
});
|
||||
|
||||
it('should get symbols for pipe expression and args', () => {
|
||||
const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!;
|
||||
assertExpressionSymbol(aSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a');
|
||||
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
||||
|
||||
const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0], cmp)!;
|
||||
assertExpressionSymbol(bSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b');
|
||||
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
||||
|
||||
const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1], cmp)!;
|
||||
assertExpressionSymbol(cSymbol);
|
||||
expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c');
|
||||
expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('input bindings', () => {
|
||||
|
@ -627,6 +1035,15 @@ function onlyAstTemplates(nodes: TmplAstNode[]): TmplAstTemplate[] {
|
|||
return nodes.filter((n): n is TmplAstTemplate => n instanceof TmplAstTemplate);
|
||||
}
|
||||
|
||||
function onlyAstElements(nodes: TmplAstNode[]): TmplAstElement[] {
|
||||
return nodes.filter((n): n is TmplAstElement => n instanceof TmplAstElement);
|
||||
}
|
||||
|
||||
function getAstElements(
|
||||
templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) {
|
||||
return onlyAstElements(templateTypeChecker.getTemplate(cmp)!);
|
||||
}
|
||||
|
||||
function getAstTemplates(
|
||||
templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) {
|
||||
return onlyAstTemplates(templateTypeChecker.getTemplate(cmp)!);
|
||||
|
@ -648,6 +1065,10 @@ function assertTemplateSymbol(tSymbol: Symbol): asserts tSymbol is TemplateSymbo
|
|||
expect(tSymbol.kind).toEqual(SymbolKind.Template);
|
||||
}
|
||||
|
||||
function assertExpressionSymbol(tSymbol: Symbol): asserts tSymbol is ExpressionSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Expression);
|
||||
}
|
||||
|
||||
function assertElementSymbol(tSymbol: Symbol): asserts tSymbol is ElementSymbol {
|
||||
expect(tSymbol.kind).toEqual(SymbolKind.Element);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue