Content-Length: 825412 | pFad | https://github.com/angular/angular/commit/1971e57a457ff9fd4dc8a353b59b51364e08b443

89 feat(compiler-cli): support type checking of host bindings (#60267) · angular/angular@1971e57 · GitHub
Skip to content

Commit 1971e57

Browse files
crisbetopkozlowski-opensource
authored andcommitted
feat(compiler-cli): support type checking of host bindings (#60267)
Historically Angular's type checking only extended to templates, however host bindings can contain expressions as well which can have type checking issues of their own. These changes expand the type checking infrastructure to cover the `host` object literal, `@HostBinding` decorators and `@HostListener` with full language service support coming in future commits. Note that initially the new functionality is disabled by default and has to be enabled using the `typeCheckHostBindings` compiler flag. PR Close #60267
1 parent eb4c22e commit 1971e57

File tree

8 files changed

+1025
-69
lines changed

8 files changed

+1025
-69
lines changed

goldens/public-api/compiler-cli/compiler_options.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface MiscOptions {
6060
compileNonExportedClasses?: boolean;
6161
disableTypeScriptVersionCheck?: boolean;
6262
forbidOrphanComponents?: boolean;
63+
typeCheckHostBindings?: boolean;
6364
}
6465

6566
// @public

packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ParseSourceSpan,
1212
SchemaMetadata,
1313
TmplAstElement,
14+
TmplAstHostElement,
1415
} from '@angular/compiler';
1516
import ts from 'typescript';
1617

@@ -59,22 +60,39 @@ export interface DomSchemaChecker {
5960
/**
6061
* Check a property binding on an element and record any diagnostics about it.
6162
*
62-
* @param id the template ID, suitable for resolution with a `TcbSourceResolver`.
63+
* @param id the type check ID, suitable for resolution with a `TcbSourceResolver`.
6364
* @param element the element node in question.
6465
* @param name the name of the property being checked.
6566
* @param span the source span of the binding. This is redundant with `element.attributes` but is
6667
* passed separately to avoid having to look up the particular property name.
6768
* @param schemas any active schemas for the template, which might affect the validity of the
6869
* property.
6970
*/
70-
checkProperty(
71+
checkTemplateElementProperty(
7172
id: string,
7273
element: TmplAstElement,
7374
name: string,
7475
span: ParseSourceSpan,
7576
schemas: SchemaMetadata[],
7677
hostIsStandalone: boolean,
7778
): void;
79+
80+
/**
81+
* Check a property binding on a host element and record any diagnostics about it.
82+
* @param id the type check ID, suitable for resolution with a `TcbSourceResolver`.
83+
* @param element the element node in question.
84+
* @param name the name of the property being checked.
85+
* @param span the source span of the binding.
86+
* @param schemas any active schemas for the template, which might affect the validity of the
87+
* property.
88+
*/
89+
checkHostElementProperty(
90+
id: string,
91+
element: TmplAstHostElement,
92+
name: string,
93+
span: ParseSourceSpan,
94+
schemas: SchemaMetadata[],
95+
): void;
7896
}
7997

8098
/**
@@ -129,7 +147,7 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker {
129147
}
130148
}
131149

132-
checkProperty(
150+
checkTemplateElementProperty(
133151
id: TypeCheckId,
134152
element: TmplAstElement,
135153
name: string,
@@ -171,4 +189,31 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker {
171189
this._diagnostics.push(diag);
172190
}
173191
}
192+
193+
checkHostElementProperty(
194+
id: TypeCheckId,
195+
element: TmplAstHostElement,
196+
name: string,
197+
span: ParseSourceSpan,
198+
schemas: SchemaMetadata[],
199+
): void {
200+
for (const tagName of element.tagNames) {
201+
if (REGISTRY.hasProperty(tagName, name, schemas)) {
202+
continue;
203+
}
204+
205+
const errorMessage = `Can't bind to '${name}' since it isn't a known property of '${tagName}'.`;
206+
const mapping = this.resolver.getHostBindingsMapping(id);
207+
const diag = makeTemplateDiagnostic(
208+
id,
209+
mapping,
210+
span,
211+
ts.DiagnosticCategory.Error,
212+
ngErrorCode(ErrorCode.SCHEMA_INVALID_ATTRIBUTE),
213+
errorMessage,
214+
);
215+
this._diagnostics.push(diag);
216+
break;
217+
}
218+
}
174219
}

packages/compiler-cli/src/ngtsc/typecheck/src/host_bindings.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import ts from 'typescript';
3434
import {createSourceSpan} from '../../annotations/common';
3535
import {ClassDeclaration} from '../../reflection';
3636

37+
/**
38+
* Comment attached to an AST node that serves as a guard to distinguish nodes
39+
* used for type checking host bindings from ones used for templates.
40+
*/
41+
const GUARD_COMMENT_TEXT = 'hostBindingsBlockGuard';
42+
3743
/** Node that represent a static name of a member. */
3844
type StaticName = ts.Identifier | ts.StringLiteralLike;
3945

@@ -119,6 +125,50 @@ export function createHostElement(
119125
return new TmplAstHostElement(tagNames, bindings, listeners, createSourceSpan(sourceNode.name));
120126
}
121127

128+
/**
129+
* Creates an AST node that can be used as a guard in `if` statements to distinguish TypeScript
130+
* nodes used for checking host bindings from ones used for checking templates.
131+
*/
132+
export function createHostBindingsBlockGuard(): ts.Expression {
133+
// Note that the comment text is quite generic. This doesn't really matter, because it is
134+
// used only inside a TCB and there's no way for users to produce a comment there.
135+
// `true /*hostBindings*/`.
136+
const trueExpr = ts.addSyntheticTrailingComment(
137+
ts.factory.createTrue(),
138+
ts.SyntaxKind.MultiLineCommentTrivia,
139+
GUARD_COMMENT_TEXT,
140+
);
141+
// Wrap the expression in parentheses to ensure that the comment is attached to the correct node.
142+
return ts.factory.createParenthesizedExpression(trueExpr);
143+
}
144+
145+
/**
146+
* Determines if a given node is a guard that indicates that descendant nodes are used to check
147+
* host bindings.
148+
*/
149+
export function isHostBindingsBlockGuard(node: ts.Node): boolean {
150+
if (!ts.isIfStatement(node)) {
151+
return false;
152+
}
153+
154+
// Needs to be kept in sync with `createHostBindingsMarker`.
155+
const expr = node.expression;
156+
if (!ts.isParenthesizedExpression(expr) || expr.expression.kind !== ts.SyntaxKind.TrueKeyword) {
157+
return false;
158+
}
159+
160+
const text = expr.getSourceFile().text;
161+
return (
162+
ts.forEachTrailingCommentRange(
163+
text,
164+
expr.expression.getEnd(),
165+
(pos, end, kind) =>
166+
kind === ts.SyntaxKind.MultiLineCommentTrivia &&
167+
text.substring(pos + 2, end - 2) === GUARD_COMMENT_TEXT,
168+
) || false
169+
);
170+
}
171+
122172
/**
123173
* If possible, creates and tracks the relevant AST node for a binding declared
124174
* through a property on the `host` literal.

packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {FullSourceMapping, SourceLocation, TypeCheckId, SourceMapping} from '../
1717
import {hasIgnoreForDiagnosticsMarker, readSpanComment} from './comments';
1818
import {ReferenceEmitEnvironment} from './reference_emit_environment';
1919
import {TypeParameterEmitter} from './type_parameter_emitter';
20+
import {isHostBindingsBlockGuard} from './host_bindings';
2021

2122
/**
2223
* External modules/identifiers that always should exist for type check
@@ -129,14 +130,37 @@ export function getSourceMapping(
129130
return null;
130131
}
131132

132-
const mapping = resolver.getTemplateSourceMapping(sourceLocation.id);
133+
if (isInHostBindingTcb(node)) {
134+
const hostSourceMapping = resolver.getHostBindingsMapping(sourceLocation.id);
135+
const span = resolver.toHostParseSourceSpan(sourceLocation.id, sourceLocation.span);
136+
if (span === null) {
137+
return null;
138+
}
139+
return {sourceLocation, sourceMapping: hostSourceMapping, span};
140+
}
141+
133142
const span = resolver.toTemplateParseSourceSpan(sourceLocation.id, sourceLocation.span);
134143
if (span === null) {
135144
return null;
136145
}
137146
// TODO(atscott): Consider adding a context span by walking up from `node` until we get a
138147
// different span.
139-
return {sourceLocation, sourceMapping: mapping, span};
148+
return {
149+
sourceLocation,
150+
sourceMapping: resolver.getTemplateSourceMapping(sourceLocation.id),
151+
span,
152+
};
153+
}
154+
155+
function isInHostBindingTcb(node: ts.Node): boolean {
156+
let current = node;
157+
while (current && !ts.isFunctionDeclaration(current)) {
158+
if (isHostBindingsBlockGuard(current)) {
159+
return true;
160+
}
161+
current = current.parent;
162+
}
163+
return false;
140164
}
141165

142166
export function findTypeCheckBlock(
@@ -145,7 +169,7 @@ export function findTypeCheckBlock(
145169
isDiagnosticRequest: boolean,
146170
): ts.Node | null {
147171
for (const stmt of file.statements) {
148-
if (ts.isFunctionDeclaration(stmt) && getTemplateId(stmt, file, isDiagnosticRequest) === id) {
172+
if (ts.isFunctionDeclaration(stmt) && getTypeCheckId(stmt, file, isDiagnosticRequest) === id) {
149173
return stmt;
150174
}
151175
}
@@ -174,7 +198,7 @@ export function findSourceLocation(
174198
if (span !== null) {
175199
// Once the positional information has been extracted, search further up the TCB to extract
176200
// the unique id that is attached with the TCB's function declaration.
177-
const id = getTemplateId(node, sourceFile, isDiagnosticsRequest);
201+
const id = getTypeCheckId(node, sourceFile, isDiagnosticsRequest);
178202
if (id === null) {
179203
return null;
180204
}
@@ -187,7 +211,7 @@ export function findSourceLocation(
187211
return null;
188212
}
189213

190-
function getTemplateId(
214+
function getTypeCheckId(
191215
node: ts.Node,
192216
sourceFile: ts.SourceFile,
193217
isDiagnosticRequest: boolean,

packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,32 @@ export function tsCastToAny(expr: ts.Expression): ts.Expression {
6161
* Thanks to narrowing of `document.createElement()`, this expression will have its type inferred
6262
* based on the tag name, including for custom elements that have appropriate .d.ts definitions.
6363
*/
64-
export function tsCreateElement(tagName: string): ts.Expression {
64+
export function tsCreateElement(...tagNames: string[]): ts.Expression {
6565
const createElement = ts.factory.createPropertyAccessExpression(
6666
/* expression */ ts.factory.createIdentifier('document'),
6767
'createElement',
6868
);
69+
70+
let arg: ts.Expression;
71+
72+
if (tagNames.length === 1) {
73+
// If there's only one tag name, we can pass it in directly.
74+
arg = ts.factory.createStringLiteral(tagNames[0]);
75+
} else {
76+
// If there's more than one name, we have to generate a union of all the tag names. To do so,
77+
// create an expression in the form of `null! as 'tag-1' | 'tag-2' | 'tag-3'`. This allows
78+
// TypeScript to infer the type as a union of the differnet tags.
79+
const assertedNullExpression = ts.factory.createNonNullExpression(ts.factory.createNull());
80+
const type = ts.factory.createUnionTypeNode(
81+
tagNames.map((tag) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(tag))),
82+
);
83+
arg = ts.factory.createAsExpression(assertedNullExpression, type);
84+
}
85+
6986
return ts.factory.createCallExpression(
7087
/* expression */ createElement,
7188
/* typeArguments */ undefined,
72-
/* argumentsArray */ [ts.factory.createStringLiteral(tagName)],
89+
/* argumentsArray */ [arg],
7390
);
7491
}
7592

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/angular/commit/1971e57a457ff9fd4dc8a353b59b51364e08b443

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy