refactor(compiler-cli): TemplateTypeChecker with checkTypeOfAttributes=false should still work (#39537)

When the compiler option `checkTypeOfAttributes` is `false`, we should
still be able to produce type information from the
`TemplateTypeChecker`. The current behavior ignores all attributes that
map to directive inputs. This commit includes those attribute bindings
in the TCB but adds the "ignore for diagnostics" marker so they do not
produce errors. This way, consumers of the TTC (the Language Service)
can still get valid information about these attributes even when the
user has configured the compiler to not produce diagnostics/errors for them.

PR Close #39537
This commit is contained in:
Andrew Scott 2020-10-30 16:27:22 -07:00 committed by Misko Hevery
parent 86fdc77ddf
commit a694838c41
3 changed files with 396 additions and 357 deletions

View File

@ -512,6 +512,11 @@ class TcbDirectiveCtorOp extends TcbOp {
const inputs = getBoundInputs(this.dir, this.node, this.tcb);
for (const input of inputs) {
// Skip text attributes if configured to do so.
if (!this.tcb.env.config.checkTypeOfAttributes &&
input.attribute instanceof TmplAstTextAttribute) {
continue;
}
for (const fieldName of input.fieldNames) {
// Skip the field if an attribute has already been bound to it; we can't have a duplicate
// key in the type constructor call.
@ -654,6 +659,12 @@ class TcbDirectiveInputsOp extends TcbOp {
}
addParseSpanInfo(assignment, input.attribute.sourceSpan);
// Ignore diagnostics for text attributes if configured to do so.
if (!this.tcb.env.config.checkTypeOfAttributes &&
input.attribute instanceof TmplAstTextAttribute) {
markIgnoreDiagnostics(assignment);
}
this.scope.addStatement(ts.createExpressionStatement(assignment));
}
@ -1732,11 +1743,6 @@ function getBoundInputs(
return;
}
// Skip text attributes if configured to do so.
if (!tcb.env.config.checkTypeOfAttributes && attr instanceof TmplAstTextAttribute) {
return;
}
// Skip the attribute if the directive does not have an input for it.
const inputs = directive.inputs.getByBindingPropertyName(attr.name);
if (inputs === null) {

View File

@ -9,7 +9,7 @@
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {ClassDeclaration} from '../../reflection';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api';
@ -66,20 +66,22 @@ runInEachFileSystem(() => {
expect(beforeSymbol).not.toBe(afterSymbol);
});
it('should get a symbol for text attributes corresponding with a directive input', () => {
const fileName = absoluteFrom('/main.ts');
describe('should get a symbol for text attributes corresponding with a directive input', () => {
let fileName: AbsoluteFsPath;
let targets: TypeCheckingTarget[];
beforeEach(() => {
fileName = absoluteFrom('/main.ts');
const dirFile = absoluteFrom('/dir.ts');
const templateString = `<div name="helloWorld"></div>`;
const {templateTypeChecker, program} = setup(
[
targets = [
{
fileName,
templates: {'Cmp': templateString},
templates: {'Cmp': templateString} as {[key: string]: string},
declarations: [{
name: 'NameDiv',
selector: 'div[name]',
file: dirFile,
type: 'directive',
type: 'directive' as const,
inputs: {name: 'name'},
}]
},
@ -88,12 +90,14 @@ runInEachFileSystem(() => {
source: `export class NameDiv {name!: string;}`,
templates: {},
}
],
);
];
});
it('checkTypeOfAttributes = true', () => {
const {templateTypeChecker, program} = setup(targets, {checkTypeOfAttributes: true});
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
const {attributes} = getAstElements(templateTypeChecker, cmp)[0];
const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!;
assertInputBindingSymbol(symbol);
expect(
@ -106,6 +110,19 @@ runInEachFileSystem(() => {
expect(mapping.span.toString()).toEqual('name');
});
it('checkTypeOfAttributes = false', () => {
const {templateTypeChecker, program} = setup(targets, {checkTypeOfAttributes: false});
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
const {attributes} = getAstElements(templateTypeChecker, cmp)[0];
const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!;
assertInputBindingSymbol(symbol);
expect(
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
.toEqual('name');
});
});
describe('templates', () => {
describe('ng-templates', () => {
let templateTypeChecker: TemplateTypeChecker;

View File

@ -104,6 +104,7 @@ function quickInfoSkeleton(): TestFile[] {
describe('quick info', () => {
let env: LanguageServiceTestEnvironment;
describe('strict templates (happy path)', () => {
beforeEach(() => {
initMockFileSystem('Native');
env = LanguageServiceTestEnvironment.setup(quickInfoSkeleton());
@ -168,7 +169,8 @@ describe('quick info', () => {
it('should work for directives with compound selectors, some of which are bindings', () => {
expectQuickInfo({
templateOverride: `<ng-template ngF¦or let-hero [ngForOf]="heroes">{{hero}}</ng-template>`,
templateOverride:
`<ng-template ngF¦or let-hero [ngForOf]="heroes">{{hero}}</ng-template>`,
expectedSpanText: 'ngFor',
expectedDisplayString: '(directive) NgForOf<Hero, Hero[]>'
});
@ -462,6 +464,20 @@ describe('quick info', () => {
expect(documentation).toBe('This is the title of the `AppCmp` Component.');
});
});
});
describe('non-strict compiler options', () => {
it('should find input binding on text attribute when strictAttributeTypes is false', () => {
initMockFileSystem('Native');
env =
LanguageServiceTestEnvironment.setup(quickInfoSkeleton(), {strictAttributeTypes: false});
expectQuickInfo({
templateOverride: `<test-comp tcN¦ame="title"></test-comp>`,
expectedSpanText: 'tcName',
expectedDisplayString: '(property) TestComponent.name: string'
});
});
});
function expectQuickInfo(
{templateOverride, expectedSpanText, expectedDisplayString}: