mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Leon Senft
parent
6d2eed61a1
commit
63ac1bd2fd
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user