Skip to content

Commit 539717f

Browse files
crisbetothePunderWoman
authored andcommitted
feat(core): support regular expressions in templates (#63887)
Updates the template syntax to support inline regular expressions. PR Close #63887
1 parent c1559ec commit 539717f

File tree

8 files changed

+138
-0
lines changed

8 files changed

+138
-0
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ParenthesizedExpression,
2727
PrefixNot,
2828
PropertyRead,
29+
RegularExpressionLiteral,
2930
SafeCall,
3031
SafeKeyedRead,
3132
SafePropertyRead,
@@ -199,6 +200,10 @@ class AstTranslator implements AstVisitor {
199200
throw new Error('Method not implemented.');
200201
}
201202

203+
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
204+
throw new Error('TODO');
205+
}
206+
202207
visitInterpolation(ast: Interpolation): ts.Expression {
203208
// Build up a chain of binary + operations to simulate the string concatenation of the
204209
// interpolation's expressions. The chain is started using an actual string literal to ensure
@@ -603,4 +608,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
603608
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any) {
604609
return ast.expression.visit(this);
605610
}
611+
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
612+
return false;
613+
}
606614
}

packages/compiler/src/expression_parser/ast.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,21 @@ export class ParenthesizedExpression extends AST {
488488
}
489489
}
490490

491+
export class RegularExpressionLiteral extends AST {
492+
constructor(
493+
span: ParseSpan,
494+
sourceSpan: AbsoluteSourceSpan,
495+
readonly body: string,
496+
readonly flags: string | null,
497+
) {
498+
super(span, sourceSpan);
499+
}
500+
501+
override visit(visitor: AstVisitor, context?: any) {
502+
return visitor.visitRegularExpressionLiteral(this, context);
503+
}
504+
}
505+
491506
/**
492507
* Records the absolute position of a text span in a source file, where `start` and `end` are the
493508
* starting and ending byte offsets, respectively, of the text span in a source file.
@@ -617,6 +632,7 @@ export interface AstVisitor {
617632
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any): any;
618633
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral, context: any): any;
619634
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any): any;
635+
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): any;
620636
visitASTWithSource?(ast: ASTWithSource, context: any): any;
621637
/**
622638
* This function is optionally defined to allow classes that implement this
@@ -719,6 +735,7 @@ export class RecursiveAstVisitor implements AstVisitor {
719735
visitParenthesizedExpression(ast: ParenthesizedExpression, context: any) {
720736
this.visit(ast.expression, context);
721737
}
738+
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {}
722739
// This is not part of the AstVisitor interface, just a helper method
723740
visitAll(asts: AST[], context: any): any {
724741
for (const ast of asts) {

packages/compiler/src/expression_parser/parser.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
PrefixNot,
4141
PropertyRead,
4242
RecursiveAstVisitor,
43+
RegularExpressionLiteral,
4344
SafeCall,
4445
SafeKeyedRead,
4546
SafePropertyRead,
@@ -581,6 +582,9 @@ enum ParseContextFlags {
581582
Writable = 1,
582583
}
583584

585+
/** Possible flags that can be used in a regex literal. */
586+
const SUPPORTED_REGEX_FLAGS = new Set(['d', 'g', 'i', 'm', 's', 'u', 'v', 'y']);
587+
584588
class _ParseAST {
585589
private rparensExpected = 0;
586590
private rbracketsExpected = 0;
@@ -1178,6 +1182,8 @@ class _ParseAST {
11781182
} else if (this.next.isPrivateIdentifier()) {
11791183
this._reportErrorForPrivateIdentifier(this.next, null);
11801184
return new EmptyExpr(this.span(start), this.sourceSpan(start));
1185+
} else if (this.next.isRegExpBody()) {
1186+
return this.parseRegularExpressionLiteral();
11811187
} else if (this.index >= this.tokens.length) {
11821188
this.error(`Unexpected end of expression: ${this.input}`);
11831189
return new EmptyExpr(this.span(start), this.sourceSpan(start));
@@ -1618,6 +1624,49 @@ class _ParseAST {
16181624
return new TemplateLiteral(this.span(start), this.sourceSpan(start), elements, expressions);
16191625
}
16201626

1627+
private parseRegularExpressionLiteral() {
1628+
const bodyToken = this.next;
1629+
this.advance();
1630+
1631+
if (!bodyToken.isRegExpBody()) {
1632+
return new EmptyExpr(this.span(this.inputIndex), this.sourceSpan(this.inputIndex));
1633+
}
1634+
1635+
let flagsToken: Token | null = null;
1636+
1637+
if (this.next.isRegExpFlags()) {
1638+
flagsToken = this.next;
1639+
this.advance();
1640+
const seenFlags = new Set<string>();
1641+
1642+
for (let i = 0; i < flagsToken.strValue.length; i++) {
1643+
const char = flagsToken.strValue[i];
1644+
1645+
if (!SUPPORTED_REGEX_FLAGS.has(char)) {
1646+
this.error(
1647+
`Unsupported regular expression flag "${char}". The supported flags are: ` +
1648+
Array.from(SUPPORTED_REGEX_FLAGS, (f) => `"${f}"`).join(', '),
1649+
flagsToken.index + i,
1650+
);
1651+
} else if (seenFlags.has(char)) {
1652+
this.error(`Duplicate regular expression flag "${char}"`, flagsToken.index + i);
1653+
} else {
1654+
seenFlags.add(char);
1655+
}
1656+
}
1657+
}
1658+
1659+
const start = bodyToken.index;
1660+
const end = flagsToken ? flagsToken.end : bodyToken.end;
1661+
1662+
return new RegularExpressionLiteral(
1663+
this.span(start, end),
1664+
this.sourceSpan(start, end),
1665+
bodyToken.strValue,
1666+
flagsToken ? flagsToken.strValue : null,
1667+
);
1668+
}
1669+
16211670
/**
16221671
* Consume the optional statement terminator: semicolon or comma.
16231672
*/

packages/compiler/src/expression_parser/serializer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ class SerializeExpressionVisitor implements expr.AstVisitor {
129129
return `void ${ast.expression.visit(this, context)}`;
130130
}
131131

132+
visitRegularExpressionLiteral(ast: expr.RegularExpressionLiteral, context: any) {
133+
return `/${ast.body}/${ast.flags || ''}`;
134+
}
135+
132136
visitASTWithSource(ast: expr.ASTWithSource, context: any): string {
133137
return ast.ast.visit(this, context);
134138
}

packages/compiler/test/expression_parser/parser_spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,36 @@ describe('parser', () => {
510510
checkBinding('typeof `hello ${name}!`');
511511
});
512512
});
513+
514+
describe('regular expression literals', () => {
515+
it('should parse a regular expression literal without flags', () => {
516+
checkBinding('/abc/');
517+
checkBinding('/[a/]$/');
518+
checkBinding('/a\\w+/');
519+
checkBinding('/^http:\\/\\/foo\\.bar/');
520+
});
521+
522+
it('should parse a regular expression literal with flags', () => {
523+
checkBinding('/abc/g');
524+
checkBinding('/[a/]$/gi');
525+
checkBinding('/a\\w+/gim');
526+
checkBinding('/^http:\\/\\/foo\\.bar/i');
527+
});
528+
529+
it('should parse a regular expression that is a part of other expressions', () => {
530+
checkBinding('/abc/.test("foo")');
531+
checkBinding('"foo".match(/(abc)/)[1].toUpperCase()');
532+
checkBinding('/abc/.test("foo") && something || somethingElse');
533+
});
534+
535+
it('should report invalid regular expression flag', () => {
536+
expectBindingError('"foo".match(/abc/O)', 'Unsupported regular expression flag "O"');
537+
});
538+
539+
it('should report duplicated regular expression flags', () => {
540+
expectBindingError('"foo".match(/abc/gig)', 'Duplicate regular expression flag "g"');
541+
});
542+
});
513543
});
514544

515545
describe('parse spans', () => {
@@ -691,6 +721,22 @@ describe('parser', () => {
691721
expect(unparseWithSpan(parseBinding(input))).toContain([jasmine.any(String), input]);
692722
}
693723
});
724+
725+
it('should record span for a regex without flags', () => {
726+
const ast = parseBinding('/^http:\\/\\/foo\\.bar/');
727+
expect(unparseWithSpan(ast)).toContain([
728+
'/^http:\\/\\/foo\\.bar/',
729+
'/^http:\\/\\/foo\\.bar/',
730+
]);
731+
});
732+
733+
it('should record span for a regex with flags', () => {
734+
const ast = parseBinding('/^http:\\/\\/foo\\.bar/gim');
735+
expect(unparseWithSpan(ast)).toContain([
736+
'/^http:\\/\\/foo\\.bar/gim',
737+
'/^http:\\/\\/foo\\.bar/gim',
738+
]);
739+
});
694740
});
695741

696742
describe('general error handling', () => {

packages/compiler/test/expression_parser/utils/unparser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
PrefixNot,
2727
PropertyRead,
2828
RecursiveAstVisitor,
29+
RegularExpressionLiteral,
2930
SafeCall,
3031
SafeKeyedRead,
3132
SafePropertyRead,
@@ -237,6 +238,10 @@ class Unparser implements AstVisitor {
237238
this._expression += ')';
238239
}
239240

241+
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
242+
this._expression += `/${ast.body}/${ast.flags || ''}`;
243+
}
244+
240245
private _visit(ast: AST) {
241246
ast.visit(this);
242247
}

packages/compiler/test/expression_parser/utils/validator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
PrefixNot,
2525
PropertyRead,
2626
RecursiveAstVisitor,
27+
RegularExpressionLiteral,
2728
SafeCall,
2829
SafeKeyedRead,
2930
SafePropertyRead,
@@ -155,6 +156,10 @@ class ASTValidator extends RecursiveAstVisitor {
155156
override visitParenthesizedExpression(ast: ParenthesizedExpression, context: any): void {
156157
this.validate(ast, () => super.visitParenthesizedExpression(ast, context));
157158
}
159+
160+
override visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): void {
161+
this.validate(ast, () => super.visitRegularExpressionLiteral(ast, context));
162+
}
158163
}
159164

160165
function inSpan(span: ParseSpan, parentSpan: ParseSpan | undefined): parentSpan is ParseSpan {

packages/compiler/test/render3/util/expression.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit
127127
this.recordAst(ast);
128128
super.visitParenthesizedExpression(ast, null);
129129
}
130+
override visitRegularExpressionLiteral(ast: e.RegularExpressionLiteral, context: any): void {
131+
this.recordAst(ast);
132+
super.visitRegularExpressionLiteral(ast, null);
133+
}
130134

131135
visitTemplate(ast: t.Template) {
132136
t.visitAll(this, ast.directives);

0 commit comments

Comments
 (0)