refactor(compiler-cli): Adjust output of TCB to support `TemplateTypeChecker` Symbol retrieval (#38618)
The statements generated in the TCB are optimized for performance and producing diagnostics. These optimizations can result in generating a TCB that does not have all the information needed by the `TemplateTypeChecker` for retrieving `Symbol`s. For example, as an optimization, the TCB will not generate variable declaration statements for directives that have no references, inputs, or outputs. However, the `TemplateTypeChecker` always needs these statements to be present in order to provide `ts.Symbol`s and `ts.Type`s for the directives. This commit adds logic to the TCB generation to ensure the required information is available in a form that the `TemplateTypeChecker` can consume. It also adds an option to the `NgCompiler` that makes this generation configurable. PR Close #38618
This commit is contained in:
parent
9e77bd3087
commit
a46e0e48a3
|
@ -99,11 +99,15 @@ export class NgCompiler {
|
|||
readonly ignoreForEmit: Set<ts.SourceFile>;
|
||||
|
||||
constructor(
|
||||
private adapter: NgCompilerAdapter, private options: NgCompilerOptions,
|
||||
private adapter: NgCompilerAdapter,
|
||||
private options: NgCompilerOptions,
|
||||
private tsProgram: ts.Program,
|
||||
private typeCheckingProgramStrategy: TypeCheckingProgramStrategy,
|
||||
private incrementalStrategy: IncrementalBuildStrategy, oldProgram: ts.Program|null = null,
|
||||
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER) {
|
||||
private incrementalStrategy: IncrementalBuildStrategy,
|
||||
private enableTemplateTypeChecker: boolean,
|
||||
oldProgram: ts.Program|null = null,
|
||||
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER,
|
||||
) {
|
||||
this.constructionDiagnostics.push(...this.adapter.constructionDiagnostics);
|
||||
const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(this.options);
|
||||
if (incompatibleTypeCheckOptionsDiagnostic !== null) {
|
||||
|
@ -212,6 +216,10 @@ export class NgCompiler {
|
|||
}
|
||||
|
||||
getTemplateTypeChecker(): TemplateTypeChecker {
|
||||
if (!this.enableTemplateTypeChecker) {
|
||||
throw new Error(
|
||||
'The `TemplateTypeChecker` does not work without `enableTemplateTypeChecker`.');
|
||||
}
|
||||
return this.ensureAnalyzed().templateTypeChecker;
|
||||
}
|
||||
|
||||
|
@ -436,6 +444,7 @@ export class NgCompiler {
|
|||
strictSafeNavigationTypes: strictTemplates,
|
||||
useContextGenericType: strictTemplates,
|
||||
strictLiteralTypes: true,
|
||||
enableTemplateTypeChecker: this.enableTemplateTypeChecker,
|
||||
};
|
||||
} else {
|
||||
typeCheckingConfig = {
|
||||
|
@ -456,6 +465,7 @@ export class NgCompiler {
|
|||
strictSafeNavigationTypes: false,
|
||||
useContextGenericType: false,
|
||||
strictLiteralTypes: false,
|
||||
enableTemplateTypeChecker: this.enableTemplateTypeChecker,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ runInEachFileSystem(() => {
|
|||
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
|
||||
const compiler = new NgCompiler(
|
||||
host, options, program, new ReusedProgramStrategy(program, host, options, []),
|
||||
new NoopIncrementalBuildStrategy());
|
||||
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false);
|
||||
|
||||
const diags = compiler.getDiagnostics(getSourceFileOrError(program, COMPONENT));
|
||||
expect(diags.length).toBe(1);
|
||||
|
|
|
@ -99,7 +99,7 @@ export class NgtscProgram implements api.Program {
|
|||
// Create the NgCompiler which will drive the rest of the compilation.
|
||||
this.compiler = new NgCompiler(
|
||||
this.host, options, this.tsProgram, reusedProgramStrategy, this.incrementalStrategy,
|
||||
reuseProgram, this.perfRecorder);
|
||||
/** enableTemplateTypeChecker */ false, reuseProgram, this.perfRecorder);
|
||||
}
|
||||
|
||||
getTsProgram(): ts.Program {
|
||||
|
|
|
@ -102,7 +102,8 @@ export class NgTscPlugin implements TscPlugin {
|
|||
program, this.host, this.options, this.host.shimExtensionPrefixes);
|
||||
this._compiler = new NgCompiler(
|
||||
this.host, this.options, program, typeCheckStrategy,
|
||||
new PatchedProgramIncrementalBuildStrategy(), oldProgram, NOOP_PERF_RECORDER);
|
||||
new PatchedProgramIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
|
||||
oldProgram, NOOP_PERF_RECORDER);
|
||||
return {
|
||||
ignoreForDiagnostics: this._compiler.ignoreForDiagnostics,
|
||||
ignoreForEmit: this._compiler.ignoreForEmit,
|
||||
|
|
|
@ -185,6 +185,22 @@ export interface TypeCheckingConfig {
|
|||
*/
|
||||
checkTypeOfNonDomReferences: boolean;
|
||||
|
||||
/**
|
||||
* Whether to adjust the output of the TCB to ensure compatibility with the `TemplateTypeChecker`.
|
||||
*
|
||||
* The statements generated in the TCB are optimized for performance and producing diagnostics.
|
||||
* These optimizations can result in generating a TCB that does not have all the information
|
||||
* needed by the `TemplateTypeChecker` for retrieving `Symbol`s. For example, as an optimization,
|
||||
* the TCB will not generate variable declaration statements for directives that have no
|
||||
* references, inputs, or outputs. However, the `TemplateTypeChecker` always needs these
|
||||
* statements to be present in order to provide `ts.Symbol`s and `ts.Type`s for the directives.
|
||||
*
|
||||
* When set to `false`, enables TCB optimizations for template diagnostics.
|
||||
* When set to `true`, ensures all information required by `TemplateTypeChecker` to
|
||||
* retrieve symbols for template nodes is available in the TCB.
|
||||
*/
|
||||
enableTemplateTypeChecker: boolean;
|
||||
|
||||
/**
|
||||
* Whether to include type information from pipes in the type-checking operation.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* @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 {AbsoluteSourceSpan} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
const parseSpanComment = /^(\d+),(\d+)$/;
|
||||
|
||||
/**
|
||||
* Reads the trailing comments and finds the first match which is a span comment (i.e. 4,10) on a
|
||||
* node and returns it as an `AbsoluteSourceSpan`.
|
||||
*
|
||||
* Will return `null` if no trailing comments on the node match the expected form of a source span.
|
||||
*/
|
||||
export function readSpanComment(sourceFile: ts.SourceFile, node: ts.Node): AbsoluteSourceSpan|null {
|
||||
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||
return null;
|
||||
}
|
||||
const commentText = sourceFile.text.substring(pos + 2, end - 2);
|
||||
const match = commentText.match(parseSpanComment);
|
||||
if (match === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AbsoluteSourceSpan(+match[1], +match[2]);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
/** Used to identify what type the comment is. */
|
||||
export enum CommentTriviaType {
|
||||
DIAGNOSTIC = 'D',
|
||||
EXPRESSION_TYPE_IDENTIFIER = 'T',
|
||||
}
|
||||
|
||||
/** Identifies what the TCB expression is for (for example, a directive declaration). */
|
||||
export enum ExpressionIdentifier {
|
||||
DIRECTIVE = 'DIR',
|
||||
}
|
||||
|
||||
/** Tags the node with the given expression identifier. */
|
||||
export function addExpressionIdentifier(node: ts.Node, identifier: ExpressionIdentifier) {
|
||||
ts.addSyntheticTrailingComment(
|
||||
node, ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
`${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`,
|
||||
/* hasTrailingNewLine */ false);
|
||||
}
|
||||
|
||||
export const IGNORE_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`;
|
||||
|
||||
/**
|
||||
* Tag the `ts.Node` with an indication that any errors arising from the evaluation of the node
|
||||
* should be ignored.
|
||||
*/
|
||||
export function markIgnoreDiagnostics(node: ts.Node): void {
|
||||
ts.addSyntheticTrailingComment(
|
||||
node, ts.SyntaxKind.MultiLineCommentTrivia, IGNORE_MARKER, /* hasTrailingNewLine */ false);
|
||||
}
|
||||
|
||||
/** Returns true if the node has a marker that indicates diagnostics errors should be ignored. */
|
||||
export function hasIgnoreMarker(node: ts.Node, sourceFile: ts.SourceFile): boolean {
|
||||
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||
return null;
|
||||
}
|
||||
const commentText = sourceFile.text.substring(pos + 2, end - 2);
|
||||
return commentText === IGNORE_MARKER;
|
||||
}) === true;
|
||||
}
|
|
@ -9,9 +9,12 @@ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
|
|||
import * as ts from 'typescript';
|
||||
|
||||
import {getTokenAtPosition} from '../../util/src/typescript';
|
||||
import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from '../api';
|
||||
import {TemplateId, TemplateSourceMapping} from '../api';
|
||||
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
|
||||
|
||||
import {hasIgnoreMarker, readSpanComment} from './comments';
|
||||
|
||||
|
||||
/**
|
||||
* Adapter interface which allows the template type-checking diagnostics code to interpret offsets
|
||||
* in a TCB and map them back to original locations in the template.
|
||||
|
@ -47,14 +50,14 @@ export function wrapForDiagnostics(expr: ts.Expression): ts.Expression {
|
|||
return ts.createParen(expr);
|
||||
}
|
||||
|
||||
const IGNORE_MARKER = 'ignore';
|
||||
|
||||
/**
|
||||
* Adds a marker to the node that signifies that any errors within the node should not be reported.
|
||||
* Wraps the node in parenthesis such that inserted span comments become attached to the proper
|
||||
* node. This is an alias for `ts.createParen` with the benefit that it signifies that the
|
||||
* inserted parenthesis are for use by the type checker, not for correctness of the rendered TCB
|
||||
* code.
|
||||
*/
|
||||
export function ignoreDiagnostics(node: ts.Node): void {
|
||||
ts.addSyntheticTrailingComment(
|
||||
node, ts.SyntaxKind.MultiLineCommentTrivia, IGNORE_MARKER, /* hasTrailingNewLine */ false);
|
||||
export function wrapForTypeChecker(expr: ts.Expression): ts.Expression {
|
||||
return ts.createParen(expr);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -200,30 +203,3 @@ function getTemplateId(node: ts.Node, sourceFile: ts.SourceFile): TemplateId|nul
|
|||
return commentText;
|
||||
}) as TemplateId || null;
|
||||
}
|
||||
|
||||
const parseSpanComment = /^(\d+),(\d+)$/;
|
||||
|
||||
function readSpanComment(sourceFile: ts.SourceFile, node: ts.Node): AbsoluteSourceSpan|null {
|
||||
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||
return null;
|
||||
}
|
||||
const commentText = sourceFile.text.substring(pos + 2, end - 2);
|
||||
const match = commentText.match(parseSpanComment);
|
||||
if (match === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AbsoluteSourceSpan(+match[1], +match[2]);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function hasIgnoreMarker(node: ts.Node, sourceFile: ts.SourceFile): boolean {
|
||||
return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
|
||||
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
||||
return null;
|
||||
}
|
||||
const commentText = sourceFile.text.substring(pos + 2, end - 2);
|
||||
return commentText === IGNORE_MARKER;
|
||||
}) === true;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional,
|
|||
import * as ts from 'typescript';
|
||||
import {TypeCheckingConfig} from '../api';
|
||||
|
||||
import {addParseSpanInfo, wrapForDiagnostics} from './diagnostics';
|
||||
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
|
||||
import {tsCastToAny} from './ts_util';
|
||||
|
||||
export const NULL_AS_ANY =
|
||||
|
@ -112,7 +112,14 @@ class AstTranslator implements AstVisitor {
|
|||
visitConditional(ast: Conditional): ts.Expression {
|
||||
const condExpr = this.translate(ast.condition);
|
||||
const trueExpr = this.translate(ast.trueExp);
|
||||
const falseExpr = this.translate(ast.falseExp);
|
||||
// Wrap `falseExpr` in parens so that the trailing parse span info is not attributed to the
|
||||
// whole conditional.
|
||||
// In the following example, the last source span comment (5,6) could be seen as the
|
||||
// trailing comment for _either_ the whole conditional expression _or_ just the `falseExpr` that
|
||||
// is immediately before it:
|
||||
// `conditional /*1,2*/ ? trueExpr /*3,4*/ : falseExpr /*5,6*/`
|
||||
// This should be instead be `conditional /*1,2*/ ? trueExpr /*3,4*/ : (falseExpr /*5,6*/)`
|
||||
const falseExpr = wrapForTypeChecker(this.translate(ast.falseExp));
|
||||
const node = ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
|
||||
addParseSpanInfo(node, ast.sourceSpan);
|
||||
return node;
|
||||
|
@ -135,7 +142,8 @@ class AstTranslator implements AstVisitor {
|
|||
// interpolation's expressions. The chain is started using an actual string literal to ensure
|
||||
// the type is inferred as 'string'.
|
||||
return ast.expressions.reduce(
|
||||
(lhs, ast) => ts.createBinary(lhs, ts.SyntaxKind.PlusToken, this.translate(ast)),
|
||||
(lhs, ast) =>
|
||||
ts.createBinary(lhs, ts.SyntaxKind.PlusToken, wrapForTypeChecker(this.translate(ast))),
|
||||
ts.createLiteral(''));
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ import {ClassPropertyName} from '../../metadata';
|
|||
import {ClassDeclaration} from '../../reflection';
|
||||
import {TemplateId, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata} from '../api';
|
||||
|
||||
import {addParseSpanInfo, addTemplateId, ignoreDiagnostics, wrapForDiagnostics} from './diagnostics';
|
||||
import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from './comments';
|
||||
import {addParseSpanInfo, addTemplateId, wrapForDiagnostics} from './diagnostics';
|
||||
import {DomSchemaChecker} from './dom';
|
||||
import {Environment} from './environment';
|
||||
import {astToTypescript, NULL_AS_ANY} from './expression';
|
||||
|
@ -22,8 +23,6 @@ import {OutOfBandDiagnosticRecorder} from './oob';
|
|||
import {ExpressionSemanticVisitor} from './template_semantics';
|
||||
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a
|
||||
* "type check block" function.
|
||||
|
@ -259,7 +258,7 @@ class TcbTemplateBodyOp extends TcbOp {
|
|||
|
||||
// The expression has already been checked in the type constructor invocation, so
|
||||
// it should be ignored when used within a template guard.
|
||||
ignoreDiagnostics(expr);
|
||||
markIgnoreDiagnostics(expr);
|
||||
|
||||
if (guard.type === 'binding') {
|
||||
// Use the binding expression itself as guard.
|
||||
|
@ -377,11 +376,80 @@ class TcbDirectiveTypeOp extends TcbOp {
|
|||
const id = this.tcb.allocateId();
|
||||
|
||||
const type = this.tcb.env.referenceType(this.dir.ref);
|
||||
addParseSpanInfo(type, this.node.startSourceSpan || this.node.sourceSpan);
|
||||
addExpressionIdentifier(type, ExpressionIdentifier.DIRECTIVE);
|
||||
this.scope.addStatement(tsDeclareVariable(id, type));
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which creates a variable for a local ref in a template.
|
||||
* The initializer for the variable is the variable expression for the directive, template, or
|
||||
* element the ref refers to. When the reference is used in the template, those TCB statements will
|
||||
* access this variable as well. For example:
|
||||
* ```
|
||||
* var _t1 = document.createElement('div');
|
||||
* var _t2 = _t1;
|
||||
* _t2.value
|
||||
* ```
|
||||
* This operation supports more fluent lookups for the `TemplateTypeChecker` when getting a symbol
|
||||
* for a reference. In most cases, this isn't essential; that is, the information for the symbol
|
||||
* could be gathered without this operation using the `BoundTarget`. However, for the case of
|
||||
* ng-template references, we will need this reference variable to not only provide a location in
|
||||
* the shim file, but also to narrow the variable to the correct `TemplateRef<T>` type rather than
|
||||
* `TemplateRef<any>` (this work is still TODO).
|
||||
*
|
||||
* Executing this operation returns a reference to the directive instance variable with its inferred
|
||||
* type.
|
||||
*/
|
||||
class TcbReferenceOp extends TcbOp {
|
||||
constructor(
|
||||
private readonly tcb: Context, private readonly scope: Scope,
|
||||
private readonly node: TmplAstReference,
|
||||
private readonly host: TmplAstElement|TmplAstTemplate,
|
||||
private readonly target: TypeCheckableDirectiveMeta|TmplAstTemplate|TmplAstElement) {
|
||||
super();
|
||||
}
|
||||
|
||||
// The statement generated by this operation is only used to for the Type Checker
|
||||
// so it can map a reference variable in the template directly to a node in the TCB.
|
||||
readonly optional = true;
|
||||
|
||||
execute(): ts.Identifier {
|
||||
const id = this.tcb.allocateId();
|
||||
let initializer = ts.getMutableClone(
|
||||
this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement ?
|
||||
this.scope.resolve(this.target) :
|
||||
this.scope.resolve(this.host, this.target));
|
||||
|
||||
// The reference is either to an element, an <ng-template> node, or to a directive on an
|
||||
// element or template.
|
||||
if ((this.target instanceof TmplAstElement && !this.tcb.env.config.checkTypeOfDomReferences) ||
|
||||
!this.tcb.env.config.checkTypeOfNonDomReferences) {
|
||||
// References to DOM nodes are pinned to 'any' when `checkTypeOfDomReferences` is `false`.
|
||||
// References to `TemplateRef`s and directives are pinned to 'any' when
|
||||
// `checkTypeOfNonDomReferences` is `false`.
|
||||
initializer =
|
||||
ts.createAsExpression(initializer, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
} else if (this.target instanceof TmplAstTemplate) {
|
||||
// Direct references to an <ng-template> node simply require a value of type
|
||||
// `TemplateRef<any>`. To get this, an expression of the form
|
||||
// `(_t1 as any as TemplateRef<any>)` is constructed.
|
||||
initializer =
|
||||
ts.createAsExpression(initializer, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
initializer = ts.createAsExpression(
|
||||
initializer,
|
||||
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
|
||||
initializer = ts.createParen(initializer);
|
||||
}
|
||||
addParseSpanInfo(initializer, this.node.sourceSpan);
|
||||
|
||||
this.scope.addStatement(tsCreateVariable(id, initializer));
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `TcbOp` which constructs an instance of a directive with types inferred from its inputs. The
|
||||
* inputs themselves are not checked here; checking of inputs is achieved in `TcbDirectiveInputsOp`.
|
||||
|
@ -441,7 +509,7 @@ class TcbDirectiveCtorOp extends TcbOp {
|
|||
// Call the type constructor of the directive to infer a type, and assign the directive
|
||||
// instance.
|
||||
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, Array.from(genericInputs.values()));
|
||||
ignoreDiagnostics(typeCtor);
|
||||
markIgnoreDiagnostics(typeCtor);
|
||||
this.scope.addStatement(tsCreateVariable(id, typeCtor));
|
||||
return id;
|
||||
}
|
||||
|
@ -731,7 +799,7 @@ class TcbUnclaimedInputsOp extends TcbOp {
|
|||
*
|
||||
* Executing this operation returns nothing.
|
||||
*/
|
||||
class TcbDirectiveOutputsOp extends TcbOp {
|
||||
export class TcbDirectiveOutputsOp extends TcbOp {
|
||||
constructor(
|
||||
private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
|
||||
private dir: TypeCheckableDirectiveMeta) {
|
||||
|
@ -789,6 +857,39 @@ class TcbDirectiveOutputsOp extends TcbOp {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs are a `ts.CallExpression` that look like one of the two:
|
||||
* - `_outputHelper(_t1["outputField"]).subscribe(handler);`
|
||||
* - `_t1.addEventListener(handler);`
|
||||
* This method reverses the operations to create a call expression for a directive output.
|
||||
* It unpacks the given call expression and returns the original element access (i.e.
|
||||
* `_t1["outputField"]` in the example above). Returns `null` if the given call expression is not
|
||||
* the expected structure of an output binding
|
||||
*/
|
||||
static decodeOutputCallExpression(node: ts.CallExpression): ts.ElementAccessExpression|null {
|
||||
// `node.expression` === `_outputHelper(_t1["outputField"]).subscribe` or `_t1.addEventListener`
|
||||
if (!ts.isPropertyAccessExpression(node.expression) ||
|
||||
node.expression.name.text === 'addEventListener') {
|
||||
// `addEventListener` outputs do not have an `ElementAccessExpression` for the output field.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ts.isCallExpression(node.expression.expression)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// `node.expression.expression` === `_outputHelper(_t1["outputField"])`
|
||||
if (node.expression.expression.arguments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [outputFieldAccess] = node.expression.expression.arguments;
|
||||
if (!ts.isElementAccessExpression(outputFieldAccess)) {
|
||||
return null;
|
||||
}
|
||||
return outputFieldAccess;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -943,6 +1044,11 @@ class Scope {
|
|||
private directiveOpMap =
|
||||
new Map<TmplAstElement|TmplAstTemplate, Map<TypeCheckableDirectiveMeta, number>>();
|
||||
|
||||
/**
|
||||
* A map of `TmplAstReference`s to the index of their `TcbReferenceOp` in the `opQueue`
|
||||
*/
|
||||
private referenceOpMap = new Map<TmplAstReference, number>();
|
||||
|
||||
/**
|
||||
* Map of immediately nested <ng-template>s (within this `Scope`) represented by `TmplAstTemplate`
|
||||
* nodes to the index of their `TcbTemplateContextOp`s in the `opQueue`.
|
||||
|
@ -1023,12 +1129,13 @@ class Scope {
|
|||
* * `TmplAstElement` - retrieve the expression for the element DOM node
|
||||
* * `TmplAstTemplate` - retrieve the template context variable
|
||||
* * `TmplAstVariable` - retrieve a template let- variable
|
||||
* * `TmplAstReference` - retrieve variable created for the local ref
|
||||
*
|
||||
* @param directive if present, a directive type on a `TmplAstElement` or `TmplAstTemplate` to
|
||||
* look up instead of the default for an element or template node.
|
||||
*/
|
||||
resolve(
|
||||
node: TmplAstElement|TmplAstTemplate|TmplAstVariable,
|
||||
node: TmplAstElement|TmplAstTemplate|TmplAstVariable|TmplAstReference,
|
||||
directive?: TypeCheckableDirectiveMeta): ts.Expression {
|
||||
// Attempt to resolve the operation locally.
|
||||
const res = this.resolveLocal(node, directive);
|
||||
|
@ -1054,7 +1161,10 @@ class Scope {
|
|||
*/
|
||||
render(): ts.Statement[] {
|
||||
for (let i = 0; i < this.opQueue.length; i++) {
|
||||
this.executeOp(i, /* skipOptional */ true);
|
||||
// Optional statements cannot be skipped when we are generating the TCB for use
|
||||
// by the TemplateTypeChecker.
|
||||
const skipOptional = !this.tcb.env.config.enableTemplateTypeChecker;
|
||||
this.executeOp(i, skipOptional);
|
||||
}
|
||||
return this.statements;
|
||||
}
|
||||
|
@ -1086,9 +1196,11 @@ class Scope {
|
|||
}
|
||||
|
||||
private resolveLocal(
|
||||
ref: TmplAstElement|TmplAstTemplate|TmplAstVariable,
|
||||
ref: TmplAstElement|TmplAstTemplate|TmplAstVariable|TmplAstReference,
|
||||
directive?: TypeCheckableDirectiveMeta): ts.Expression|null {
|
||||
if (ref instanceof TmplAstVariable && this.varMap.has(ref)) {
|
||||
if (ref instanceof TmplAstReference && this.referenceOpMap.has(ref)) {
|
||||
return this.resolveOp(this.referenceOpMap.get(ref)!);
|
||||
} else if (ref instanceof TmplAstVariable && this.varMap.has(ref)) {
|
||||
// Resolving a context variable for this template.
|
||||
// Execute the `TcbVariableOp` associated with the `TmplAstVariable`.
|
||||
return this.resolveOp(this.varMap.get(ref)!);
|
||||
|
@ -1163,27 +1275,38 @@ class Scope {
|
|||
for (const child of node.children) {
|
||||
this.appendNode(child);
|
||||
}
|
||||
this.checkReferencesOfNode(node);
|
||||
this.checkAndAppendReferencesOfNode(node);
|
||||
} else if (node instanceof TmplAstTemplate) {
|
||||
// Template children are rendered in a child scope.
|
||||
this.appendDirectivesAndInputsOfNode(node);
|
||||
this.appendOutputsOfNode(node);
|
||||
const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
|
||||
this.templateCtxOpMap.set(node, ctxIndex);
|
||||
if (this.tcb.env.config.checkTemplateBodies) {
|
||||
const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
|
||||
this.templateCtxOpMap.set(node, ctxIndex);
|
||||
this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node));
|
||||
}
|
||||
this.checkReferencesOfNode(node);
|
||||
this.checkAndAppendReferencesOfNode(node);
|
||||
} else if (node instanceof TmplAstBoundText) {
|
||||
this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node));
|
||||
}
|
||||
}
|
||||
|
||||
private checkReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void {
|
||||
private checkAndAppendReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void {
|
||||
for (const ref of node.references) {
|
||||
if (this.tcb.boundTarget.getReferenceTarget(ref) === null) {
|
||||
const target = this.tcb.boundTarget.getReferenceTarget(ref);
|
||||
if (target === null) {
|
||||
this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref);
|
||||
continue;
|
||||
}
|
||||
|
||||
let ctxIndex: number;
|
||||
if (target instanceof TmplAstTemplate || target instanceof TmplAstElement) {
|
||||
ctxIndex = this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target)) - 1;
|
||||
} else {
|
||||
ctxIndex =
|
||||
this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target.directive)) - 1;
|
||||
}
|
||||
this.referenceOpMap.set(ref, ctxIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1422,62 +1545,9 @@ class TcbExpressionTranslator {
|
|||
return null;
|
||||
}
|
||||
|
||||
// This expression has a binding to some variable or reference in the template. Resolve it.
|
||||
if (binding instanceof TmplAstVariable) {
|
||||
const expr = ts.getMutableClone(this.scope.resolve(binding));
|
||||
addParseSpanInfo(expr, ast.sourceSpan);
|
||||
return expr;
|
||||
} else if (binding instanceof TmplAstReference) {
|
||||
const target = this.tcb.boundTarget.getReferenceTarget(binding);
|
||||
if (target === null) {
|
||||
// This reference is unbound. Traversal of the `TmplAstReference` itself should have
|
||||
// recorded the error in the `OutOfBandDiagnosticRecorder`.
|
||||
// Still check the rest of the expression if possible by using an `any` value.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
// The reference is either to an element, an <ng-template> node, or to a directive on an
|
||||
// element or template.
|
||||
|
||||
if (target instanceof TmplAstElement) {
|
||||
if (!this.tcb.env.config.checkTypeOfDomReferences) {
|
||||
// References to DOM nodes are pinned to 'any'.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target));
|
||||
addParseSpanInfo(expr, ast.sourceSpan);
|
||||
return expr;
|
||||
} else if (target instanceof TmplAstTemplate) {
|
||||
if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
|
||||
// References to `TemplateRef`s pinned to 'any'.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
// Direct references to an <ng-template> node simply require a value of type
|
||||
// `TemplateRef<any>`. To get this, an expression of the form
|
||||
// `(null as any as TemplateRef<any>)` is constructed.
|
||||
let value: ts.Expression = ts.createNull();
|
||||
value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
value = ts.createAsExpression(
|
||||
value,
|
||||
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
|
||||
value = ts.createParen(value);
|
||||
addParseSpanInfo(value, ast.sourceSpan);
|
||||
return value;
|
||||
} else {
|
||||
if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
|
||||
// References to directives are pinned to 'any'.
|
||||
return NULL_AS_ANY;
|
||||
}
|
||||
|
||||
const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
|
||||
addParseSpanInfo(expr, ast.sourceSpan);
|
||||
return expr;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unreachable: ${binding}`);
|
||||
}
|
||||
const expr = ts.getMutableClone(this.scope.resolve(binding));
|
||||
addParseSpanInfo(expr, ast.sourceSpan);
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,12 @@ describe('type check blocks diagnostics', () => {
|
|||
it('should annotate conditions', () => {
|
||||
expect(tcbWithSpans('{{ a ? b : c }}'))
|
||||
.toContain(
|
||||
'(((ctx).a /*3,4*/) /*3,4*/ ? ((ctx).b /*7,8*/) /*7,8*/ : ((ctx).c /*11,12*/) /*11,12*/) /*3,12*/');
|
||||
'(((ctx).a /*3,4*/) /*3,4*/ ? ((ctx).b /*7,8*/) /*7,8*/ : (((ctx).c /*11,12*/) /*11,12*/)) /*3,12*/');
|
||||
});
|
||||
|
||||
it('should annotate interpolations', () => {
|
||||
expect(tcbWithSpans('{{ hello }} {{ world }}'))
|
||||
.toContain('"" + ((ctx).hello /*3,8*/) /*3,8*/ + ((ctx).world /*15,20*/) /*15,20*/');
|
||||
.toContain('"" + (((ctx).hello /*3,8*/) /*3,8*/) + (((ctx).world /*15,20*/) /*15,20*/)');
|
||||
});
|
||||
|
||||
it('should annotate literal map expressions', () => {
|
||||
|
@ -47,7 +47,7 @@ describe('type check blocks diagnostics', () => {
|
|||
|
||||
it('should annotate literals', () => {
|
||||
const TEMPLATE = '{{ 123 }}';
|
||||
expect(tcbWithSpans(TEMPLATE)).toContain('123 /*3,6*/;');
|
||||
expect(tcbWithSpans(TEMPLATE)).toContain('123 /*3,6*/');
|
||||
});
|
||||
|
||||
it('should annotate non-null assertions', () => {
|
||||
|
@ -57,7 +57,7 @@ describe('type check blocks diagnostics', () => {
|
|||
|
||||
it('should annotate prefix not', () => {
|
||||
const TEMPLATE = `{{ !a }}`;
|
||||
expect(tcbWithSpans(TEMPLATE)).toContain('!(((ctx).a /*4,5*/) /*4,5*/) /*3,5*/;');
|
||||
expect(tcbWithSpans(TEMPLATE)).toContain('!(((ctx).a /*4,5*/) /*4,5*/) /*3,5*/');
|
||||
});
|
||||
|
||||
it('should annotate method calls', () => {
|
||||
|
@ -141,7 +141,7 @@ describe('type check blocks diagnostics', () => {
|
|||
}];
|
||||
const block = tcbWithSpans(TEMPLATE, PIPES);
|
||||
expect(block).toContain(
|
||||
'(null as TestPipe).transform(((ctx).a /*3,4*/) /*3,4*/, ((ctx).b /*12,13*/) /*12,13*/) /*3,13*/;');
|
||||
'((null as TestPipe).transform(((ctx).a /*3,4*/) /*3,4*/, ((ctx).b /*12,13*/) /*12,13*/) /*3,13*/);');
|
||||
});
|
||||
|
||||
describe('attaching multiple comments for multiple references', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement, TmplAstReference, Type} from '@angular/compiler';
|
||||
import {CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement, Type} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system';
|
||||
|
@ -173,6 +173,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
|
|||
strictSafeNavigationTypes: true,
|
||||
useContextGenericType: true,
|
||||
strictLiteralTypes: true,
|
||||
enableTemplateTypeChecker: false,
|
||||
};
|
||||
|
||||
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
|
||||
|
@ -230,6 +231,7 @@ export function tcb(
|
|||
strictSafeNavigationTypes: true,
|
||||
useContextGenericType: true,
|
||||
strictLiteralTypes: true,
|
||||
enableTemplateTypeChecker: false,
|
||||
};
|
||||
options = options || {
|
||||
emitSpans: false,
|
||||
|
|
|
@ -13,38 +13,38 @@ import {ALL_ENABLED_CONFIG, tcb, TestDeclaration, TestDirective} from './test_ut
|
|||
|
||||
describe('type check blocks', () => {
|
||||
it('should generate a basic block for a binding', () => {
|
||||
expect(tcb('{{hello}} {{world}}')).toContain('"" + ((ctx).hello) + ((ctx).world);');
|
||||
expect(tcb('{{hello}} {{world}}')).toContain('"" + (((ctx).hello)) + (((ctx).world));');
|
||||
});
|
||||
|
||||
it('should generate literal map expressions', () => {
|
||||
const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
|
||||
expect(tcb(TEMPLATE)).toContain('(ctx).method({ "foo": ((ctx).a), "bar": ((ctx).b) });');
|
||||
expect(tcb(TEMPLATE)).toContain('(ctx).method({ "foo": ((ctx).a), "bar": ((ctx).b) })');
|
||||
});
|
||||
|
||||
it('should generate literal array expressions', () => {
|
||||
const TEMPLATE = '{{ method([a, b]) }}';
|
||||
expect(tcb(TEMPLATE)).toContain('(ctx).method([((ctx).a), ((ctx).b)]);');
|
||||
expect(tcb(TEMPLATE)).toContain('(ctx).method([((ctx).a), ((ctx).b)])');
|
||||
});
|
||||
|
||||
it('should handle non-null assertions', () => {
|
||||
const TEMPLATE = `{{a!}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!);');
|
||||
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!)');
|
||||
});
|
||||
|
||||
it('should handle unary - operator', () => {
|
||||
const TEMPLATE = `{{-1}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(-1);');
|
||||
expect(tcb(TEMPLATE)).toContain('(-1)');
|
||||
});
|
||||
|
||||
it('should handle keyed property access', () => {
|
||||
const TEMPLATE = `{{a[b]}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)];');
|
||||
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)]');
|
||||
});
|
||||
|
||||
it('should handle nested ternary expressions', () => {
|
||||
const TEMPLATE = `{{a ? b : c ? d : e}}`;
|
||||
expect(tcb(TEMPLATE))
|
||||
.toContain('(((ctx).a) ? ((ctx).b) : (((ctx).c) ? ((ctx).d) : ((ctx).e)))');
|
||||
.toContain('(((ctx).a) ? ((ctx).b) : ((((ctx).c) ? ((ctx).d) : (((ctx).e)))))');
|
||||
});
|
||||
|
||||
it('should handle quote expressions as any type', () => {
|
||||
|
@ -106,7 +106,7 @@ describe('type check blocks', () => {
|
|||
it('should handle method calls of template variables', () => {
|
||||
const TEMPLATE = `<ng-template let-a>{{a(1)}}</ng-template>`;
|
||||
expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;');
|
||||
expect(tcb(TEMPLATE)).toContain('(_t2).a(1);');
|
||||
expect(tcb(TEMPLATE)).toContain('(_t2).a(1)');
|
||||
});
|
||||
|
||||
it('should handle implicit vars when using microsyntax', () => {
|
||||
|
@ -179,8 +179,9 @@ describe('type check blocks', () => {
|
|||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t2 = Dir.ngTypeCtor((null!)); ' +
|
||||
'var _t1 = Dir.ngTypeCtor({ "input": (_t2) });');
|
||||
'var _t2 = Dir.ngTypeCtor({ "input": (null!) }); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'_t2.input = (_t1);');
|
||||
});
|
||||
|
||||
it('should generate circular references between two directives correctly', () => {
|
||||
|
@ -208,9 +209,12 @@ describe('type check blocks', () => {
|
|||
];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t3 = DirB.ngTypeCtor((null!)); ' +
|
||||
'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) }); ' +
|
||||
'var _t1 = DirB.ngTypeCtor({ "inputB": (_t2) });');
|
||||
'var _t4 = DirA.ngTypeCtor({ "inputA": (null!) }); ' +
|
||||
'var _t3 = _t4; ' +
|
||||
'var _t2 = DirB.ngTypeCtor({ "inputB": (_t3) }); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'_t4.inputA = (_t1); ' +
|
||||
'_t2.inputB = (_t3);');
|
||||
});
|
||||
|
||||
it('should handle empty bindings', () => {
|
||||
|
@ -263,8 +267,10 @@ describe('type check blocks', () => {
|
|||
`;
|
||||
const block = tcb(TEMPLATE);
|
||||
expect(block).not.toContain('"div"');
|
||||
expect(block).toContain('var _t1 = document.createElement("button");');
|
||||
expect(block).toContain('(ctx).handle(_t1);');
|
||||
expect(block).toContain(
|
||||
'var _t2 = document.createElement("button"); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'_t2.addEventListener');
|
||||
});
|
||||
|
||||
it('should only generate directive declarations that have bindings or are referenced', () => {
|
||||
|
@ -313,7 +319,8 @@ describe('type check blocks', () => {
|
|||
expect(block).toContain('_t1.input = (((ctx).value));');
|
||||
expect(block).toContain('var _t2: HasOutput = (null!)');
|
||||
expect(block).toContain('_t2["output"]');
|
||||
expect(block).toContain('var _t3: HasReference = (null!)');
|
||||
expect(block).toContain('var _t4: HasReference = (null!)');
|
||||
expect(block).toContain('var _t3 = _t4;');
|
||||
expect(block).toContain('(_t3).a');
|
||||
expect(block).not.toContain('NoBindings');
|
||||
expect(block).not.toContain('NoReference');
|
||||
|
@ -325,7 +332,8 @@ describe('type check blocks', () => {
|
|||
<input #i>
|
||||
`;
|
||||
expect(tcb(TEMPLATE))
|
||||
.toContain('var _t1 = document.createElement("input"); "" + ((_t1).value);');
|
||||
.toContain(
|
||||
'var _t2 = document.createElement("input"); var _t1 = _t2; "" + (((_t1).value));');
|
||||
});
|
||||
|
||||
it('should generate a forward directive reference correctly', () => {
|
||||
|
@ -339,7 +347,11 @@ describe('type check blocks', () => {
|
|||
selector: '[dir]',
|
||||
exportAs: ['dir'],
|
||||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t1: Dir = (null!); "" + ((_t1).value);');
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t2: Dir = (null!); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'"" + (((_t1).value));');
|
||||
});
|
||||
|
||||
it('should handle style and class bindings specially', () => {
|
||||
|
@ -385,8 +397,9 @@ describe('type check blocks', () => {
|
|||
}];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: Dir = (null!); ' +
|
||||
'_t1.input = (_t1);');
|
||||
'var _t2: Dir = (null!); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'_t2.input = (_t1);');
|
||||
});
|
||||
|
||||
it('should generate circular references between two directives correctly', () => {
|
||||
|
@ -412,10 +425,12 @@ describe('type check blocks', () => {
|
|||
];
|
||||
expect(tcb(TEMPLATE, DIRECTIVES))
|
||||
.toContain(
|
||||
'var _t1: DirB = (null!); ' +
|
||||
'var _t2: DirA = (null!); ' +
|
||||
'_t2.inputA = (_t1); ' +
|
||||
'_t1.inputA = (_t2);');
|
||||
'var _t2: DirB = (null!); ' +
|
||||
'var _t1 = _t2; ' +
|
||||
'var _t3: DirA = (null!); ' +
|
||||
'_t3.inputA = (_t1); ' +
|
||||
'var _t4 = _t3; ' +
|
||||
'_t2.inputA = (_t4);');
|
||||
});
|
||||
|
||||
it('should handle undeclared properties', () => {
|
||||
|
@ -566,7 +581,7 @@ describe('type check blocks', () => {
|
|||
it('should handle $any casts', () => {
|
||||
const TEMPLATE = `{{$any(a)}}`;
|
||||
const block = tcb(TEMPLATE);
|
||||
expect(block).toContain('(((ctx).a) as any);');
|
||||
expect(block).toContain('(((ctx).a) as any)');
|
||||
});
|
||||
|
||||
describe('experimental DOM checking via lib.dom.d.ts', () => {
|
||||
|
@ -699,6 +714,7 @@ describe('type check blocks', () => {
|
|||
strictSafeNavigationTypes: true,
|
||||
useContextGenericType: true,
|
||||
strictLiteralTypes: true,
|
||||
enableTemplateTypeChecker: false,
|
||||
};
|
||||
|
||||
describe('config.applyTemplateContextGuards', () => {
|
||||
|
@ -718,16 +734,27 @@ describe('type check blocks', () => {
|
|||
});
|
||||
|
||||
describe('config.checkTemplateBodies', () => {
|
||||
const TEMPLATE = `<ng-template>{{a}}</ng-template>`;
|
||||
const TEMPLATE = `<ng-template #ref>{{a}}</ng-template>{{ref}}`;
|
||||
|
||||
it('should descend into template bodies when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('((ctx).a);');
|
||||
expect(block).toContain('((ctx).a)');
|
||||
});
|
||||
it('should not descend into template bodies when disabled', () => {
|
||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTemplateBodies: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).not.toContain('((ctx).a);');
|
||||
expect(block).not.toContain('((ctx).a)');
|
||||
});
|
||||
|
||||
it('generates a references var when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('var _t2 = (_t1 as any as core.TemplateRef<any>);');
|
||||
});
|
||||
|
||||
it('generates a reference var when disabled', () => {
|
||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTemplateBodies: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain('var _t2 = (_t1 as any as core.TemplateRef<any>);');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -844,7 +871,9 @@ describe('type check blocks', () => {
|
|||
const DISABLED_CONFIG:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomReferences: false};
|
||||
const block = tcb(TEMPLATE, [], DISABLED_CONFIG);
|
||||
expect(block).toContain('(null as any).value');
|
||||
expect(block).toContain(
|
||||
'var _t1 = (_t2 as any); ' +
|
||||
'"" + (((_t1).value));');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -868,14 +897,18 @@ describe('type check blocks', () => {
|
|||
|
||||
it('should trace references to an <ng-template> when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain('((null as any as core.TemplateRef<any>)).value2');
|
||||
expect(block).toContain(
|
||||
'var _t4 = (_t3 as any as core.TemplateRef<any>); ' +
|
||||
'"" + (((_t4).value2));');
|
||||
});
|
||||
|
||||
it('should use any for reference types when disabled', () => {
|
||||
const DISABLED_CONFIG:
|
||||
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfNonDomReferences: false};
|
||||
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
|
||||
expect(block).toContain('(null as any).value');
|
||||
expect(block).toContain(
|
||||
'var _t1 = (_t2 as any); ' +
|
||||
'"" + (((_t1).value));');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -914,12 +947,12 @@ describe('type check blocks', () => {
|
|||
|
||||
it('should check types of pipes when enabled', () => {
|
||||
const block = tcb(TEMPLATE, PIPES);
|
||||
expect(block).toContain('(null as TestPipe).transform(((ctx).a), ((ctx).b), ((ctx).c));');
|
||||
expect(block).toContain('(null as TestPipe).transform(((ctx).a), ((ctx).b), ((ctx).c))');
|
||||
});
|
||||
it('should not check types of pipes when disabled', () => {
|
||||
const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfPipes: false};
|
||||
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 any).transform(((ctx).a), ((ctx).b), ((ctx).c))');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,9 @@ export class LanguageService {
|
|||
program,
|
||||
this.strategy,
|
||||
new PatchedProgramIncrementalBuildStrategy(),
|
||||
/** enableTemplateTypeChecker */ true,
|
||||
this.lastKnownProgram,
|
||||
/** perfRecorder (use default) */ undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue