mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(compiler): implement let declarations in html ast (#55848)
Adds a new `LetDeclaration` node to the AST that captures the `LetStart`, `LetValue` and `LetEnd` tokens into a single node. PR Close #55848
This commit is contained in:
parent
5ee235729c
commit
d44c8ee98c
16 changed files with 219 additions and 2 deletions
|
|
@ -332,6 +332,8 @@ class _Visitor implements html.Visitor {
|
|||
|
||||
visitBlockParameter(parameter: html.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {}
|
||||
|
||||
private _init(mode: _VisitorMode, interpolationConfig: InterpolationConfig): void {
|
||||
this._mode = mode;
|
||||
this._inI18nBlock = false;
|
||||
|
|
|
|||
|
|
@ -239,6 +239,10 @@ class _I18nVisitor implements html.Visitor {
|
|||
throw new Error('Unreachable code');
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert, text and interpolated tokens up into text and placeholder pieces.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ class XliffParser implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(parameter: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
@ -376,6 +378,8 @@ class XmlToI18n implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(parameter: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,6 +334,8 @@ class Xliff2Parser implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(parameter: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
@ -428,6 +430,8 @@ class XmlToI18n implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(parameter: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ class XtbParser implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(block: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
@ -226,6 +228,8 @@ class XmlToI18n implements ml.Visitor {
|
|||
|
||||
visitBlockParameter(block: ml.BlockParameter, context: any) {}
|
||||
|
||||
visitLetDeclaration(decl: ml.LetDeclaration, context: any) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,20 @@ export class BlockParameter implements BaseNode {
|
|||
}
|
||||
}
|
||||
|
||||
export class LetDeclaration implements BaseNode {
|
||||
constructor(
|
||||
public name: string,
|
||||
public value: string,
|
||||
public sourceSpan: ParseSourceSpan,
|
||||
readonly nameSpan: ParseSourceSpan,
|
||||
public valueSpan: ParseSourceSpan,
|
||||
) {}
|
||||
|
||||
visit(visitor: Visitor, context: any): any {
|
||||
return visitor.visitLetDeclaration(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Visitor {
|
||||
// Returning a truthy value from `visit()` will prevent `visitAll()` from the call to the typed
|
||||
// method and result returned will become the result included in `visitAll()`s result array.
|
||||
|
|
@ -165,6 +179,7 @@ export interface Visitor {
|
|||
visitExpansionCase(expansionCase: ExpansionCase, context: any): any;
|
||||
visitBlock(block: Block, context: any): any;
|
||||
visitBlockParameter(parameter: BlockParameter, context: any): any;
|
||||
visitLetDeclaration(decl: LetDeclaration, context: any): any;
|
||||
}
|
||||
|
||||
export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): any[] {
|
||||
|
|
@ -213,6 +228,8 @@ export class RecursiveVisitor implements Visitor {
|
|||
|
||||
visitBlockParameter(ast: BlockParameter, context: any): any {}
|
||||
|
||||
visitLetDeclaration(decl: LetDeclaration, context: any) {}
|
||||
|
||||
private visitChildren<T extends Node>(
|
||||
context: any,
|
||||
cb: (visit: <V extends Node>(children: V[] | undefined) => void) => void,
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ export class WhitespaceVisitor implements html.Visitor {
|
|||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
return decl;
|
||||
}
|
||||
}
|
||||
|
||||
function createWhitespaceProcessedTextToken({type, parts, sourceSpan}: TextToken): TextToken {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ class _Expander implements html.Visitor {
|
|||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
return decl;
|
||||
}
|
||||
}
|
||||
|
||||
// Plural forms are expanded to `NgPlural` and `NgPluralCase`s
|
||||
|
|
|
|||
|
|
@ -25,9 +25,13 @@ import {
|
|||
ExpansionCaseValueToken,
|
||||
ExpansionFormStartToken,
|
||||
IncompleteBlockOpenToken,
|
||||
IncompleteLetToken,
|
||||
IncompleteTagOpenToken,
|
||||
InterpolatedAttributeToken,
|
||||
InterpolatedTextToken,
|
||||
LetEndToken,
|
||||
LetStartToken,
|
||||
LetValueToken,
|
||||
TagCloseToken,
|
||||
TagOpenStartToken,
|
||||
TextToken,
|
||||
|
|
@ -127,6 +131,12 @@ class _TreeBuilder {
|
|||
} else if (this._peek.type === TokenType.INCOMPLETE_BLOCK_OPEN) {
|
||||
this._closeVoidElement();
|
||||
this._consumeIncompleteBlock(this._advance());
|
||||
} else if (this._peek.type === TokenType.LET_START) {
|
||||
this._closeVoidElement();
|
||||
this._consumeLet(this._advance());
|
||||
} else if (this._peek.type === TokenType.INCOMPLETE_LET) {
|
||||
this._closeVoidElement();
|
||||
this._consumeIncompleteLet(this._advance());
|
||||
} else {
|
||||
// Skip all other tokens...
|
||||
this._advance();
|
||||
|
|
@ -627,6 +637,89 @@ class _TreeBuilder {
|
|||
);
|
||||
}
|
||||
|
||||
private _consumeLet(startToken: LetStartToken) {
|
||||
const name = startToken.parts[0];
|
||||
let valueToken: LetValueToken;
|
||||
let endToken: LetEndToken;
|
||||
|
||||
if (this._peek.type !== TokenType.LET_VALUE) {
|
||||
this.errors.push(
|
||||
TreeError.create(
|
||||
startToken.parts[0],
|
||||
startToken.sourceSpan,
|
||||
`Invalid @let declaration "${name}". Declaration must have a value.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
valueToken = this._advance();
|
||||
}
|
||||
|
||||
// Type cast is necessary here since TS narrowed the type of `peek` above.
|
||||
if ((this._peek as Token).type !== TokenType.LET_END) {
|
||||
this.errors.push(
|
||||
TreeError.create(
|
||||
startToken.parts[0],
|
||||
startToken.sourceSpan,
|
||||
`Unterminated @let declaration "${name}". Declaration must be terminated with a semicolon.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
endToken = this._advance();
|
||||
}
|
||||
|
||||
const end = endToken.sourceSpan.fullStart;
|
||||
const span = new ParseSourceSpan(
|
||||
startToken.sourceSpan.start,
|
||||
end,
|
||||
startToken.sourceSpan.fullStart,
|
||||
);
|
||||
|
||||
// The start token usually captures the `@let`. Construct a name span by
|
||||
// offsetting the start by the length of any text before the name.
|
||||
const startOffset = startToken.sourceSpan.toString().lastIndexOf(name);
|
||||
const nameStart = startToken.sourceSpan.start.moveBy(startOffset);
|
||||
const nameSpan = new ParseSourceSpan(nameStart, startToken.sourceSpan.end);
|
||||
const node = new html.LetDeclaration(
|
||||
name,
|
||||
valueToken.parts[0],
|
||||
span,
|
||||
nameSpan,
|
||||
valueToken.sourceSpan,
|
||||
);
|
||||
|
||||
this._addToParent(node);
|
||||
}
|
||||
|
||||
private _consumeIncompleteLet(token: IncompleteLetToken) {
|
||||
// Incomplete `@let` declaration may end up with an empty name.
|
||||
const name = token.parts[0] ?? '';
|
||||
const nameString = name ? ` "${name}"` : '';
|
||||
|
||||
// If there's at least a name, we can salvage an AST node that can be used for completions.
|
||||
if (name.length > 0) {
|
||||
const startOffset = token.sourceSpan.toString().lastIndexOf(name);
|
||||
const nameStart = token.sourceSpan.start.moveBy(startOffset);
|
||||
const nameSpan = new ParseSourceSpan(nameStart, token.sourceSpan.end);
|
||||
const valueSpan = new ParseSourceSpan(
|
||||
token.sourceSpan.start,
|
||||
token.sourceSpan.start.moveBy(0),
|
||||
);
|
||||
const node = new html.LetDeclaration(name, '', token.sourceSpan, nameSpan, valueSpan);
|
||||
this._addToParent(node);
|
||||
}
|
||||
|
||||
this.errors.push(
|
||||
TreeError.create(
|
||||
token.parts[0],
|
||||
token.sourceSpan,
|
||||
`Incomplete @let declaration${nameString}. ` +
|
||||
`@let declarations must be written as \`@let <name> = <value>;\``,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _getContainer(): NodeContainer | null {
|
||||
return this._containerStack.length > 0
|
||||
? this._containerStack[this._containerStack.length - 1]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class XmlParser extends Parser {
|
|||
}
|
||||
|
||||
override parse(source: string, url: string, options: TokenizeOptions = {}): ParseTreeResult {
|
||||
// Blocks aren't supported in an XML context.
|
||||
return super.parse(source, url, {...options, tokenizeBlocks: false});
|
||||
// Blocks and let declarations aren't supported in an XML context.
|
||||
return super.parse(source, url, {...options, tokenizeBlocks: false, tokenizeLet: false});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,6 +389,10 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
return null;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
throw new Error('TODO: implement R3 LetDeclaration');
|
||||
}
|
||||
|
||||
visitBlockParameter() {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -893,6 +897,10 @@ class NonBindableVisitor implements html.Visitor {
|
|||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
return null;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
throw new Error('TODO: implement R3 LetDeclaration');
|
||||
}
|
||||
}
|
||||
|
||||
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
|
||||
|
|
|
|||
|
|
@ -187,6 +187,10 @@ export class I18nMetaVisitor implements html.Visitor {
|
|||
return parameter;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
return decl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the general form `meta` passed into extract the explicit metadata needed to create a
|
||||
* `Message`.
|
||||
|
|
|
|||
|
|
@ -114,6 +114,17 @@ class _Humanizer implements html.Visitor {
|
|||
this.result.push(this._appendContext(parameter, [html.BlockParameter, parameter.expression]));
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
const res = this._appendContext(decl, [html.LetDeclaration, decl.name, decl.value]);
|
||||
|
||||
if (this.includeSourceSpan) {
|
||||
res.push(decl.nameSpan?.toString() ?? null);
|
||||
res.push(decl.valueSpan?.toString() ?? null);
|
||||
}
|
||||
|
||||
this.result.push(res);
|
||||
}
|
||||
|
||||
private _appendContext(ast: html.Node, input: any[]): any[] {
|
||||
if (!this.includeSourceSpan) return input;
|
||||
input.push(ast.sourceSpan.toString());
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,54 @@ describe('HtmlParser', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('let declaration', () => {
|
||||
function parseLet(input: string) {
|
||||
return parser.parse(input, 'TestComp', {tokenizeLet: true});
|
||||
}
|
||||
|
||||
it('should parse a let declaration', () => {
|
||||
expect(humanizeDom(parseLet('@let foo = 123;'))).toEqual([
|
||||
[html.LetDeclaration, 'foo', '123'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse a let declaration that is nested in a parent', () => {
|
||||
expect(humanizeDom(parseLet('@grandparent {@parent {@let foo = 123;}}'))).toEqual([
|
||||
[html.Block, 'grandparent', 0],
|
||||
[html.Block, 'parent', 1],
|
||||
[html.LetDeclaration, 'foo', '123'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the source location of a @let declaration', () => {
|
||||
expect(humanizeDomSourceSpans(parseLet('@let foo = 123 + 456;'))).toEqual([
|
||||
[html.LetDeclaration, 'foo', '123 + 456', '@let foo = 123 + 456', 'foo', '123 + 456'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report an error for an incomplete let declaration', () => {
|
||||
expect(humanizeErrors(parseLet('@let foo =').errors)).toEqual([
|
||||
[
|
||||
'foo',
|
||||
'Incomplete @let declaration "foo". @let declarations must be written as `@let <name> = <value>;`',
|
||||
'0:0',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations of an incomplete let declaration', () => {
|
||||
const parseResult = parseLet('@let foo =');
|
||||
|
||||
// It's expected that errors will be reported for the incomplete declaration,
|
||||
// but we still want to check the spans since they're important even for broken templates.
|
||||
parseResult.errors = [];
|
||||
|
||||
expect(humanizeDomSourceSpans(parseResult)).toEqual([
|
||||
[html.LetDeclaration, 'foo', '', '@let foo =', 'foo =', ''],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source spans', () => {
|
||||
it('should store the location', () => {
|
||||
expect(
|
||||
|
|
@ -1307,6 +1355,7 @@ describe('HtmlParser', () => {
|
|||
html.visitAll(this, block.children);
|
||||
}
|
||||
visitBlockParameter(parameter: html.BlockParameter, context: any) {}
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {}
|
||||
})();
|
||||
|
||||
html.visitAll(visitor, result.rootNodes);
|
||||
|
|
@ -1350,6 +1399,9 @@ describe('HtmlParser', () => {
|
|||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
throw Error('Unexpected');
|
||||
}
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
throw Error('Unexpected');
|
||||
}
|
||||
})();
|
||||
const result = parser.parse('<div id="foo"></div><div id="bar"></div>', 'TestComp');
|
||||
const traversal = html.visitAll(visitor, result.rootNodes);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ class _SerializerVisitor implements html.Visitor {
|
|||
return parameter.expression;
|
||||
}
|
||||
|
||||
visitLetDeclaration(decl: html.LetDeclaration, context: any) {
|
||||
return `@let ${decl.name} = ${decl.value};`;
|
||||
}
|
||||
|
||||
private _visitAll(nodes: html.Node[], separator = '', prefix = ''): string {
|
||||
return nodes.length > 0 ? prefix + nodes.map((a) => a.visit(this, null)).join(separator) : '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Element,
|
||||
Expansion,
|
||||
ExpansionCase,
|
||||
LetDeclaration,
|
||||
Text,
|
||||
Visitor,
|
||||
} from '@angular/compiler';
|
||||
|
|
@ -31,4 +32,5 @@ export class BaseVisitor implements Visitor {
|
|||
visitExpansionCase(_expansionCase: ExpansionCase, _context: any): any {}
|
||||
visitBlock(_block: Block, _context: any) {}
|
||||
visitBlockParameter(_parameter: BlockParameter, _context: any) {}
|
||||
visitLetDeclaration(_decl: LetDeclaration, _context: any) {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue