angular/packages/compiler/src/template_parser/binding_parser.ts
Kristiyan Kostadinov fb1fa8b0fc fix(compiler-cli): more accurate diagnostics for host binding parser errors (#58870)
Currently host bindings are in a bit of a weird state, because their source spans all point to the root object literal, rather than the individual expression. This is tricky to handle at the moment, because the object is being passed around as a `Record<string, string>` since the compiler needs to support both JIT and non-JIT environments, and because the AOT compiler evaluates the entire literal rather than doing it expression-by-expression. As a result, when we report errors in one of the host bindings, we end up highlighting the entire expression which can be very noisy in an IDE.

These changes aim to report a more accurate error for the most common case where the `host` object is initialized to a `string -> string` object literal by matching the failing expression to one of the property initializers. Note that this isn't 100% reliable, because we can't map cases like `host: SOME_CONST`, but it's still better than the current setup.

PR Close #58870
2024-11-25 15:25:48 +00:00

897 lines
27 KiB
TypeScript

/**
* @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.dev/license
*/
import {SecurityContext} from '../core';
import {
AbsoluteSourceSpan,
AST,
ASTWithSource,
Binary,
BindingPipe,
BindingType,
BoundElementProperty,
Conditional,
EmptyExpr,
KeyedRead,
NonNullAssert,
ParsedEvent,
ParsedEventType,
ParsedProperty,
ParsedPropertyType,
ParsedVariable,
ParserError,
PrefixNot,
PropertyRead,
RecursiveAstVisitor,
TemplateBinding,
VariableBinding,
} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {InterpolationConfig} from '../ml_parser/defaults';
import {mergeNsAndName} from '../ml_parser/tags';
import {InterpolatedAttributeToken, InterpolatedTextToken} from '../ml_parser/tokens';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
const PROPERTY_PARTS_SEPARATOR = '.';
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
const TEMPLATE_ATTR_PREFIX = '*';
const ANIMATE_PROP_PREFIX = 'animate-';
export interface HostProperties {
[key: string]: string;
}
export interface HostListeners {
[key: string]: string;
}
/**
* Parses bindings in templates and in the directive host area.
*/
export class BindingParser {
constructor(
private _exprParser: Parser,
private _interpolationConfig: InterpolationConfig,
private _schemaRegistry: ElementSchemaRegistry,
public errors: ParseError[],
private _allowInvalidAssignmentEvents = false,
) {}
get interpolationConfig(): InterpolationConfig {
return this._interpolationConfig;
}
createBoundHostProperties(
properties: HostProperties,
sourceSpan: ParseSourceSpan,
): ParsedProperty[] | null {
const boundProps: ParsedProperty[] = [];
for (const propName of Object.keys(properties)) {
const expression = properties[propName];
if (typeof expression === 'string') {
this.parsePropertyBinding(
propName,
expression,
true,
false,
sourceSpan,
sourceSpan.start.offset,
undefined,
[],
// Use the `sourceSpan` for `keySpan`. This isn't really accurate, but neither is the
// sourceSpan, as it represents the sourceSpan of the host itself rather than the
// source of the host binding (which doesn't exist in the template). Regardless,
// neither of these values are used in Ivy but are only here to satisfy the function
// signature. This should likely be refactored in the future so that `sourceSpan`
// isn't being used inaccurately.
boundProps,
sourceSpan,
);
} else {
this._reportError(
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
sourceSpan,
);
}
}
return boundProps;
}
createDirectiveHostEventAsts(
hostListeners: HostListeners,
sourceSpan: ParseSourceSpan,
): ParsedEvent[] | null {
const targetEvents: ParsedEvent[] = [];
for (const propName of Object.keys(hostListeners)) {
const expression = hostListeners[propName];
if (typeof expression === 'string') {
// Use the `sourceSpan` for `keySpan` and `handlerSpan`. This isn't really accurate, but
// neither is the `sourceSpan`, as it represents the `sourceSpan` of the host itself
// rather than the source of the host binding (which doesn't exist in the template).
// Regardless, neither of these values are used in Ivy but are only here to satisfy the
// function signature. This should likely be refactored in the future so that `sourceSpan`
// isn't being used inaccurately.
this.parseEvent(
propName,
expression,
/* isAssignmentEvent */ false,
sourceSpan,
sourceSpan,
[],
targetEvents,
sourceSpan,
);
} else {
this._reportError(
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
sourceSpan,
);
}
}
return targetEvents;
}
parseInterpolation(
value: string,
sourceSpan: ParseSourceSpan,
interpolatedTokens: InterpolatedAttributeToken[] | InterpolatedTextToken[] | null,
): ASTWithSource {
const sourceInfo = sourceSpan.start.toString();
const absoluteOffset = sourceSpan.fullStart.offset;
try {
const ast = this._exprParser.parseInterpolation(
value,
sourceInfo,
absoluteOffset,
interpolatedTokens,
this._interpolationConfig,
)!;
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
}
}
/**
* Similar to `parseInterpolation`, but treats the provided string as a single expression
* element that would normally appear within the interpolation prefix and suffix (`{{` and `}}`).
* This is used for parsing the switch expression in ICUs.
*/
parseInterpolationExpression(expression: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = sourceSpan.start.toString();
const absoluteOffset = sourceSpan.start.offset;
try {
const ast = this._exprParser.parseInterpolationExpression(
expression,
sourceInfo,
absoluteOffset,
);
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
}
}
/**
* Parses the bindings in a microsyntax expression, and converts them to
* `ParsedProperty` or `ParsedVariable`.
*
* @param tplKey template binding name
* @param tplValue template binding value
* @param sourceSpan span of template binding relative to entire the template
* @param absoluteValueOffset start of the tplValue relative to the entire template
* @param targetMatchableAttrs potential attributes to match in the template
* @param targetProps target property bindings in the template
* @param targetVars target variables in the template
*/
parseInlineTemplateBinding(
tplKey: string,
tplValue: string,
sourceSpan: ParseSourceSpan,
absoluteValueOffset: number,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
targetVars: ParsedVariable[],
isIvyAst: boolean,
) {
const absoluteKeyOffset = sourceSpan.start.offset + TEMPLATE_ATTR_PREFIX.length;
const bindings = this._parseTemplateBindings(
tplKey,
tplValue,
sourceSpan,
absoluteKeyOffset,
absoluteValueOffset,
);
for (const binding of bindings) {
// sourceSpan is for the entire HTML attribute. bindingSpan is for a particular
// binding within the microsyntax expression so it's more narrow than sourceSpan.
const bindingSpan = moveParseSourceSpan(sourceSpan, binding.sourceSpan);
const key = binding.key.source;
const keySpan = moveParseSourceSpan(sourceSpan, binding.key.span);
if (binding instanceof VariableBinding) {
const value = binding.value ? binding.value.source : '$implicit';
const valueSpan = binding.value
? moveParseSourceSpan(sourceSpan, binding.value.span)
: undefined;
targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
} else if (binding.value) {
const srcSpan = isIvyAst ? bindingSpan : sourceSpan;
const valueSpan = moveParseSourceSpan(sourceSpan, binding.value.ast.sourceSpan);
this._parsePropertyAst(
key,
binding.value,
false,
srcSpan,
keySpan,
valueSpan,
targetMatchableAttrs,
targetProps,
);
} else {
targetMatchableAttrs.push([key, '' /* value */]);
// Since this is a literal attribute with no RHS, source span should be
// just the key span.
this.parseLiteralAttr(
key,
null /* value */,
keySpan,
absoluteValueOffset,
undefined /* valueSpan */,
targetMatchableAttrs,
targetProps,
keySpan,
);
}
}
}
/**
* Parses the bindings in a microsyntax expression, e.g.
* ```
* <tag *tplKey="let value1 = prop; let value2 = localVar">
* ```
*
* @param tplKey template binding name
* @param tplValue template binding value
* @param sourceSpan span of template binding relative to entire the template
* @param absoluteKeyOffset start of the `tplKey`
* @param absoluteValueOffset start of the `tplValue`
*/
private _parseTemplateBindings(
tplKey: string,
tplValue: string,
sourceSpan: ParseSourceSpan,
absoluteKeyOffset: number,
absoluteValueOffset: number,
): TemplateBinding[] {
const sourceInfo = sourceSpan.start.toString();
try {
const bindingsResult = this._exprParser.parseTemplateBindings(
tplKey,
tplValue,
sourceInfo,
absoluteKeyOffset,
absoluteValueOffset,
);
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.warnings.forEach((warning) => {
this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING);
});
return bindingsResult.templateBindings;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return [];
}
}
parseLiteralAttr(
name: string,
value: string | null,
sourceSpan: ParseSourceSpan,
absoluteOffset: number,
valueSpan: ParseSourceSpan | undefined,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
keySpan: ParseSourceSpan,
) {
if (isAnimationLabel(name)) {
name = name.substring(1);
if (keySpan !== undefined) {
keySpan = moveParseSourceSpan(
keySpan,
new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
);
}
if (value) {
this._reportError(
`Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
sourceSpan,
ParseErrorLevel.ERROR,
);
}
this._parseAnimation(
name,
value,
sourceSpan,
absoluteOffset,
keySpan,
valueSpan,
targetMatchableAttrs,
targetProps,
);
} else {
targetProps.push(
new ParsedProperty(
name,
this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset),
ParsedPropertyType.LITERAL_ATTR,
sourceSpan,
keySpan,
valueSpan,
),
);
}
}
parsePropertyBinding(
name: string,
expression: string,
isHost: boolean,
isPartOfAssignmentBinding: boolean,
sourceSpan: ParseSourceSpan,
absoluteOffset: number,
valueSpan: ParseSourceSpan | undefined,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
keySpan: ParseSourceSpan,
) {
if (name.length === 0) {
this._reportError(`Property name is missing in binding`, sourceSpan);
}
let isAnimationProp = false;
if (name.startsWith(ANIMATE_PROP_PREFIX)) {
isAnimationProp = true;
name = name.substring(ANIMATE_PROP_PREFIX.length);
if (keySpan !== undefined) {
keySpan = moveParseSourceSpan(
keySpan,
new AbsoluteSourceSpan(
keySpan.start.offset + ANIMATE_PROP_PREFIX.length,
keySpan.end.offset,
),
);
}
} else if (isAnimationLabel(name)) {
isAnimationProp = true;
name = name.substring(1);
if (keySpan !== undefined) {
keySpan = moveParseSourceSpan(
keySpan,
new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
);
}
}
if (isAnimationProp) {
this._parseAnimation(
name,
expression,
sourceSpan,
absoluteOffset,
keySpan,
valueSpan,
targetMatchableAttrs,
targetProps,
);
} else {
this._parsePropertyAst(
name,
this.parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset),
isPartOfAssignmentBinding,
sourceSpan,
keySpan,
valueSpan,
targetMatchableAttrs,
targetProps,
);
}
}
parsePropertyInterpolation(
name: string,
value: string,
sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan | undefined,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
keySpan: ParseSourceSpan,
interpolatedTokens: InterpolatedAttributeToken[] | InterpolatedTextToken[] | null,
): boolean {
const expr = this.parseInterpolation(value, valueSpan || sourceSpan, interpolatedTokens);
if (expr) {
this._parsePropertyAst(
name,
expr,
false,
sourceSpan,
keySpan,
valueSpan,
targetMatchableAttrs,
targetProps,
);
return true;
}
return false;
}
private _parsePropertyAst(
name: string,
ast: ASTWithSource,
isPartOfAssignmentBinding: boolean,
sourceSpan: ParseSourceSpan,
keySpan: ParseSourceSpan,
valueSpan: ParseSourceSpan | undefined,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
) {
targetMatchableAttrs.push([name, ast.source!]);
targetProps.push(
new ParsedProperty(
name,
ast,
isPartOfAssignmentBinding ? ParsedPropertyType.TWO_WAY : ParsedPropertyType.DEFAULT,
sourceSpan,
keySpan,
valueSpan,
),
);
}
private _parseAnimation(
name: string,
expression: string | null,
sourceSpan: ParseSourceSpan,
absoluteOffset: number,
keySpan: ParseSourceSpan,
valueSpan: ParseSourceSpan | undefined,
targetMatchableAttrs: string[][],
targetProps: ParsedProperty[],
) {
if (name.length === 0) {
this._reportError('Animation trigger is missing', sourceSpan);
}
// This will occur when a @trigger is not paired with an expression.
// For animations it is valid to not have an expression since */void
// states will be applied by angular when the element is attached/detached
const ast = this.parseBinding(
expression || 'undefined',
false,
valueSpan || sourceSpan,
absoluteOffset,
);
targetMatchableAttrs.push([name, ast.source!]);
targetProps.push(
new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, keySpan, valueSpan),
);
}
parseBinding(
value: string,
isHostBinding: boolean,
sourceSpan: ParseSourceSpan,
absoluteOffset: number,
): ASTWithSource {
const sourceInfo = ((sourceSpan && sourceSpan.start) || '(unknown)').toString();
try {
const ast = isHostBinding
? this._exprParser.parseSimpleBinding(
value,
sourceInfo,
absoluteOffset,
this._interpolationConfig,
)
: this._exprParser.parseBinding(
value,
sourceInfo,
absoluteOffset,
this._interpolationConfig,
);
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
}
}
createBoundElementProperty(
elementSelector: string,
boundProp: ParsedProperty,
skipValidation: boolean = false,
mapPropertyName: boolean = true,
): BoundElementProperty {
if (boundProp.isAnimation) {
return new BoundElementProperty(
boundProp.name,
BindingType.Animation,
SecurityContext.NONE,
boundProp.expression,
null,
boundProp.sourceSpan,
boundProp.keySpan,
boundProp.valueSpan,
);
}
let unit: string | null = null;
let bindingType: BindingType = undefined!;
let boundPropertyName: string | null = null;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContexts: SecurityContext[] = undefined!;
// Check for special cases (prefix style, attr, class)
if (parts.length > 1) {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts.slice(1).join(PROPERTY_PARTS_SEPARATOR);
if (!skipValidation) {
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
}
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry,
elementSelector,
boundPropertyName,
true,
);
const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) {
const ns = boundPropertyName.substring(0, nsSeparatorIdx);
const name = boundPropertyName.substring(nsSeparatorIdx + 1);
boundPropertyName = mergeNsAndName(ns, name);
}
bindingType = BindingType.Attribute;
} else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1];
bindingType = BindingType.Class;
securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
bindingType = BindingType.Style;
securityContexts = [SecurityContext.STYLE];
}
}
// If not a special case, use the full property name
if (boundPropertyName === null) {
const mappedPropName = this._schemaRegistry.getMappedPropName(boundProp.name);
boundPropertyName = mapPropertyName ? mappedPropName : boundProp.name;
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry,
elementSelector,
mappedPropName,
false,
);
bindingType =
boundProp.type === ParsedPropertyType.TWO_WAY ? BindingType.TwoWay : BindingType.Property;
if (!skipValidation) {
this._validatePropertyOrAttributeName(mappedPropName, boundProp.sourceSpan, false);
}
}
return new BoundElementProperty(
boundPropertyName,
bindingType,
securityContexts[0],
boundProp.expression,
unit,
boundProp.sourceSpan,
boundProp.keySpan,
boundProp.valueSpan,
);
}
// TODO: keySpan should be required but was made optional to avoid changing VE parser.
parseEvent(
name: string,
expression: string,
isAssignmentEvent: boolean,
sourceSpan: ParseSourceSpan,
handlerSpan: ParseSourceSpan,
targetMatchableAttrs: string[][],
targetEvents: ParsedEvent[],
keySpan: ParseSourceSpan,
) {
if (name.length === 0) {
this._reportError(`Event name is missing in binding`, sourceSpan);
}
if (isAnimationLabel(name)) {
name = name.slice(1);
if (keySpan !== undefined) {
keySpan = moveParseSourceSpan(
keySpan,
new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset),
);
}
this._parseAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents, keySpan);
} else {
this._parseRegularEvent(
name,
expression,
isAssignmentEvent,
sourceSpan,
handlerSpan,
targetMatchableAttrs,
targetEvents,
keySpan,
);
}
}
calcPossibleSecurityContexts(
selector: string,
propName: string,
isAttribute: boolean,
): SecurityContext[] {
const prop = this._schemaRegistry.getMappedPropName(propName);
return calcPossibleSecurityContexts(this._schemaRegistry, selector, prop, isAttribute);
}
private _parseAnimationEvent(
name: string,
expression: string,
sourceSpan: ParseSourceSpan,
handlerSpan: ParseSourceSpan,
targetEvents: ParsedEvent[],
keySpan: ParseSourceSpan,
) {
const matches = splitAtPeriod(name, [name, '']);
const eventName = matches[0];
const phase = matches[1].toLowerCase();
const ast = this._parseAction(expression, handlerSpan);
targetEvents.push(
new ParsedEvent(
eventName,
phase,
ParsedEventType.Animation,
ast,
sourceSpan,
handlerSpan,
keySpan,
),
);
if (eventName.length === 0) {
this._reportError(`Animation event name is missing in binding`, sourceSpan);
}
if (phase) {
if (phase !== 'start' && phase !== 'done') {
this._reportError(
`The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`,
sourceSpan,
);
}
} else {
this._reportError(
`The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`,
sourceSpan,
);
}
}
private _parseRegularEvent(
name: string,
expression: string,
isAssignmentEvent: boolean,
sourceSpan: ParseSourceSpan,
handlerSpan: ParseSourceSpan,
targetMatchableAttrs: string[][],
targetEvents: ParsedEvent[],
keySpan: ParseSourceSpan,
): void {
// long format: 'target: eventName'
const [target, eventName] = splitAtColon(name, [null!, name]);
const prevErrorCount = this.errors.length;
const ast = this._parseAction(expression, handlerSpan);
const isValid = this.errors.length === prevErrorCount;
targetMatchableAttrs.push([name!, ast.source!]);
// Don't try to validate assignment events if there were other
// parsing errors to avoid adding more noise to the error logs.
if (isAssignmentEvent && isValid && !this._isAllowedAssignmentEvent(ast)) {
this._reportError('Unsupported expression in a two-way binding', sourceSpan);
}
targetEvents.push(
new ParsedEvent(
eventName,
target,
isAssignmentEvent ? ParsedEventType.TwoWay : ParsedEventType.Regular,
ast,
sourceSpan,
handlerSpan,
keySpan,
),
);
// Don't detect directives for event names for now,
// so don't add the event name to the matchableAttrs
}
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = ((sourceSpan && sourceSpan.start) || '(unknown').toString();
const absoluteOffset = sourceSpan && sourceSpan.start ? sourceSpan.start.offset : 0;
try {
const ast = this._exprParser.parseAction(
value,
sourceInfo,
absoluteOffset,
this._interpolationConfig,
);
if (ast) {
this._reportExpressionParserErrors(ast.errors, sourceSpan);
}
if (!ast || ast.ast instanceof EmptyExpr) {
this._reportError(`Empty expressions are not allowed`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
}
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo, absoluteOffset);
}
}
private _reportError(
message: string,
sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.ERROR,
relatedError?: ParserError,
) {
this.errors.push(new ParseError(sourceSpan, message, level, relatedError));
}
private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
for (const error of errors) {
this._reportError(error.message, sourceSpan, undefined, error);
}
}
/**
* @param propName the name of the property / attribute
* @param sourceSpan
* @param isAttr true when binding to an attribute
*/
private _validatePropertyOrAttributeName(
propName: string,
sourceSpan: ParseSourceSpan,
isAttr: boolean,
): void {
const report = isAttr
? this._schemaRegistry.validateAttribute(propName)
: this._schemaRegistry.validateProperty(propName);
if (report.error) {
this._reportError(report.msg!, sourceSpan, ParseErrorLevel.ERROR);
}
}
/**
* Returns whether a parsed AST is allowed to be used within the event side of a two-way binding.
* @param ast Parsed AST to be checked.
*/
private _isAllowedAssignmentEvent(ast: AST): boolean {
if (ast instanceof ASTWithSource) {
return this._isAllowedAssignmentEvent(ast.ast);
}
if (ast instanceof NonNullAssert) {
return this._isAllowedAssignmentEvent(ast.expression);
}
if (ast instanceof PropertyRead || ast instanceof KeyedRead) {
return true;
}
// TODO(crisbeto): this logic is only here to support the automated migration away
// from invalid bindings. It should be removed once the migration is deleted.
if (!this._allowInvalidAssignmentEvents) {
return false;
}
if (ast instanceof Binary) {
return (
(ast.operation === '&&' || ast.operation === '||' || ast.operation === '??') &&
(ast.right instanceof PropertyRead || ast.right instanceof KeyedRead)
);
}
return ast instanceof Conditional || ast instanceof PrefixNot;
}
}
export class PipeCollector extends RecursiveAstVisitor {
pipes = new Map<string, BindingPipe>();
override visitPipe(ast: BindingPipe, context: any): any {
this.pipes.set(ast.name, ast);
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
}
function isAnimationLabel(name: string): boolean {
return name[0] == '@';
}
export function calcPossibleSecurityContexts(
registry: ElementSchemaRegistry,
selector: string,
propName: string,
isAttribute: boolean,
): SecurityContext[] {
const ctxs: SecurityContext[] = [];
CssSelector.parse(selector).forEach((selector) => {
const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
const notElementNames = new Set(
selector.notSelectors
.filter((selector) => selector.isElementSelector())
.map((selector) => selector.element),
);
const possibleElementNames = elementNames.filter(
(elementName) => !notElementNames.has(elementName),
);
ctxs.push(
...possibleElementNames.map((elementName) =>
registry.securityContext(elementName, propName, isAttribute),
),
);
});
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
}
/**
* Compute a new ParseSourceSpan based off an original `sourceSpan` by using
* absolute offsets from the specified `absoluteSpan`.
*
* @param sourceSpan original source span
* @param absoluteSpan absolute source span to move to
*/
function moveParseSourceSpan(
sourceSpan: ParseSourceSpan,
absoluteSpan: AbsoluteSourceSpan,
): ParseSourceSpan {
// The difference of two absolute offsets provide the relative offset
const startDiff = absoluteSpan.start - sourceSpan.start.offset;
const endDiff = absoluteSpan.end - sourceSpan.end.offset;
return new ParseSourceSpan(
sourceSpan.start.moveBy(startDiff),
sourceSpan.end.moveBy(endDiff),
sourceSpan.fullStart.moveBy(startDiff),
sourceSpan.details,
);
}