refactor(compiler-cli): Enable pipe information when checkTypeOfPipes=false (#39555)
When `checkTypeOfPipes` is set to `false`, the configuration is meant to ignore the signature of the pipe's `transform` method for diagnostics. However, we still should produce some information about the pipe for the `TemplateTypeChecker`. This change refactors the returned symbol for pipes so that it also includes information about the pipe's class instance as it appears in the TCB. PR Close #39555
This commit is contained in:
parent
6e4e68cb30
commit
2b74a05a65
|
@ -24,13 +24,14 @@ export enum SymbolKind {
|
||||||
Template,
|
Template,
|
||||||
Expression,
|
Expression,
|
||||||
DomBinding,
|
DomBinding,
|
||||||
|
Pipe,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of an entity in the `TemplateAst`.
|
* A representation of an entity in the `TemplateAst`.
|
||||||
*/
|
*/
|
||||||
export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol|
|
export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol|
|
||||||
VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol|DomBindingSymbol;
|
VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol|DomBindingSymbol|PipeSymbol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `Symbol` which declares a new named entity in the template scope.
|
* A `Symbol` which declares a new named entity in the template scope.
|
||||||
|
@ -276,3 +277,37 @@ export interface DomBindingSymbol {
|
||||||
/** The symbol for the element or template of the text attribute. */
|
/** The symbol for the element or template of the text attribute. */
|
||||||
host: ElementSymbol|TemplateSymbol;
|
host: ElementSymbol|TemplateSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation for a call to a pipe's transform method in the TCB.
|
||||||
|
*/
|
||||||
|
export interface PipeSymbol {
|
||||||
|
kind: SymbolKind.Pipe;
|
||||||
|
|
||||||
|
/** The `ts.Type` of the transform node. */
|
||||||
|
tsType: ts.Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `ts.Symbol` for the transform call. This could be `null` when `checkTypeOfPipes` is set to
|
||||||
|
* `false` because the transform call would be of the form `(_pipe1 as any).transform()`
|
||||||
|
*/
|
||||||
|
tsSymbol: ts.Symbol|null;
|
||||||
|
|
||||||
|
/** The position of the transform call in the template. */
|
||||||
|
shimLocation: ShimLocation;
|
||||||
|
|
||||||
|
/** The symbol for the pipe class as an instance that appears in the TCB. */
|
||||||
|
classSymbol: ClassSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents an instance of a class found in the TCB, i.e. `var _pipe1: MyPipe = null!; */
|
||||||
|
export interface ClassSymbol {
|
||||||
|
/** The `ts.Type` of class. */
|
||||||
|
tsType: ts.Type;
|
||||||
|
|
||||||
|
/** The `ts.Symbol` for class. */
|
||||||
|
tsSymbol: ts.Symbol;
|
||||||
|
|
||||||
|
/** The position for the variable declaration for the class instance. */
|
||||||
|
shimLocation: ShimLocation;
|
||||||
|
}
|
||||||
|
|
|
@ -257,7 +257,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||||
boundTarget,
|
boundTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node);
|
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node, pipes);
|
||||||
|
|
||||||
// If inlining is not supported, but is required for either the TCB or one of its directive
|
// If inlining is not supported, but is required for either the TCB or one of its directive
|
||||||
// dependencies, then exit here with an error.
|
// dependencies, then exit here with an error.
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
|
||||||
import {ClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
|
import {ClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {Reference} from '../../imports';
|
||||||
import {getTokenAtPosition} from '../../util/src/typescript';
|
import {getTokenAtPosition} from '../../util/src/typescript';
|
||||||
import {FullTemplateMapping, SourceLocation, TemplateId, TemplateSourceMapping} from '../api';
|
import {FullTemplateMapping, SourceLocation, TemplateId, TemplateSourceMapping} from '../api';
|
||||||
|
|
||||||
|
@ -37,7 +38,9 @@ export interface TemplateSourceResolver {
|
||||||
toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null;
|
toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requiresInlineTypeCheckBlock(node: ClassDeclaration<ts.ClassDeclaration>): boolean {
|
export function requiresInlineTypeCheckBlock(
|
||||||
|
node: ClassDeclaration<ts.ClassDeclaration>,
|
||||||
|
usedPipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>): boolean {
|
||||||
// In order to qualify for a declared TCB (not inline) two conditions must be met:
|
// In order to qualify for a declared TCB (not inline) two conditions must be met:
|
||||||
// 1) the class must be exported
|
// 1) the class must be exported
|
||||||
// 2) it must not have constrained generic types
|
// 2) it must not have constrained generic types
|
||||||
|
@ -47,6 +50,11 @@ export function requiresInlineTypeCheckBlock(node: ClassDeclaration<ts.ClassDecl
|
||||||
} else if (!checkIfGenericTypesAreUnbound(node)) {
|
} else if (!checkIfGenericTypesAreUnbound(node)) {
|
||||||
// Condition 2 is false, the class has constrained generic types
|
// Condition 2 is false, the class has constrained generic types
|
||||||
return true;
|
return true;
|
||||||
|
} else if (Array.from(usedPipes.values())
|
||||||
|
.some(pipeRef => !checkIfClassIsExported(pipeRef.node))) {
|
||||||
|
// If one of the pipes used by the component is not exported, a non-inline TCB will not be able
|
||||||
|
// to import it, so this requires an inline TCB.
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {AbsoluteFsPath} from '../../file_system';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {ComponentScopeReader} from '../../scope';
|
import {ComponentScopeReader} from '../../scope';
|
||||||
import {isAssignment} from '../../util/src/typescript';
|
import {isAssignment} from '../../util/src/typescript';
|
||||||
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api';
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api';
|
||||||
|
|
||||||
import {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments';
|
import {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments';
|
||||||
import {TemplateData} from './context';
|
import {TemplateData} from './context';
|
||||||
|
@ -61,6 +61,8 @@ export class SymbolBuilder {
|
||||||
symbol = this.getSymbolOfVariable(node);
|
symbol = this.getSymbolOfVariable(node);
|
||||||
} else if (node instanceof TmplAstReference) {
|
} else if (node instanceof TmplAstReference) {
|
||||||
symbol = this.getSymbolOfReference(node);
|
symbol = this.getSymbolOfReference(node);
|
||||||
|
} else if (node instanceof BindingPipe) {
|
||||||
|
symbol = this.getSymbolOfPipe(node);
|
||||||
} else if (node instanceof AST) {
|
} else if (node instanceof AST) {
|
||||||
symbol = this.getSymbolOfTemplateExpression(node);
|
symbol = this.getSymbolOfTemplateExpression(node);
|
||||||
} else {
|
} else {
|
||||||
|
@ -397,6 +399,45 @@ export class SymbolBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSymbolOfPipe(expression: BindingPipe): PipeSymbol|null {
|
||||||
|
const node = findFirstMatchingNode(
|
||||||
|
this.typeCheckBlock, {withSpan: expression.sourceSpan, filter: ts.isCallExpression});
|
||||||
|
if (node === null || !ts.isPropertyAccessExpression(node.expression)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodAccess = node.expression;
|
||||||
|
// Find the node for the pipe variable from the transform property access. This will be one of
|
||||||
|
// two forms: `_pipe1.transform` or `(_pipe1 as any).transform`.
|
||||||
|
const pipeVariableNode = ts.isParenthesizedExpression(methodAccess.expression) &&
|
||||||
|
ts.isAsExpression(methodAccess.expression.expression) ?
|
||||||
|
methodAccess.expression.expression.expression :
|
||||||
|
methodAccess.expression;
|
||||||
|
const pipeDeclaration = this.getTypeChecker().getSymbolAtLocation(pipeVariableNode);
|
||||||
|
if (pipeDeclaration === undefined || pipeDeclaration.valueDeclaration === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeInstance = this.getSymbolOfTsNode(pipeDeclaration.valueDeclaration);
|
||||||
|
if (pipeInstance === null || pipeInstance.tsSymbol === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbolInfo = this.getSymbolOfTsNode(methodAccess);
|
||||||
|
if (symbolInfo === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: SymbolKind.Pipe,
|
||||||
|
...symbolInfo,
|
||||||
|
classSymbol: {
|
||||||
|
...pipeInstance,
|
||||||
|
tsSymbol: pipeInstance.tsSymbol,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getSymbolOfTemplateExpression(expression: AST): VariableSymbol|ReferenceSymbol
|
private getSymbolOfTemplateExpression(expression: AST): VariableSymbol|ReferenceSymbol
|
||||||
|ExpressionSymbol|null {
|
|ExpressionSymbol|null {
|
||||||
if (expression instanceof ASTWithSource) {
|
if (expression instanceof ASTWithSource) {
|
||||||
|
@ -446,10 +487,6 @@ export class SymbolBuilder {
|
||||||
// still get the type of the whole conditional expression to include `|undefined`.
|
// still get the type of the whole conditional expression to include `|undefined`.
|
||||||
tsType: this.getTypeChecker().getTypeAtLocation(node)
|
tsType: this.getTypeChecker().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 {
|
} else {
|
||||||
const symbolInfo = this.getSymbolOfTsNode(node);
|
const symbolInfo = this.getSymbolOfTsNode(node);
|
||||||
return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression};
|
return symbolInfo === null ? null : {...symbolInfo, kind: SymbolKind.Expression};
|
||||||
|
|
|
@ -1599,7 +1599,8 @@ class TcbExpressionTranslator {
|
||||||
pipe = this.tcb.env.pipeInst(pipeRef);
|
pipe = this.tcb.env.pipeInst(pipeRef);
|
||||||
} else {
|
} else {
|
||||||
// Use an 'any' value when not checking the type of the pipe.
|
// Use an 'any' value when not checking the type of the pipe.
|
||||||
pipe = NULL_AS_ANY;
|
pipe = ts.createAsExpression(
|
||||||
|
this.tcb.env.pipeInst(pipeRef), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||||
}
|
}
|
||||||
const args = ast.args.map(arg => this.translate(arg));
|
const args = ast.args.map(arg => this.translate(arg));
|
||||||
const methodAccess = ts.createPropertyAccess(pipe, 'transform');
|
const methodAccess = ts.createPropertyAccess(pipe, 'transform');
|
||||||
|
|
|
@ -973,7 +973,8 @@ describe('type check blocks', () => {
|
||||||
it('should not check types of pipes when disabled', () => {
|
it('should not check types of pipes when disabled', () => {
|
||||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfPipes: false};
|
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfPipes: false};
|
||||||
const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG);
|
const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG);
|
||||||
expect(block).toContain('(null as any).transform(((ctx).a), ((ctx).b), ((ctx).c))');
|
expect(block).toContain(
|
||||||
|
'((null as TestPipe) as any).transform(((ctx).a), ((ctx).b), ((ctx).c))');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
||||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
import {runInEachFileSystem} from '../../file_system/testing';
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {ClassDeclaration} from '../../reflection';
|
import {ClassDeclaration} from '../../reflection';
|
||||||
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api';
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api';
|
||||||
|
|
||||||
import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
||||||
|
|
||||||
|
@ -710,34 +710,35 @@ runInEachFileSystem(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('pipes', () => {
|
describe('pipes', () => {
|
||||||
let templateTypeChecker: TemplateTypeChecker;
|
let templateTypeChecker: TemplateTypeChecker;
|
||||||
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
let cmp: ClassDeclaration<ts.ClassDeclaration>;
|
||||||
let binding: BindingPipe;
|
let binding: BindingPipe;
|
||||||
let program: ts.Program;
|
let program: ts.Program;
|
||||||
|
|
||||||
beforeEach(() => {
|
function setupPipesTest(checkTypeOfPipes = true) {
|
||||||
const fileName = absoluteFrom('/main.ts');
|
const fileName = absoluteFrom('/main.ts');
|
||||||
const templateString = `<div [inputA]="a | test:b:c"></div>`;
|
const templateString = `<div [inputA]="a | test:b:c"></div>`;
|
||||||
const testValues = setup([
|
const testValues = setup(
|
||||||
{
|
[
|
||||||
fileName,
|
{
|
||||||
templates: {'Cmp': templateString},
|
fileName,
|
||||||
source: `
|
templates: {'Cmp': templateString},
|
||||||
|
source: `
|
||||||
export class Cmp { a: string; b: number; c: boolean }
|
export class Cmp { a: string; b: number; c: boolean }
|
||||||
export class TestPipe {
|
export class TestPipe {
|
||||||
transform(value: string, repeat: number, commaSeparate: boolean): string[] {
|
transform(value: string, repeat: number, commaSeparate: boolean): string[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
declarations: [{
|
declarations: [{
|
||||||
type: 'pipe',
|
type: 'pipe',
|
||||||
name: 'TestPipe',
|
name: 'TestPipe',
|
||||||
pipeName: 'test',
|
pipeName: 'test',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
]);
|
],
|
||||||
|
{checkTypeOfPipes});
|
||||||
program = testValues.program;
|
program = testValues.program;
|
||||||
templateTypeChecker = testValues.templateTypeChecker;
|
templateTypeChecker = testValues.templateTypeChecker;
|
||||||
const sf = getSourceFileOrError(testValues.program, fileName);
|
const sf = getSourceFileOrError(testValues.program, fileName);
|
||||||
|
@ -745,38 +746,55 @@ runInEachFileSystem(() => {
|
||||||
binding =
|
binding =
|
||||||
(getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource).ast as
|
(getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource).ast as
|
||||||
BindingPipe;
|
BindingPipe;
|
||||||
});
|
}
|
||||||
|
|
||||||
it('should get symbol for pipe', () => {
|
it('should get symbol for pipe', () => {
|
||||||
|
setupPipesTest();
|
||||||
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
|
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
|
||||||
assertExpressionSymbol(pipeSymbol);
|
assertPipeSymbol(pipeSymbol);
|
||||||
expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!))
|
expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!))
|
||||||
.toEqual('transform');
|
.toEqual('transform');
|
||||||
expect(
|
expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol))
|
||||||
(pipeSymbol.tsSymbol!.declarations[0].parent as ts.ClassDeclaration).name!.getText())
|
|
||||||
.toEqual('TestPipe');
|
.toEqual('TestPipe');
|
||||||
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType))
|
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!))
|
||||||
.toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]');
|
.toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get symbols for pipe expression and args', () => {
|
it('should get symbol for pipe, checkTypeOfPipes: false', () => {
|
||||||
const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!;
|
setupPipesTest(false);
|
||||||
assertExpressionSymbol(aSymbol);
|
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
|
||||||
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a');
|
assertPipeSymbol(pipeSymbol);
|
||||||
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
expect(pipeSymbol.tsSymbol).toBeNull();
|
||||||
|
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual('any');
|
||||||
const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0], cmp)!;
|
expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol))
|
||||||
assertExpressionSymbol(bSymbol);
|
.toEqual('TestPipe');
|
||||||
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b');
|
expect(program.getTypeChecker().typeToString(pipeSymbol.classSymbol.tsType))
|
||||||
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
.toEqual('TestPipe');
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
for (const checkTypeOfPipes of [true, false]) {
|
||||||
|
describe(`checkTypeOfPipes: ${checkTypeOfPipes}`, () => {
|
||||||
|
// Because the args are property reads, we still need information about them.
|
||||||
|
it(`should get symbols for pipe expression and args`, () => {
|
||||||
|
setupPipesTest(checkTypeOfPipes);
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should get a symbol for PropertyWrite expressions', () => {
|
it('should get a symbol for PropertyWrite expressions', () => {
|
||||||
const fileName = absoluteFrom('/main.ts');
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
@ -1515,6 +1533,10 @@ function assertExpressionSymbol(tSymbol: Symbol): asserts tSymbol is ExpressionS
|
||||||
expect(tSymbol.kind).toEqual(SymbolKind.Expression);
|
expect(tSymbol.kind).toEqual(SymbolKind.Expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertPipeSymbol(tSymbol: Symbol): asserts tSymbol is PipeSymbol {
|
||||||
|
expect(tSymbol.kind).toEqual(SymbolKind.Pipe);
|
||||||
|
}
|
||||||
|
|
||||||
function assertElementSymbol(tSymbol: Symbol): asserts tSymbol is ElementSymbol {
|
function assertElementSymbol(tSymbol: Symbol): asserts tSymbol is ElementSymbol {
|
||||||
expect(tSymbol.kind).toEqual(SymbolKind.Element);
|
expect(tSymbol.kind).toEqual(SymbolKind.Element);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,15 @@ export class DefinitionBuilder {
|
||||||
// LS users to "go to definition" on an item in the template that maps to a class and be
|
// LS users to "go to definition" on an item in the template that maps to a class and be
|
||||||
// taken to the directive or HTML class.
|
// taken to the directive or HTML class.
|
||||||
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
||||||
|
case SymbolKind.Pipe: {
|
||||||
|
if (symbol.tsSymbol !== null) {
|
||||||
|
return this.getDefinitionsForSymbols(symbol);
|
||||||
|
} else {
|
||||||
|
// If there is no `ts.Symbol` for the pipe transform, we want to return the
|
||||||
|
// type definition (the pipe class).
|
||||||
|
return this.getTypeDefinitionsForSymbols(symbol.classSymbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
case SymbolKind.Output:
|
case SymbolKind.Output:
|
||||||
case SymbolKind.Input: {
|
case SymbolKind.Input: {
|
||||||
const bindingDefs = this.getDefinitionsForSymbols(...symbol.bindings);
|
const bindingDefs = this.getDefinitionsForSymbols(...symbol.bindings);
|
||||||
|
@ -135,6 +144,15 @@ export class DefinitionBuilder {
|
||||||
node, definitionMeta.parent, templateInfo.component);
|
node, definitionMeta.parent, templateInfo.component);
|
||||||
return [...bindingDefs, ...directiveDefs];
|
return [...bindingDefs, ...directiveDefs];
|
||||||
}
|
}
|
||||||
|
case SymbolKind.Pipe: {
|
||||||
|
if (symbol.tsSymbol !== null) {
|
||||||
|
return this.getTypeDefinitionsForSymbols(symbol);
|
||||||
|
} else {
|
||||||
|
// If there is no `ts.Symbol` for the pipe transform, we want to return the
|
||||||
|
// type definition (the pipe class).
|
||||||
|
return this.getTypeDefinitionsForSymbols(symbol.classSymbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
case SymbolKind.Reference:
|
case SymbolKind.Reference:
|
||||||
return this.getTypeDefinitionsForSymbols({shimLocation: symbol.targetLocation});
|
return this.getTypeDefinitionsForSymbols({shimLocation: symbol.targetLocation});
|
||||||
case SymbolKind.Expression:
|
case SymbolKind.Expression:
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AST, BindingPipe, ImplicitReceiver, MethodCall, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
import {AST, ImplicitReceiver, MethodCall, ThisReceiver, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
||||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
||||||
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
|
import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTextSpanOfNode} from './utils';
|
||||||
|
|
||||||
export class QuickInfoBuilder {
|
export class QuickInfoBuilder {
|
||||||
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
||||||
|
@ -47,10 +47,10 @@ export class QuickInfoBuilder {
|
||||||
return this.getQuickInfoForDomBinding(symbol);
|
return this.getQuickInfoForDomBinding(symbol);
|
||||||
case SymbolKind.Directive:
|
case SymbolKind.Directive:
|
||||||
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
||||||
|
case SymbolKind.Pipe:
|
||||||
|
return this.getQuickInfoForPipeSymbol(symbol);
|
||||||
case SymbolKind.Expression:
|
case SymbolKind.Expression:
|
||||||
return this.node instanceof BindingPipe ?
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
||||||
this.getQuickInfoForPipeSymbol(symbol) :
|
|
||||||
this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +93,16 @@ export class QuickInfoBuilder {
|
||||||
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol): ts.QuickInfo|undefined {
|
private getQuickInfoForPipeSymbol(symbol: PipeSymbol): ts.QuickInfo|undefined {
|
||||||
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
if (symbol.tsSymbol !== null) {
|
||||||
return quickInfo === undefined ? undefined :
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation);
|
||||||
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
|
return quickInfo === undefined ? undefined :
|
||||||
|
updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE);
|
||||||
|
} else {
|
||||||
|
return createQuickInfo(
|
||||||
|
this.typeChecker.typeToString(symbol.classSymbol.tsType), DisplayInfoKind.PIPE,
|
||||||
|
getTextSpanOfNode(this.node));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getQuickInfoForDomBinding(symbol: DomBindingSymbol) {
|
private getQuickInfoForDomBinding(symbol: DomBindingSymbol) {
|
||||||
|
|
|
@ -95,6 +95,7 @@ export class ReferenceBuilder {
|
||||||
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
|
const {shimPath, positionInShimFile} = symbol.bindings[0].shimLocation;
|
||||||
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
||||||
}
|
}
|
||||||
|
case SymbolKind.Pipe:
|
||||||
case SymbolKind.Expression: {
|
case SymbolKind.Expression: {
|
||||||
const {shimPath, positionInShimFile} = symbol.shimLocation;
|
const {shimPath, positionInShimFile} = symbol.shimLocation;
|
||||||
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
return this.getReferencesAtTypescriptPosition(shimPath, positionInShimFile);
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
|
import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||||
|
|
||||||
|
import {LanguageServiceTestEnvironment} from './env';
|
||||||
|
import {humanizeDefinitionInfo} from './test_utils';
|
||||||
|
|
||||||
|
describe('definitions', () => {
|
||||||
|
let env: LanguageServiceTestEnvironment;
|
||||||
|
|
||||||
|
it('returns the pipe class as definition when checkTypeOfPipes is false', () => {
|
||||||
|
initMockFileSystem('Native');
|
||||||
|
const testFiles: TestFile[] = [
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/app.ts'),
|
||||||
|
contents: `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({templateUrl: 'app.html'})
|
||||||
|
export class AppCmp {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [AppCmp], imports: [CommonModule]})
|
||||||
|
export class AppModule {}
|
||||||
|
`,
|
||||||
|
isRoot: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/app.html'),
|
||||||
|
contents: `Will be overridden`,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// checkTypeOfPipes is set to false when strict templates is false
|
||||||
|
env = LanguageServiceTestEnvironment.setup(testFiles, {strictTemplates: false});
|
||||||
|
const definitions = getDefinitionsAndAssertBoundSpan(
|
||||||
|
{templateOverride: '{{"1/1/2020" | dat¦e}}', expectedSpanText: 'date'});
|
||||||
|
expect(definitions!.length).toEqual(1);
|
||||||
|
|
||||||
|
const [def] = definitions;
|
||||||
|
expect(def.textSpan).toContain('DatePipe');
|
||||||
|
expect(def.contextSpan).toContain('DatePipe');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDefinitionsAndAssertBoundSpan(
|
||||||
|
{templateOverride, expectedSpanText}: {templateOverride: string, expectedSpanText: string}):
|
||||||
|
Array<{textSpan: string, contextSpan: string | undefined, fileName: string}> {
|
||||||
|
const {cursor, text} =
|
||||||
|
env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride);
|
||||||
|
env.expectNoSourceDiagnostics();
|
||||||
|
env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp');
|
||||||
|
const definitionAndBoundSpan =
|
||||||
|
env.ngLS.getDefinitionAndBoundSpan(absoluteFrom('/app.html'), cursor);
|
||||||
|
const {textSpan, definitions} = definitionAndBoundSpan!;
|
||||||
|
expect(text.substring(textSpan.start, textSpan.start + textSpan.length))
|
||||||
|
.toEqual(expectedSpanText);
|
||||||
|
expect(definitions).toBeTruthy();
|
||||||
|
const overrides = new Map<string, string>();
|
||||||
|
overrides.set(absoluteFrom('/app.ts'), text);
|
||||||
|
return definitions!.map(d => humanizeDefinitionInfo(d, env.host, overrides));
|
||||||
|
}
|
||||||
|
});
|
|
@ -507,6 +507,15 @@ describe('quick info', () => {
|
||||||
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work for pipes even if checkTypeOfPipes is false', () => {
|
||||||
|
initMockFileSystem('Native');
|
||||||
|
// checkTypeOfPipes is set to false when strict templates is false
|
||||||
|
env = LanguageServiceTestEnvironment.setup(quickInfoSkeleton(), {strictTemplates: false});
|
||||||
|
const templateOverride = `<p>The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}</p>`;
|
||||||
|
expectQuickInfo(
|
||||||
|
{templateOverride, expectedSpanText: 'date', expectedDisplayString: '(pipe) DatePipe'});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectQuickInfo(
|
function expectQuickInfo(
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||||
import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env';
|
import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env';
|
||||||
|
|
||||||
|
import {MockServerHost} from './mock_host';
|
||||||
|
|
||||||
export function getText(contents: string, textSpan: ts.TextSpan) {
|
export function getText(contents: string, textSpan: ts.TextSpan) {
|
||||||
return contents.substr(textSpan.start, textSpan.length);
|
return contents.substr(textSpan.start, textSpan.length);
|
||||||
}
|
}
|
||||||
|
@ -51,3 +53,25 @@ export function createModuleWithDeclarations(
|
||||||
return LanguageServiceTestEnvironment.setup(
|
return LanguageServiceTestEnvironment.setup(
|
||||||
[moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]);
|
[moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HumanizedDefinitionInfo {
|
||||||
|
fileName: string;
|
||||||
|
textSpan: string;
|
||||||
|
contextSpan: string|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanizeDefinitionInfo(
|
||||||
|
def: ts.DefinitionInfo, host: MockServerHost,
|
||||||
|
overrides: Map<string, string> = new Map()): HumanizedDefinitionInfo {
|
||||||
|
const contents = (overrides.get(def.fileName) !== undefined ? overrides.get(def.fileName) :
|
||||||
|
host.readFile(def.fileName)) ??
|
||||||
|
'';
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileName: def.fileName,
|
||||||
|
textSpan: contents.substr(def.textSpan.start, def.textSpan.start + def.textSpan.length),
|
||||||
|
contextSpan: def.contextSpan ?
|
||||||
|
contents.substr(def.contextSpan.start, def.contextSpan.start + def.contextSpan.length) :
|
||||||
|
undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
|
import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
||||||
|
|
||||||
|
import {LanguageServiceTestEnvironment} from './env';
|
||||||
|
import {HumanizedDefinitionInfo, humanizeDefinitionInfo} from './test_utils';
|
||||||
|
|
||||||
|
describe('type definitions', () => {
|
||||||
|
let env: LanguageServiceTestEnvironment;
|
||||||
|
|
||||||
|
it('returns the pipe class as definition when checkTypeOfPipes is false', () => {
|
||||||
|
initMockFileSystem('Native');
|
||||||
|
const testFiles: TestFile[] = [
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/app.ts'),
|
||||||
|
contents: `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({templateUrl: 'app.html'})
|
||||||
|
export class AppCmp {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [AppCmp], imports: [CommonModule]})
|
||||||
|
export class AppModule {}
|
||||||
|
`,
|
||||||
|
isRoot: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/app.html'),
|
||||||
|
contents: `Will be overridden`,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// checkTypeOfPipes is set to false when strict templates is false
|
||||||
|
env = LanguageServiceTestEnvironment.setup(testFiles, {strictTemplates: false});
|
||||||
|
const definitions =
|
||||||
|
getTypeDefinitionsAndAssertBoundSpan({templateOverride: '{{"1/1/2020" | dat¦e}}'});
|
||||||
|
expect(definitions!.length).toEqual(1);
|
||||||
|
|
||||||
|
const [def] = definitions;
|
||||||
|
expect(def.textSpan).toContain('DatePipe');
|
||||||
|
expect(def.contextSpan).toContain('DatePipe');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getTypeDefinitionsAndAssertBoundSpan({templateOverride}: {templateOverride: string}):
|
||||||
|
HumanizedDefinitionInfo[] {
|
||||||
|
const {cursor, text} =
|
||||||
|
env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride);
|
||||||
|
env.expectNoSourceDiagnostics();
|
||||||
|
env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp');
|
||||||
|
const defs = env.ngLS.getTypeDefinitionAtPosition(absoluteFrom('/app.html'), cursor);
|
||||||
|
expect(defs).toBeTruthy();
|
||||||
|
const overrides = new Map<string, string>();
|
||||||
|
overrides.set(absoluteFrom('/app.html'), text);
|
||||||
|
return defs!.map(d => humanizeDefinitionInfo(d, env.host, overrides));
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue