feat(ivy): template type-checking for '#' references in templates (#29698)
Previously the template type-checking engine processed templates in a linear manner, and could not handle '#' references within a template. One reason for this is that '#' references are non-linear - a reference can be used before its declaration. Consider the template: ```html {{ref.value}} <input #ref> ``` Accommodating this required refactoring the type-checking code generator to be able to produce Type Check Block (TCB) code non-linearly. Now, each template is processed and a list of TCB operations (`TcbOp`s) are created. Non-linearity is modeled via dependencies between operations, with the appropriate protection in place for circular dependencies. Testing strategy: TCB tests included. PR Close #29698
This commit is contained in:
parent
9f5288dad3
commit
073d258deb
File diff suppressed because it is too large
Load Diff
@ -6,13 +6,13 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Expression, ExternalExpr, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
|
import {CssSelector, Expression, ExternalExpr, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports';
|
import {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports';
|
||||||
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
|
||||||
import {ImportManager} from '../../translator';
|
import {ImportManager} from '../../translator';
|
||||||
import {TypeCheckBlockMetadata} from '../src/api';
|
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from '../src/api';
|
||||||
import {generateTypeCheckBlock} from '../src/type_check_block';
|
import {generateTypeCheckBlock} from '../src/type_check_block';
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +39,67 @@ describe('type check blocks', () => {
|
|||||||
const TEMPLATE = `{{a[b]}}`;
|
const TEMPLATE = `{{a[b]}}`;
|
||||||
expect(tcb(TEMPLATE)).toContain('ctx.a[ctx.b];');
|
expect(tcb(TEMPLATE)).toContain('ctx.a[ctx.b];');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate a forward element reference correctly', () => {
|
||||||
|
const TEMPLATE = `
|
||||||
|
{{ i.value }}
|
||||||
|
<input #i>
|
||||||
|
`;
|
||||||
|
expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); _t1.value;');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a forward directive reference correctly', () => {
|
||||||
|
const TEMPLATE = `
|
||||||
|
{{d.value}}
|
||||||
|
<div dir #d="dir"></div>
|
||||||
|
`;
|
||||||
|
const DIRECTIVES: TestDirective[] = [{
|
||||||
|
name: 'Dir',
|
||||||
|
selector: '[dir]',
|
||||||
|
exportAs: ['dir'],
|
||||||
|
}];
|
||||||
|
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||||
|
.toContain(
|
||||||
|
'var _t1 = i0.Dir.ngTypeCtor({}); _t1.value; var _t2 = document.createElement("div");');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a circular directive reference correctly', () => {
|
||||||
|
const TEMPLATE = `
|
||||||
|
<div dir #d="dir" [input]="d"></div>
|
||||||
|
`;
|
||||||
|
const DIRECTIVES: TestDirective[] = [{
|
||||||
|
name: 'Dir',
|
||||||
|
selector: '[dir]',
|
||||||
|
exportAs: ['dir'],
|
||||||
|
inputs: {input: 'input'},
|
||||||
|
}];
|
||||||
|
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = i0.Dir.ngTypeCtor({ input: (null!) });');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate circular references between two directives correctly', () => {
|
||||||
|
const TEMPLATE = `
|
||||||
|
<div #a="dirA" dir-a [inputA]="b">A</div>
|
||||||
|
<div #b="dirB" dir-b [inputB]="a">B</div>
|
||||||
|
`;
|
||||||
|
const DIRECTIVES: TestDirective[] = [
|
||||||
|
{
|
||||||
|
name: 'DirA',
|
||||||
|
selector: '[dir-a]',
|
||||||
|
exportAs: ['dirA'],
|
||||||
|
inputs: {inputA: 'inputA'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DirB',
|
||||||
|
selector: '[dir-b]',
|
||||||
|
exportAs: ['dirB'],
|
||||||
|
inputs: {inputA: 'inputB'},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||||
|
.toContain(
|
||||||
|
'var _t3 = i0.DirB.ngTypeCtor({ inputA: (null!) });' +
|
||||||
|
' var _t2 = i1.DirA.ngTypeCtor({ inputA: _t3 });');
|
||||||
});
|
});
|
||||||
|
|
||||||
function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> {
|
function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> {
|
||||||
@ -50,13 +111,36 @@ function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDec
|
|||||||
throw new Error(`Class ${name} not found in file`);
|
throw new Error(`Class ${name} not found in file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
|
||||||
|
type TestDirective =
|
||||||
|
Partial<Pick<TypeCheckableDirectiveMeta, Exclude<keyof TypeCheckableDirectiveMeta, 'ref'>>>&
|
||||||
|
{selector: string, name: string};
|
||||||
|
|
||||||
function tcb(template: string): string {
|
function tcb(template: string, directives: TestDirective[] = []): string {
|
||||||
const sf = ts.createSourceFile('synthetic.ts', 'class Test {}', ts.ScriptTarget.Latest, true);
|
const classes = ['Test', ...directives.map(dir => dir.name)];
|
||||||
|
const code = classes.map(name => `class ${name} {}`).join('\n');
|
||||||
|
|
||||||
|
const sf = ts.createSourceFile('synthetic.ts', code, ts.ScriptTarget.Latest, true);
|
||||||
const clazz = getClass(sf, 'Test');
|
const clazz = getClass(sf, 'Test');
|
||||||
const {nodes} = parseTemplate(template, 'synthetic.html');
|
const {nodes} = parseTemplate(template, 'synthetic.html');
|
||||||
const matcher = new SelectorMatcher();
|
const matcher = new SelectorMatcher();
|
||||||
|
|
||||||
|
for (const dir of directives) {
|
||||||
|
const selector = CssSelector.parse(dir.selector);
|
||||||
|
const meta: TypeCheckableDirectiveMeta = {
|
||||||
|
name: dir.name,
|
||||||
|
ref: new Reference(getClass(sf, dir.name)),
|
||||||
|
exportAs: dir.exportAs || null,
|
||||||
|
hasNgTemplateContextGuard: dir.hasNgTemplateContextGuard || false,
|
||||||
|
inputs: dir.inputs || {},
|
||||||
|
isComponent: dir.isComponent || false,
|
||||||
|
ngTemplateGuards: dir.ngTemplateGuards || [],
|
||||||
|
outputs: dir.outputs || {},
|
||||||
|
queries: dir.queries || [],
|
||||||
|
};
|
||||||
|
matcher.addSelectables(selector, meta);
|
||||||
|
}
|
||||||
|
|
||||||
const binder = new R3TargetBinder(matcher);
|
const binder = new R3TargetBinder(matcher);
|
||||||
const boundTarget = binder.bind({template: nodes});
|
const boundTarget = binder.bind({template: nodes});
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ export * from './ml_parser/interpolation_config';
|
|||||||
export * from './ml_parser/tags';
|
export * from './ml_parser/tags';
|
||||||
export {LexerRange} from './ml_parser/lexer';
|
export {LexerRange} from './ml_parser/lexer';
|
||||||
export {NgModuleCompiler} from './ng_module_compiler';
|
export {NgModuleCompiler} from './ng_module_compiler';
|
||||||
export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast';
|
export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast';
|
||||||
export {EmitterVisitorContext} from './output/abstract_emitter';
|
export {EmitterVisitorContext} from './output/abstract_emitter';
|
||||||
export {JitEvaluator} from './output/output_jit';
|
export {JitEvaluator} from './output/output_jit';
|
||||||
export * from './output/ts_emitter';
|
export * from './output/ts_emitter';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user