refactor(compiler-cli): decouple host element creation logic from TypeScript

Reworks the logic for constructing a `HostElement` node so that we can reuse it without depending on TypeScript APIs.
This commit is contained in:
Kristiyan Kostadinov
2026-03-23 06:56:14 +01:00
committed by Leon Senft
parent 6d2eed61a1
commit 63ac1bd2fd
6 changed files with 302 additions and 209 deletions
@@ -28,3 +28,11 @@ export type {ReferenceEmitter} from '../src/ngtsc/imports';
export type {ReflectionHost, ClassDeclaration} from '../src/ngtsc/reflection';
export {ClassPropertyMapping} from '../src/ngtsc/metadata/src/property_mapping';
export type {TypeCheckSourceResolver} from '../src/ngtsc/typecheck/src/tcb_util';
export {
createHostElement,
type SourceNode,
type StaticSourceNode,
type HostObjectLiteralBinding,
type HostListenerDecorator,
type HostBindingDecorator,
} from '../src/ngtsc/typecheck/src/host_bindings';
@@ -135,6 +135,7 @@ import {
compileNgFactoryDefField,
compileResults,
createForwardRefResolver,
createSourceSpan,
extractClassDebugInfo,
extractClassMetadata,
extractSchemas,
@@ -231,10 +232,12 @@ const isUsedPipe = (decl: AnyUsedType): decl is UsedPipe =>
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler
implements
DecoratorHandler<Decorator, ComponentAnalysisData, ComponentSymbol, ComponentResolutionData>
{
export class ComponentDecoratorHandler implements DecoratorHandler<
Decorator,
ComponentAnalysisData,
ComponentSymbol,
ComponentResolutionData
> {
constructor(
private reflector: ReflectionHost,
private evaluator: PartialEvaluator,
@@ -1167,10 +1170,10 @@ export class ComponentDecoratorHandler
? createHostElement(
'component',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
createSourceSpan(node.name),
meta.hostBindingNodes.hostObjectLiteralBindings,
meta.hostBindingNodes.hostBindingDecorators,
meta.hostBindingNodes.hostListenerDecorators,
)
: null;
const hostBindingsContext: HostBindingsContext | null =
@@ -63,6 +63,7 @@ import {
compileInputTransformFields,
compileNgFactoryDefField,
compileResults,
createSourceSpan,
extractClassMetadata,
findAngularDecorator,
getDirectiveDiagnostics,
@@ -132,9 +133,12 @@ export interface DirectiveHandlerData {
resources: DirectiveResources;
}
export class DirectiveDecoratorHandler
implements DecoratorHandler<Decorator | null, DirectiveHandlerData, DirectiveSymbol, unknown>
{
export class DirectiveDecoratorHandler implements DecoratorHandler<
Decorator | null,
DirectiveHandlerData,
DirectiveSymbol,
unknown
> {
constructor(
private reflector: ReflectionHost,
private evaluator: PartialEvaluator,
@@ -360,10 +364,10 @@ export class DirectiveDecoratorHandler
const hostElement = createHostElement(
'directive',
meta.meta.selector,
node,
meta.hostBindingNodes.literal,
meta.hostBindingNodes.bindingDecorators,
meta.hostBindingNodes.listenerDecorators,
createSourceSpan(node.name),
meta.hostBindingNodes.hostObjectLiteralBindings,
meta.hostBindingNodes.hostBindingDecorators,
meta.hostBindingNodes.hostListenerDecorators,
);
if (hostElement !== null && scope.directivesOnHost !== null) {
@@ -56,7 +56,6 @@ import {
ForeignFunctionResolver,
PartialEvaluator,
ResolvedValue,
traceDynamicValue,
} from '../../../partial_evaluator';
import {
AmbientImport,
@@ -93,6 +92,13 @@ import {tryParseSignalInputMapping} from './input_function';
import {tryParseSignalModelMapping} from './model_function';
import {tryParseInitializerBasedOutput} from './output_function';
import {tryParseSignalQueryFromInitializer} from './query_functions';
import {
HostObjectLiteralBinding,
HostListenerDecorator,
HostBindingDecorator,
SourceNode,
StaticSourceNode,
} from '../../../typecheck/src/host_bindings';
const EMPTY_OBJECT: {[key: string]: string} = {};
@@ -105,9 +111,10 @@ export const queryDecoratorNames: QueryDecoratorName[] = [
];
export interface HostBindingNodes {
literal: ts.ObjectLiteralExpression | null;
bindingDecorators: Set<ts.Decorator>;
listenerDecorators: Set<ts.Decorator>;
hostObjectLiteralBindings: HostObjectLiteralBinding[];
hostBindingDecorators: HostBindingDecorator[];
hostListenerDecorators: HostListenerDecorator[];
rawNodes: ts.Node[];
}
const QUERY_TYPES = new Set<string>(queryDecoratorNames);
@@ -290,9 +297,10 @@ export function extractDirectiveMetadata(
}
const hostBindingNodes: HostBindingNodes = {
literal: null,
bindingDecorators: new Set<ts.Decorator>(),
listenerDecorators: new Set<ts.Decorator>(),
hostObjectLiteralBindings: [],
hostBindingDecorators: [],
hostListenerDecorators: [],
rawNodes: [],
};
const host = extractHostBindings(
@@ -600,7 +608,17 @@ function extractHostBindings(
const hostExpression = metadata.get('host')!;
bindings = evaluateHostExpressionBindings(hostExpression, evaluator);
if (ts.isObjectLiteralExpression(hostExpression)) {
hostBindingNodes.literal = hostExpression;
hostBindingNodes.rawNodes.push(hostExpression);
for (const prop of hostExpression.properties) {
if (ts.isPropertyAssignment(prop)) {
hostBindingNodes.hostObjectLiteralBindings.push({
key: sourceNodeFromTs(prop.name),
value: sourceNodeFromTs(prop.initializer),
sourceSpan: createSourceSpan(prop),
});
}
}
}
} else {
bindings = parseHostBindings({});
@@ -645,7 +663,23 @@ function extractHostBindings(
}
if (ts.isDecorator(decorator.node)) {
hostBindingNodes.bindingDecorators.add(decorator.node);
const member = decorator.node.parent;
hostBindingNodes.rawNodes.push(decorator.node.expression);
// TODO(crisbeto): this doesn't cover getters which is likely hiding some errors.
if (member && ts.isPropertyDeclaration(member) && member.name) {
const memberName = sourceNodeFromTs(member.name);
if (memberName.kind === 'string' || memberName.kind === 'identifier') {
hostBindingNodes.hostBindingDecorators.push({
memberName,
memberSpan: createSourceSpan(member),
arguments: decorator.args === null ? [] : decorator.args.map(sourceNodeFromTs),
decoratorSpan: createSourceSpan(decorator.node),
});
}
}
}
// Since this is a decorator, we know that the value is a class member. Always access it
@@ -710,7 +744,33 @@ function extractHostBindings(
}
if (ts.isDecorator(decorator.node)) {
hostBindingNodes.listenerDecorators.add(decorator.node);
const member = decorator.node.parent;
hostBindingNodes.rawNodes.push(decorator.node.expression);
if (member && ts.isMethodDeclaration(member) && member.name) {
const memberName = sourceNodeFromTs(member.name);
if (memberName.kind === 'string' || memberName.kind === 'identifier') {
let eventName: SourceNode | null = null;
let args: SourceNode[] | undefined;
if (decorator.args !== null && decorator.args.length > 0) {
eventName = sourceNodeFromTs(decorator.args[0]);
args =
decorator.args.length > 1 && ts.isArrayLiteralExpression(decorator.args[1])
? decorator.args[1].elements.map(sourceNodeFromTs)
: [];
}
hostBindingNodes.hostListenerDecorators.push({
eventName,
memberName,
memberSpan: createSourceSpan(member),
arguments: args ?? [],
decoratorSpan: createSourceSpan(decorator.node),
});
}
}
}
bindings.listeners[eventName] = `${member.name}(${args.join(',')})`;
@@ -720,6 +780,28 @@ function extractHostBindings(
return bindings;
}
function sourceNodeFromTs(node: ts.Node): SourceNode {
const sourceSpan = createSourceSpan(node);
if (ts.isStringLiteralLike(node) || ts.isIdentifier(node)) {
// Offset by one on both sides to skip over the quotes.
if (ts.isStringLiteralLike(node)) {
sourceSpan.fullStart = sourceSpan.fullStart.moveBy(1);
sourceSpan.start = sourceSpan.start.moveBy(1);
sourceSpan.end = sourceSpan.end.moveBy(-1);
}
return {
kind: ts.isIdentifier(node) ? 'identifier' : 'string',
sourceSpan,
source: node.getText(),
text: node.text,
};
}
return {kind: 'unspecified', sourceSpan};
}
function extractQueriesFromDecorator(
queryData: ts.Expression,
reflector: ReflectionHost,
@@ -2143,16 +2225,8 @@ function toR3InputMetadata(mapping: InputMapping): R3InputMetadata {
export function extractHostBindingResources(nodes: HostBindingNodes): ReadonlySet<Resource> {
const result = new Set<Resource>();
if (nodes.literal !== null) {
result.add({path: null, node: nodes.literal});
}
for (const current of nodes.bindingDecorators) {
result.add({path: null, node: current.expression});
}
for (const current of nodes.listenerDecorators) {
result.add({path: null, node: current.expression});
for (const node of nodes.rawNodes) {
result.add({path: null, node});
}
return result;
@@ -31,8 +31,6 @@ import {
ImplicitReceiver,
} from '@angular/compiler';
import ts from 'typescript';
import {createSourceSpan} from '../../annotations/common';
import {ClassDeclaration} from '../../reflection';
/**
* Comment attached to an AST node that serves as a guard to distinguish nodes
@@ -40,61 +38,89 @@ import {ClassDeclaration} from '../../reflection';
*/
const GUARD_COMMENT_TEXT = 'hostBindingsBlockGuard';
/** Node that represent a static name of a member. */
type StaticName = ts.Identifier | ts.StringLiteralLike;
/** Represents information extracted from the source AST. */
export type SourceNode =
| StaticSourceNode
| {
kind: 'unspecified';
sourceSpan: ParseSourceSpan;
};
/** Property assignment node with a static name and initializer. */
type StaticPropertyAssignment = ts.PropertyAssignment & {
name: StaticName;
initializer: ts.StringLiteralLike;
};
/** A `SourceNode` which represents a static expression. */
export interface StaticSourceNode {
kind: 'string' | 'identifier';
/** Raw source code of the node (e.g. strings include the quotes). */
source: string;
/** Actual text of the node (e.g. value inside the quotes in strings). */
text: string;
/** Location information about the node. */
sourceSpan: ParseSourceSpan;
}
/** A single binding inside the `host` object of a directive. */
export interface HostObjectLiteralBinding {
/** Node representing the key of the binding. */
key: SourceNode;
/** Node representing the value of the binding. */
value: SourceNode;
/** Location information about the entire binding. */
sourceSpan: ParseSourceSpan;
}
/** A single binding declared by a `@HostListener` decorator on a class member. */
export interface HostListenerDecorator {
/** Node declaring the name of the event (e.g. first argument of `@HostListener`). */
eventName: SourceNode | null;
/** Node representing the name of the member that was decorated. */
memberName: StaticSourceNode;
/** Location information about the member that the decorator is set on. */
memberSpan: ParseSourceSpan;
/** Arguments passed to the event. */
arguments: SourceNode[];
/** Location information about the decorator. */
decoratorSpan: ParseSourceSpan;
}
/** A single binding declared by the `@HostBinding` decorator on a class member. */
export interface HostBindingDecorator {
/** Node representing the name of the member that was decorated. */
memberName: StaticSourceNode;
/** Location information about the member that the decorator is set on. */
memberSpan: ParseSourceSpan;
/** Arguments passed into the decorator */
arguments: SourceNode[];
/** Location information about the decorator. */
decoratorSpan: ParseSourceSpan;
}
/**
* Creates an AST node that represents the host element of a directive.
* Can return null if there are no valid bindings to be checked.
* @param type Whether the host element is for a directive or a component.
* @param selector Selector of the directive.
* @param sourceNode Class declaration for the directive.
* @param literal `host` object literal from the decorator.
* @param bindingDecorators `HostBinding` decorators discovered on the node.
* @param listenerDecorators `HostListener` decorators discovered on the node.
* @param meta Metadata used to construct the host element.
*/
export function createHostElement(
type: 'component' | 'directive',
selector: string | null,
sourceNode: ClassDeclaration,
literal: ts.ObjectLiteralExpression | null,
bindingDecorators: Iterable<ts.Decorator>,
listenerDecorators: Iterable<ts.Decorator>,
nameSpan: ParseSourceSpan,
hostObjectLiteralBindings: HostObjectLiteralBinding[],
hostBindingDecorators: HostBindingDecorator[],
hostListenerDecorators: HostListenerDecorator[],
): TmplAstHostElement | null {
const bindings: TmplAstBoundAttribute[] = [];
const listeners: TmplAstBoundEvent[] = [];
let parser: BindingParser | null = null;
if (literal !== null) {
for (const prop of literal.properties) {
// We only support type checking of static bindings.
if (
ts.isPropertyAssignment(prop) &&
ts.isStringLiteralLike(prop.initializer) &&
isStaticName(prop.name)
) {
parser ??= makeBindingParser();
createNodeFromHostLiteralProperty(
prop as StaticPropertyAssignment,
parser,
bindings,
listeners,
);
}
}
for (const binding of hostObjectLiteralBindings) {
// We only support type checking of static bindings.
parser ??= makeBindingParser();
createNodeFromHostLiteralProperty(binding, parser, bindings, listeners);
}
for (const decorator of bindingDecorators) {
for (const decorator of hostBindingDecorators) {
createNodeFromBindingDecorator(decorator, bindings);
}
for (const decorator of listenerDecorators) {
for (const decorator of hostListenerDecorators) {
parser ??= makeBindingParser();
createNodeFromListenerDecorator(decorator, parser, listeners);
}
@@ -122,7 +148,7 @@ export function createHostElement(
tagNames.push(`ng-${type}`);
}
return new TmplAstHostElement(tagNames, bindings, listeners, createSourceSpan(sourceNode.name));
return new TmplAstHostElement(tagNames, bindings, listeners, nameSpan);
}
/**
@@ -173,7 +199,7 @@ export function isHostBindingsBlockGuard(node: ts.Node): boolean {
* @param listeners Array tracking the event listeners of the host element.
*/
function createNodeFromHostLiteralProperty(
property: StaticPropertyAssignment,
binding: HostObjectLiteralBinding,
parser: BindingParser,
bindings: TmplAstBoundAttribute[],
listeners: TmplAstBoundEvent[],
@@ -181,17 +207,25 @@ function createNodeFromHostLiteralProperty(
// TODO(crisbeto): surface parsing errors here, because currently they just get ignored.
// They'll still get reported when the handler tries to parse the bindings, but here we
// can highlight the nodes more accurately.
const {name, initializer} = property;
const {key, value, sourceSpan} = binding;
if (name.text.startsWith('[') && name.text.endsWith(']')) {
const {attrName, type} = inferBoundAttribute(name.text.slice(1, -1));
const valueSpan = createStaticExpressionSpan(initializer);
const ast = parser.parseBinding(initializer.text, true, valueSpan, valueSpan.start.offset);
if (key.kind !== 'string' || value.kind !== 'string') {
return;
}
if (key.text.startsWith('[') && key.text.endsWith(']')) {
const {attrName, type} = inferBoundAttribute(key.text.slice(1, -1));
const ast = parser.parseBinding(
value.text,
true,
value.sourceSpan,
value.sourceSpan.start.offset,
);
if (ast.errors.length > 0) {
return; // See TODO above.
}
fixupSpans(ast, initializer);
fixupSpans(ast, value);
bindings.push(
new TmplAstBoundAttribute(
attrName,
@@ -199,31 +233,31 @@ function createNodeFromHostLiteralProperty(
0,
ast,
null,
createSourceSpan(property),
createStaticExpressionSpan(name),
valueSpan,
sourceSpan,
key.sourceSpan,
value.sourceSpan,
undefined,
),
);
} else if (name.text.startsWith('(') && name.text.endsWith(')')) {
} else if (key.text.startsWith('(') && key.text.endsWith(')')) {
const events: ParsedEvent[] = [];
parser.parseEvent(
name.text.slice(1, -1),
initializer.text,
key.text.slice(1, -1),
value.text,
false,
createSourceSpan(property),
createStaticExpressionSpan(initializer),
sourceSpan,
value.sourceSpan,
[],
events,
createStaticExpressionSpan(name),
key.sourceSpan,
);
if (events.length === 0 || events[0].handler.errors.length > 0) {
return; // See TODO above.
}
fixupSpans(events[0].handler, initializer);
fixupSpans(events[0].handler, value);
listeners.push(TmplAstBoundEvent.fromParsedEvent(events[0]));
}
}
@@ -234,34 +268,23 @@ function createNodeFromHostLiteralProperty(
* @param bindings Array tracking the bound attributes of the host element.
*/
function createNodeFromBindingDecorator(
decorator: ts.Decorator,
decorator: HostBindingDecorator,
bindings: TmplAstBoundAttribute[],
): void {
// We only support decorators that are being called.
if (!ts.isCallExpression(decorator.expression)) {
return;
}
const args = decorator.expression.arguments;
const property = decorator.parent;
let nameNode: StaticName | null = null;
let propertyName: StaticName | null = null;
if (property && ts.isPropertyDeclaration(property) && isStaticName(property.name)) {
propertyName = property.name;
}
const args = decorator.arguments;
let nameNode: SourceNode;
// The first parameter is optional. If omitted, the name
// of the class member is used as the property.
if (args.length === 0) {
nameNode = propertyName;
} else if (ts.isStringLiteralLike(args[0])) {
nameNode = decorator.memberName;
} else if (args[0].kind === 'string') {
nameNode = args[0];
} else {
return;
}
if (nameNode === null || propertyName === null) {
if (nameNode.kind !== 'string' && nameNode.kind !== 'identifier') {
return;
}
@@ -272,17 +295,21 @@ function createNodeFromBindingDecorator(
// manually here. Note that we use a dummy span with -1/-1 as offsets, because it isn't
// used for type checking and constructing it accurately would take some effort.
const span = new ParseSpan(-1, -1);
const propertyStart = property.getStart();
const propertyStart = decorator.memberSpan.start.offset;
const receiver = new ThisReceiver(span, new AbsoluteSourceSpan(propertyStart, propertyStart));
const nameSpan = new AbsoluteSourceSpan(propertyName.getStart(), propertyName.getEnd());
const read = ts.isIdentifier(propertyName)
? new PropertyRead(span, nameSpan, nameSpan, receiver, propertyName.text)
: new KeyedRead(
span,
nameSpan,
receiver,
new LiteralPrimitive(span, nameSpan, propertyName.text),
);
const nameSpan = new AbsoluteSourceSpan(
nameNode.sourceSpan.start.offset,
nameNode.sourceSpan.end.offset,
);
const read =
decorator.memberName.kind === 'string'
? new KeyedRead(
span,
nameSpan,
receiver,
new LiteralPrimitive(span, nameSpan, decorator.memberName.text),
)
: new PropertyRead(span, nameSpan, nameSpan, receiver, decorator.memberName.text);
const {attrName, type} = inferBoundAttribute(nameNode.text);
bindings.push(
@@ -292,9 +319,9 @@ function createNodeFromBindingDecorator(
0,
read,
null,
createSourceSpan(decorator),
createStaticExpressionSpan(nameNode),
createSourceSpan(decorator),
decorator.decoratorSpan,
nameNode.sourceSpan,
decorator.decoratorSpan,
undefined,
),
);
@@ -307,25 +334,11 @@ function createNodeFromBindingDecorator(
* @param bindings Array tracking the bound events of the host element.
*/
function createNodeFromListenerDecorator(
decorator: ts.Decorator,
decorator: HostListenerDecorator,
parser: BindingParser,
listeners: TmplAstBoundEvent[],
): void {
// We only support decorators that are being called with at least one argument.
if (!ts.isCallExpression(decorator.expression) || decorator.expression.arguments.length === 0) {
return;
}
const args = decorator.expression.arguments;
const method = decorator.parent;
// Only handle decorators that are statically analyzable.
if (
!method ||
!ts.isMethodDeclaration(method) ||
!isStaticName(method.name) ||
!ts.isStringLiteralLike(args[0])
) {
if (decorator.eventName === null || decorator.eventName.kind !== 'string') {
return;
}
@@ -335,53 +348,61 @@ function createNodeFromListenerDecorator(
// something like `(foo)="handleFoo()"` in the AST. Instead we construct the expressions
// manually here. Note that we use a dummy span with -1/-1 as offsets, because it isn't
// used for type checking and constructing it accurately would take some effort.
const span = new ParseSpan(-1, -1);
const dummySpan = new ParseSpan(-1, -1);
const argNodes: AST[] = [];
const methodStart = method.getStart();
const methodReceiver = new ThisReceiver(span, new AbsoluteSourceSpan(methodStart, methodStart));
const nameSpan = new AbsoluteSourceSpan(method.name.getStart(), method.name.getEnd());
const receiver = ts.isIdentifier(method.name)
? new PropertyRead(span, nameSpan, nameSpan, methodReceiver, method.name.text)
: new KeyedRead(
span,
nameSpan,
methodReceiver,
new LiteralPrimitive(span, nameSpan, method.name.text),
);
const methodStart = decorator.memberSpan.start.offset;
const methodReceiver = new ThisReceiver(
dummySpan,
new AbsoluteSourceSpan(methodStart, methodStart),
);
const nameSpan = new AbsoluteSourceSpan(
decorator.memberName.sourceSpan.start.offset,
decorator.memberName.sourceSpan.end.offset,
);
const receiver =
decorator.memberName.kind === 'string'
? new KeyedRead(
dummySpan,
nameSpan,
methodReceiver,
new LiteralPrimitive(dummySpan, nameSpan, decorator.memberName.text),
)
: new PropertyRead(dummySpan, nameSpan, nameSpan, methodReceiver, decorator.memberName.text);
if (args.length > 1 && ts.isArrayLiteralExpression(args[1])) {
for (const expr of args[1].elements) {
// If the parameter is a static string, parse it using the binding parser since it can be any
// expression, otherwise treat it as `any` so the rest of the parameters can be checked.
if (ts.isStringLiteralLike(expr)) {
const span = createStaticExpressionSpan(expr);
const ast = parser.parseBinding(expr.text, true, span, span.start.offset);
fixupSpans(ast, expr);
argNodes.push(ast);
} else {
// Represents `$any(0)`. We need to construct it manually in order to set the right spans.
const expressionSpan = new AbsoluteSourceSpan(expr.getStart(), expr.getEnd());
const anyRead = new PropertyRead(
span,
expressionSpan,
expressionSpan,
new ImplicitReceiver(span, expressionSpan),
'$any',
);
const anyCall = new Call(
span,
expressionSpan,
anyRead,
[new LiteralPrimitive(span, expressionSpan, 0)],
expressionSpan,
);
argNodes.push(anyCall);
}
for (const arg of decorator.arguments) {
// If the parameter is a static string, parse it using the binding parser since it can be any
// expression, otherwise treat it as `any` so the rest of the parameters can be checked.
if (arg.kind === 'string') {
const span = arg.sourceSpan;
const ast = parser.parseBinding(arg.text, true, span, span.start.offset);
fixupSpans(ast, arg);
argNodes.push(ast);
} else {
// Represents `$any(0)`. We need to construct it manually in order to set the right spans.
const expressionSpan = new AbsoluteSourceSpan(
arg.sourceSpan.start.offset,
arg.sourceSpan.end.offset,
);
const anyRead = new PropertyRead(
dummySpan,
expressionSpan,
expressionSpan,
new ImplicitReceiver(dummySpan, expressionSpan),
'$any',
);
const anyCall = new Call(
dummySpan,
expressionSpan,
anyRead,
[new LiteralPrimitive(dummySpan, expressionSpan, 0)],
expressionSpan,
);
argNodes.push(anyCall);
}
}
const callNode = new Call(span, nameSpan, receiver, argNodes, span);
const eventNameNode = args[0];
const callNode = new Call(dummySpan, nameSpan, receiver, argNodes, dummySpan);
const eventNameNode = decorator.eventName;
let type: ParsedEventType;
let eventName: string;
let phase: string | null;
@@ -408,9 +429,9 @@ function createNodeFromListenerDecorator(
callNode,
target,
phase,
createSourceSpan(decorator),
createSourceSpan(decorator),
createStaticExpressionSpan(eventNameNode),
decorator.decoratorSpan,
decorator.decoratorSpan,
eventNameNode.sourceSpan,
),
);
}
@@ -452,31 +473,12 @@ function inferBoundAttribute(name: string): {attrName: string; type: BindingType
return {attrName, type};
}
/** Checks whether the specified node is a static name node. */
function isStaticName(node: ts.Node): node is StaticName {
return ts.isIdentifier(node) || ts.isStringLiteralLike(node);
}
/** Creates a `ParseSourceSpan` pointing to a static expression AST node's source. */
function createStaticExpressionSpan(node: ts.StringLiteralLike | ts.Identifier): ParseSourceSpan {
const span = createSourceSpan(node);
// Offset by one on both sides to skip over the quotes.
if (ts.isStringLiteralLike(node)) {
span.fullStart = span.fullStart.moveBy(1);
span.start = span.start.moveBy(1);
span.end = span.end.moveBy(-1);
}
return span;
}
/**
* Adjusts the spans of a parsed AST so that they're appropriate for a host bindings context.
* @param ast The parsed AST that may need to be adjusted.
* @param initializer TypeScript node from which the source of the AST was extracted.
*/
function fixupSpans(ast: AST, initializer: ts.StringLiteralLike): void {
function fixupSpans(ast: AST, node: StaticSourceNode): void {
// When parsing the initializer as a property/event binding, we use `.text` which excludes escaped
// quotes and is generally what we want, because preserving them would result in a parser error,
// however it has the downside in that the spans of the expressions following the escaped
@@ -501,11 +503,13 @@ function fixupSpans(ast: AST, initializer: ts.StringLiteralLike): void {
// 4. Constructing some sort of string like `<host ${name.getText()}=${initializer.getText()}/>`,
// passing it through the HTML parser and extracting the first attribute from it - wasn't explored
// much, but likely has the same issues as approach #3.
const escapeIndex = initializer.getText().indexOf('\\', 1);
const escapeIndex = node.source.indexOf('\\', 1);
if (escapeIndex > -1) {
const newSpan = new ParseSpan(0, initializer.getWidth());
const newSourceSpan = new AbsoluteSourceSpan(initializer.getStart(), initializer.getEnd());
const start = node.sourceSpan.start.offset;
const end = node.sourceSpan.end.offset;
const newSpan = new ParseSpan(0, end - start);
const newSourceSpan = new AbsoluteSourceSpan(start, end);
ast.visit(new ReplaceSpanVisitor(escapeIndex, newSpan, newSourceSpan));
}
}
@@ -579,7 +579,7 @@ runInEachFileSystem(() => {
expect(getDiagnosticSourceCode(diags[1])).toBe('handleClick');
});
it('should report diagnostic on the entire initializer of property binding if node contains escaped string', () => {
it('should report diagnostic on the entire expression of property binding if node contains escaped string', () => {
env.write(
'test.ts',
`
@@ -599,10 +599,10 @@ runInEachFileSystem(() => {
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Property 'doesNotExist' does not exist on type 'Dir'.`);
expect(getDiagnosticSourceCode(diags[0])).toBe(`'prefix + \\'123\\' + doesNotExist'`);
expect(getDiagnosticSourceCode(diags[0])).toBe(`prefix + \\'123\\' + doesNotExist`);
});
it('should report diagnostic on the entire initializer of event binding if node contains escaped string', () => {
it('should report diagnostic on the entire expression of event binding if node contains escaped string', () => {
env.write(
'test.ts',
`
@@ -624,7 +624,7 @@ runInEachFileSystem(() => {
expect(diags[0].messageText).toBe(
`Argument of type 'string' is not assignable to parameter of type 'number'.`,
);
expect(getDiagnosticSourceCode(diags[0])).toBe(`'handleClick(\\'foo\\')'`);
expect(getDiagnosticSourceCode(diags[0])).toBe(`handleClick(\\'foo\\')`);
});
it('should preserve diagnostic location of nodes that occur before escaped string', () => {