JoostK c18c7e23ec fix(compiler): exclude trailing whitespace from element source spans (#40513)
If the template parse option `leadingTriviaChars` is configured to
consider whitespace as trivia, any trailing whitespace of an element
would be considered as leading trivia of the subsequent element, such
that its `start` span would start _after_ the whitespace. This means
that the start span cannot be used to mark the end of the current
element, as its trailing whitespace would then be included in its span.
Instead, the full start of the subsequent element should be used.

To harden the tests that for the Ivy parser, the test utility `parseR3`
has been adjusted to use the same configuration for `leadingTriviaChars`
as would be the case in its production counterpart `parseTemplate`. This
uncovered another bug in offset handling of the interpolation parser,
where the absolute offset was computed from the start source span
(which excludes leading trivia) whereas the interpolation expression
would include the leading trivia. As such, the absolute offset now also
uses the full start span.

Fixes #39148

PR Close #40513
2021-01-28 08:53:02 -08:00

129 lines
4.5 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.io/license
*/
import * as e from '../../../src/expression_parser/ast';
import {Lexer} from '../../../src/expression_parser/lexer';
import {Parser} from '../../../src/expression_parser/parser';
import * as html from '../../../src/ml_parser/ast';
import {HtmlParser, ParseTreeResult} from '../../../src/ml_parser/html_parser';
import {WhitespaceVisitor} from '../../../src/ml_parser/html_whitespaces';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
import * as a from '../../../src/render3/r3_ast';
import {htmlAstToRender3Ast, Render3ParseResult} from '../../../src/render3/r3_template_transform';
import {I18nMetaVisitor} from '../../../src/render3/view/i18n/meta';
import {LEADING_TRIVIA_CHARS} from '../../../src/render3/view/template';
import {BindingParser} from '../../../src/template_parser/binding_parser';
import {MockSchemaRegistry} from '../../../testing';
export function findExpression(tmpl: a.Node[], expr: string): e.AST|null {
const res = tmpl.reduce((found, node) => {
if (found !== null) {
return found;
} else {
return findExpressionInNode(node, expr);
}
}, null as e.AST | null);
if (res instanceof e.ASTWithSource) {
return res.ast;
}
return res;
}
function findExpressionInNode(node: a.Node, expr: string): e.AST|null {
if (node instanceof a.Element || node instanceof a.Template) {
return findExpression(
[
...node.inputs,
...node.outputs,
...node.children,
],
expr);
} else if (node instanceof a.BoundAttribute || node instanceof a.BoundText) {
const ts = toStringExpression(node.value);
return toStringExpression(node.value) === expr ? node.value : null;
} else if (node instanceof a.BoundEvent) {
return toStringExpression(node.handler) === expr ? node.handler : null;
} else {
return null;
}
}
export function toStringExpression(expr: e.AST): string {
while (expr instanceof e.ASTWithSource) {
expr = expr.ast;
}
if (expr instanceof e.PropertyRead) {
if (expr.receiver instanceof e.ImplicitReceiver) {
return expr.name;
} else {
return `${toStringExpression(expr.receiver)}.${expr.name}`;
}
} else if (expr instanceof e.ImplicitReceiver) {
return '';
} else if (expr instanceof e.Interpolation) {
let str = '{{';
for (let i = 0; i < expr.expressions.length; i++) {
str += expr.strings[i] + toStringExpression(expr.expressions[i]);
}
str += expr.strings[expr.strings.length - 1] + '}}';
return str;
} else {
throw new Error(`Unsupported type: ${(expr as any).constructor.name}`);
}
}
// Parse an html string to IVY specific info
export function parseR3(
input: string,
options: {preserveWhitespaces?: boolean,
leadingTriviaChars?: string[],
ignoreError?: boolean} = {}): Render3ParseResult {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(input, 'path:://to/template', {
tokenizeExpansionForms: true,
leadingTriviaChars: options.leadingTriviaChars ?? LEADING_TRIVIA_CHARS,
});
if (parseResult.errors.length > 0 && !options.ignoreError) {
const msg = parseResult.errors.map(e => e.toString()).join('\n');
throw new Error(msg);
}
let htmlNodes = processI18nMeta(parseResult).rootNodes;
if (!options.preserveWhitespaces) {
htmlNodes = html.visitAll(new WhitespaceVisitor(), htmlNodes);
}
const expressionParser = new Parser(new Lexer());
const schemaRegistry = new MockSchemaRegistry(
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false},
['onEvent'], ['onEvent']);
const bindingParser =
new BindingParser(expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, []);
const r3Result = htmlAstToRender3Ast(htmlNodes, bindingParser);
if (r3Result.errors.length > 0 && !options.ignoreError) {
const msg = r3Result.errors.map(e => e.toString()).join('\n');
throw new Error(msg);
}
return r3Result;
}
export function processI18nMeta(
htmlAstWithErrors: ParseTreeResult,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
return new ParseTreeResult(
html.visitAll(
new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false),
htmlAstWithErrors.rootNodes),
htmlAstWithErrors.errors);
}