Skip to content

Commit

Permalink
refactor(compiler-cli): Adjust generated TCB when checkTypeOfPipes is…
Browse files Browse the repository at this point in the history
… false

When `checkTypeOfPipes` is set to `false`, our TCB currently generates
the a statement like the following when pipes appear in the template:
`(_pipe1 as any).transform(args)`

This did enable us to get _some_ information from the Language Service
about pipes in this case because we still had access to the pipe
instance. However, because it is immediately cast to `any`, we cannot
get type information about the transform access. That means actions like "go to
definition", "find references", "quick info", etc. will return
incomplete information or fail altogether.

Instead, this commit changes the TCB to generate `(_pipe1.transform as any)(args)`.
This gives us the ability to get complete information for the LS
operations listed above.
  • Loading branch information
atscott committed Jan 26, 2021
1 parent ae9f7c0 commit 77cd197
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 79 deletions.
Expand Up @@ -430,19 +430,14 @@ 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)) {
const methodAccess = findFirstMatchingNode(
this.typeCheckBlock,
{withSpan: expression.nameSpan, filter: ts.isPropertyAccessExpression});
if (methodAccess === null) {
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 pipeVariableNode = methodAccess.expression;
const pipeDeclaration = this.getTypeChecker().getSymbolAtLocation(pipeVariableNode);
if (pipeDeclaration === undefined || pipeDeclaration.valueDeclaration === undefined) {
return null;
Expand Down
Expand Up @@ -1594,17 +1594,19 @@ class TcbExpressionTranslator {

// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = NULL_AS_ANY;
} else if (this.tcb.env.config.checkTypeOfPipes) {
} else {
// Use a variable declared as the pipe's type.
pipe = this.tcb.env.pipeInst(pipeRef);
} else {
// Use an 'any' value when not checking the type of the pipe.
pipe = ts.createAsExpression(
this.tcb.env.pipeInst(pipeRef), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}
const args = ast.args.map(arg => this.translate(arg));
const methodAccess = ts.createPropertyAccess(pipe, 'transform');
let methodAccess: ts.Expression =
ts.factory.createPropertyAccessExpression(pipe, 'transform');
addParseSpanInfo(methodAccess, ast.nameSpan);
if (!this.tcb.env.config.checkTypeOfPipes) {
methodAccess = ts.factory.createAsExpression(
methodAccess, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}

const result = ts.createCall(
/* expression */ methodAccess,
/* typeArguments */ undefined,
Expand Down
Expand Up @@ -974,7 +974,7 @@ describe('type check blocks', () => {
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfPipes: false};
const block = tcb(TEMPLATE, PIPES, DISABLED_CONFIG);
expect(block).toContain(
'((null as TestPipe) as any).transform(((ctx).a), ((ctx).b), ((ctx).c))');
'((null as TestPipe).transform as any)(((ctx).a), ((ctx).b), ((ctx).c))');
});
});

Expand Down
Expand Up @@ -749,29 +749,19 @@ runInEachFileSystem(() => {
BindingPipe;
}

it('should get symbol for pipe', () => {
setupPipesTest();
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
assertPipeSymbol(pipeSymbol);
expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!))
.toEqual('transform');
expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol))
.toEqual('TestPipe');
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!))
.toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]');
});

it('should get symbol for pipe, checkTypeOfPipes: false', () => {
setupPipesTest(false);
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)! as PipeSymbol;
assertPipeSymbol(pipeSymbol);
expect(pipeSymbol.tsSymbol).toBeNull();
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual('any');
expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol))
.toEqual('TestPipe');
expect(program.getTypeChecker().typeToString(pipeSymbol.classSymbol.tsType))
.toEqual('TestPipe');
});
for (const checkTypeOfPipes of [true, false]) {
it(`should get symbol for pipe, checkTypeOfPipes: ${checkTypeOfPipes}`, () => {
setupPipesTest(checkTypeOfPipes);
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
assertPipeSymbol(pipeSymbol);
expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!))
.toEqual('transform');
expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol))
.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', () => {
setupPipesTest(false);
Expand Down
Expand Up @@ -70,6 +70,7 @@ HostBindingComp.ɵcmp = i0.ɵɵngDeclareComponent({ version: "0.0.0-PLACEHOLDER"
}]
}], null, null); })();
class MyForwardPipe {
transform() { }
}
MyForwardPipe.ɵfac = function MyForwardPipe_Factory(t) { return new (t || MyForwardPipe)(); };
MyForwardPipe.ɵpipe = i0.ɵɵdefinePipe({ name: "my_forward_pipe", type: MyForwardPipe, pure: true });
Expand Down
Expand Up @@ -11,6 +11,7 @@ export class HostBindingComp {

@Pipe({name: 'my_forward_pipe'})
class MyForwardPipe {
transform() {}
}

@NgModule({declarations: [HostBindingComp, MyForwardPipe]})
Expand Down
20 changes: 15 additions & 5 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Expand Up @@ -671,7 +671,9 @@ runInEachFileSystem(os => {
// ModuleA classes
@Pipe({name: 'number'})
class PipeA {}
class PipeA {
transform() {}
}
@NgModule({
declarations: [PipeA],
Expand All @@ -682,7 +684,9 @@ runInEachFileSystem(os => {
// ModuleB classes
@Pipe({name: 'number'})
class PipeB {}
class PipeB {
transform() {}
}
@Component({
selector: 'app',
Expand Down Expand Up @@ -714,7 +718,9 @@ runInEachFileSystem(os => {
// ModuleA classes
@Pipe({name: 'number'})
class PipeA {}
class PipeA {
transform() {}
}
@NgModule({
declarations: [PipeA],
Expand All @@ -725,7 +731,9 @@ runInEachFileSystem(os => {
// ModuleB classes
@Pipe({name: 'number'})
class PipeB {}
class PipeB {
transform() {}
}
@NgModule({
declarations: [PipeB],
Expand Down Expand Up @@ -1550,7 +1558,9 @@ runInEachFileSystem(os => {
import {Component, NgModule, Pipe} from '@angular/core';
@Pipe({name: 'test'})
export class TestPipe {}
export class TestPipe {
transform() {}
}
@Component({selector: 'test-cmp', template: '{{value | test}}'})
export class TestCmp {
Expand Down
11 changes: 5 additions & 6 deletions packages/language-service/ivy/test/definitions_spec.ts
Expand Up @@ -10,7 +10,7 @@ import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';

import {extractCursorInfo, LanguageServiceTestEnvironment} from './env';
import {assertFileNames, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils';
import {assertFileNames, assertTextSpans, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils';

describe('definitions', () => {
it('gets definition for template reference in overridden template', () => {
Expand All @@ -35,7 +35,7 @@ describe('definitions', () => {
assertFileNames(Array.from(definitions!), ['app.html']);
});

it('returns the pipe class as definition when checkTypeOfPipes is false', () => {
it('returns the pipe definitions when checkTypeOfPipes is false', () => {
initMockFileSystem('Native');
const {cursor, text} = extractCursorInfo('{{"1/1/2020" | dat¦e}}');
const templateFile = {contents: text, name: absoluteFrom('/app.html')};
Expand All @@ -55,10 +55,9 @@ describe('definitions', () => {
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor);
expect(text.substr(textSpan.start, textSpan.length)).toEqual('date');

expect(definitions.length).toEqual(1);
const [def] = definitions;
expect(def.textSpan).toContain('DatePipe');
expect(def.contextSpan).toContain('DatePipe');
expect(definitions.length).toEqual(3);
assertTextSpans(definitions, ['transform']);
assertFileNames(definitions, ['index.d.ts']);
});

it('gets definitions for all inputs when attribute matches more than one', () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/language-service/ivy/test/quick_info_spec.ts
Expand Up @@ -513,8 +513,13 @@ describe('quick info', () => {
// 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'});
expectQuickInfo({
templateOverride,
expectedSpanText: 'date',
expectedDisplayString:
'(pipe) DatePipe.transform(value: string | number | Date, format?: string | undefined, timezone?: ' +
'string | undefined, locale?: string | undefined): string | null (+2 overloads)'
});
});
});

Expand Down
46 changes: 25 additions & 21 deletions packages/language-service/ivy/test/references_and_rename_spec.ts
Expand Up @@ -734,37 +734,41 @@ describe('find references and rename locations', () => {
prefixPipeFile = {name: _('/prefix-pipe.ts'), contents: cursorInfo.text};
});

describe('when cursor is on pipe name', () => {
let cursor: number;
beforeEach(() => {
const appContentsWithCursor = `
for (const checkTypeOfPipes of [true, false]) {
describe(`when cursor is on pipe name, checkTypeOfPipes: ${checkTypeOfPipes}`, () => {
let cursor: number;
beforeEach(() => {
const appContentsWithCursor = `
import {Component} from '@angular/core';
@Component({template: '{{birthday | prefi¦xPipe: "MM/dd/yy"}}'})
export class AppCmp {
birthday = '';
}
`;
const cursorInfo = extractCursorInfo(appContentsWithCursor);
cursor = cursorInfo.cursor;
const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
env = createModuleWithDeclarations([appFile, prefixPipeFile]);
});
const cursorInfo = extractCursorInfo(appContentsWithCursor);
cursor = cursorInfo.cursor;
const appFile = {name: _('/app.ts'), contents: cursorInfo.text};
// checkTypeOfPipes is set to false when strictTemplates is false
env =
createModuleWithDeclarations([appFile, prefixPipeFile], [], {strictTemplates: false});
});

it('should find references', () => {
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(5);
assertFileNames(refs, ['index.d.ts', 'prefix-pipe.ts', 'app.ts']);
assertTextSpans(refs, ['transform', 'prefixPipe']);
});
it('should find references', () => {
const refs = getReferencesAtPosition(_('/app.ts'), cursor)!;
expect(refs.length).toBe(5);
assertFileNames(refs, ['index.d.ts', 'prefix-pipe.ts', 'app.ts']);
assertTextSpans(refs, ['transform', 'prefixPipe']);
});

it('should find rename locations', () => {
const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
expect(renameLocations.length).toBe(2);
assertFileNames(renameLocations, ['prefix-pipe.ts', 'app.ts']);
assertTextSpans(renameLocations, ['prefixPipe']);
it('should find rename locations', () => {
const renameLocations = getRenameLocationsAtPosition(_('/app.ts'), cursor)!;
expect(renameLocations.length).toBe(2);
assertFileNames(renameLocations, ['prefix-pipe.ts', 'app.ts']);
assertTextSpans(renameLocations, ['prefixPipe']);
});
});
});
}

describe('when cursor is on pipe name expression', () => {
it('finds rename locations', () => {
Expand Down
9 changes: 4 additions & 5 deletions packages/language-service/ivy/test/type_definitions_spec.ts
Expand Up @@ -10,7 +10,7 @@ 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 {humanizeDocumentSpanLike} from './test_utils';
import {assertFileNames, assertTextSpans, humanizeDocumentSpanLike} from './test_utils';

describe('type definitions', () => {
let env: LanguageServiceTestEnvironment;
Expand Down Expand Up @@ -41,11 +41,10 @@ describe('type definitions', () => {
env = LanguageServiceTestEnvironment.setup(testFiles, {strictTemplates: false});
const definitions =
getTypeDefinitionsAndAssertBoundSpan({templateOverride: '{{"1/1/2020" | dat¦e}}'});
expect(definitions!.length).toEqual(1);
expect(definitions!.length).toEqual(3);

const [def] = definitions;
expect(def.textSpan).toContain('DatePipe');
expect(def.contextSpan).toContain('DatePipe');
assertTextSpans(definitions, ['transform']);
assertFileNames(definitions, ['index.d.ts']);
});

function getTypeDefinitionsAndAssertBoundSpan({templateOverride}: {templateOverride: string}) {
Expand Down

0 comments on commit 77cd197

Please sign in to comment.