From d2f3f00dc3383d6bc8f5675568cc878080465427 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sun, 13 Oct 2024 19:23:33 +0200 Subject: [PATCH 1/4] feat(compiler): add support for the `typeof` keyword in template expressions. This commit adds the support for `typeof` in template expressions like interpolation, bindings, control flow blocks etc. --- .../src/ngtsc/typecheck/src/expression.ts | 11 +++++++ .../r3_view_compiler/GOLDEN_PARTIAL.js | 2 ++ .../test_cases/r3_view_compiler/operators.ts | 1 + .../r3_view_compiler/operators_template.js | 2 +- .../compiler/src/expression_parser/ast.ts | 29 ++++++++++++++++++ .../compiler/src/expression_parser/lexer.ts | 30 +++++++++++-------- .../compiler/src/expression_parser/parser.ts | 6 ++++ .../src/template/pipeline/src/ingest.ts | 2 ++ .../test/expression_parser/lexer_spec.ts | 6 ++++ .../test/expression_parser/parser_spec.ts | 4 +++ .../test/expression_parser/utils/unparser.ts | 5 ++++ .../test/expression_parser/utils/validator.ts | 5 ++++ .../compiler/test/render3/util/expression.ts | 4 +++ 13 files changed, 93 insertions(+), 14 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 884601439200..3c55db3cfe2e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -25,6 +25,7 @@ import { LiteralPrimitive, NonNullAssert, PrefixNot, + PrefixTypeof, PropertyRead, PropertyWrite, SafeCall, @@ -275,6 +276,13 @@ class AstTranslator implements AstVisitor { return node; } + visitPrefixTypeof(ast: PrefixTypeof): ts.Expression { + const expression = wrapForDiagnostics(this.translate(ast.expression)); + const node = ts.factory.createTypeOfExpression(expression); + addParseSpanInfo(node, ast.sourceSpan); + return node; + } + visitPropertyRead(ast: PropertyRead): ts.Expression { // This is a normal property read - convert the receiver to an expression and emit the correct // TypeScript expression to read the property. @@ -541,6 +549,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor { visitPrefixNot(ast: PrefixNot): boolean { return ast.expression.visit(this); } + visitPrefixTypeof(ast: PrefixNot): boolean { + return ast.expression.visit(this); + } visitNonNullAssert(ast: PrefixNot): boolean { return ast.expression.visit(this); } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js index 0c42f7865a53..9470f2014c45 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js @@ -79,6 +79,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0- {{ 1 + 2 }} {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} + {{ typeof {} === 'object' }} `, isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ type: Component, @@ -87,6 +88,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE {{ 1 + 2 }} {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} + {{ typeof {} === 'object' }} `, standalone: false }] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts index ddbc12641050..f536ba8740df 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts @@ -5,6 +5,7 @@ import {Component, NgModule} from '@angular/core'; {{ 1 + 2 }} {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} + {{ typeof {} === 'object' }} `, standalone: false }) diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js index c1a3d7fb87d6..22aec426c6dd 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js @@ -2,7 +2,7 @@ template: function MyApp_Template(rf, $ctx$) { if (rf & 1) { $i0$.ɵɵtext(0); } if (rf & 2) { - i0.ɵɵtextInterpolate3(" ", 1 + 2, " ", 1 % 2 + 3 / 4 * 5, " ", +1, "\n"); + i0.ɵɵtextInterpolate4(" ", 1 + 2, " ", 1 % 2 + 3 / 4 * 5, " ", +1, " ", typeof i0.ɵɵpureFunction0(4, _c0) === "object","\n"); } } \ No newline at end of file diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index f53f594b2c97..c84010e5943a 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -373,6 +373,19 @@ export class PrefixNot extends AST { } } +export class PrefixTypeof extends AST { + constructor( + span: ParseSpan, + sourceSpan: AbsoluteSourceSpan, + public expression: AST, + ) { + super(span, sourceSpan); + } + override visit(visitor: AstVisitor, context: any = null): any { + return visitor.visitPrefixTypeof(this, context); + } +} + export class NonNullAssert extends AST { constructor( span: ParseSpan, @@ -534,6 +547,7 @@ export interface AstVisitor { visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any; visitPipe(ast: BindingPipe, context: any): any; visitPrefixNot(ast: PrefixNot, context: any): any; + visitPrefixTypeof(ast: PrefixTypeof, context: any): any; visitNonNullAssert(ast: NonNullAssert, context: any): any; visitPropertyRead(ast: PropertyRead, context: any): any; visitPropertyWrite(ast: PropertyWrite, context: any): any; @@ -601,6 +615,9 @@ export class RecursiveAstVisitor implements AstVisitor { visitPrefixNot(ast: PrefixNot, context: any): any { this.visit(ast.expression, context); } + visitPrefixTypeof(ast: PrefixTypeof, context: any) { + this.visit(ast.expression, context); + } visitNonNullAssert(ast: NonNullAssert, context: any): any { this.visit(ast.expression, context); } @@ -715,6 +732,10 @@ export class AstTransformer implements AstVisitor { return new PrefixNot(ast.span, ast.sourceSpan, ast.expression.visit(this)); } + visitPrefixTypeof(ast: PrefixNot, context: any): AST { + return new PrefixTypeof(ast.span, ast.sourceSpan, ast.expression.visit(this)); + } + visitNonNullAssert(ast: NonNullAssert, context: any): AST { return new NonNullAssert(ast.span, ast.sourceSpan, ast.expression.visit(this)); } @@ -891,6 +912,14 @@ export class AstMemoryEfficientTransformer implements AstVisitor { return ast; } + visitPrefixTypeof(ast: PrefixTypeof, context: any): AST { + const expression = ast.expression.visit(this); + if (expression !== ast.expression) { + return new PrefixTypeof(ast.span, ast.sourceSpan, expression); + } + return ast; + } + visitNonNullAssert(ast: NonNullAssert, context: any): AST { const expression = ast.expression.visit(this); if (expression !== ast.expression) { diff --git a/packages/compiler/src/expression_parser/lexer.ts b/packages/compiler/src/expression_parser/lexer.ts index 5114ce58229c..8445e6ee0adc 100644 --- a/packages/compiler/src/expression_parser/lexer.ts +++ b/packages/compiler/src/expression_parser/lexer.ts @@ -19,7 +19,19 @@ export enum TokenType { Error, } -const KEYWORDS = ['var', 'let', 'as', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this']; +const KEYWORDS = [ + 'var', + 'let', + 'as', + 'null', + 'undefined', + 'true', + 'false', + 'if', + 'else', + 'this', + 'typeof', +]; export class Lexer { tokenize(text: string): Token[] { @@ -99,6 +111,10 @@ export class Token { return this.type == TokenType.Keyword && this.strValue == 'this'; } + isKeywordTypeof(): boolean { + return this.type == TokenType.Keyword && this.strValue == 'typeof'; + } + isError(): boolean { return this.type == TokenType.Error; } @@ -436,18 +452,6 @@ function isIdentifierStart(code: number): boolean { ); } -export function isIdentifier(input: string): boolean { - if (input.length == 0) return false; - const scanner = new _Scanner(input); - if (!isIdentifierStart(scanner.peek)) return false; - scanner.advance(); - while (scanner.peek !== chars.$EOF) { - if (!isIdentifierPart(scanner.peek)) return false; - scanner.advance(); - } - return true; -} - function isIdentifierPart(code: number): boolean { return chars.isAsciiLetter(code) || chars.isDigit(code) || code == chars.$_ || code == chars.$$; } diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 06898cfb5353..26f224c8c502 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -37,6 +37,7 @@ import { ParserError, ParseSpan, PrefixNot, + PrefixTypeof, PropertyRead, PropertyWrite, RecursiveAstVisitor, @@ -960,6 +961,11 @@ class _ParseAST { result = this.parsePrefix(); return new PrefixNot(this.span(start), this.sourceSpan(start), result); } + } else if (this.next.type == TokenType.Keyword && this.next.strValue === 'typeof') { + this.advance(); + const start = this.inputIndex; + let result = this.parsePrefix(); + return new PrefixTypeof(this.span(start), this.sourceSpan(start), result); } return this.parseCallChain(); } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 5c51a7623dca..7caed63f5930 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -1150,6 +1150,8 @@ function convertAst( convertAst(ast.expression, job, baseSourceSpan), convertSourceSpan(ast.span, baseSourceSpan), ); + } else if (ast instanceof e.PrefixTypeof) { + return o.typeofExpr(convertAst(ast.expression, job, baseSourceSpan)); } else { throw new Error( `Unhandled expression type "${ast.constructor.name}" in file "${baseSourceSpan?.start.file.url}"`, diff --git a/packages/compiler/test/expression_parser/lexer_spec.ts b/packages/compiler/test/expression_parser/lexer_spec.ts index 0a39cc465f04..7d4223cc0b5a 100644 --- a/packages/compiler/test/expression_parser/lexer_spec.ts +++ b/packages/compiler/test/expression_parser/lexer_spec.ts @@ -185,6 +185,12 @@ describe('lexer', () => { expect(tokens[0].isKeywordUndefined()).toBe(true); }); + it('should tokenize typeof', () => { + const tokens: Token[] = lex('typeof'); + expectKeywordToken(tokens[0], 0, 6, 'typeof'); + expect(tokens[0].isKeywordTypeof()).toBe(true); + }); + it('should ignore whitespace', () => { const tokens: Token[] = lex('a \t \n \r b'); expectIdentifierToken(tokens[0], 0, 1, 'a'); diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 25b1382bf88e..aa9d447a775a 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -98,6 +98,10 @@ describe('parser', () => { checkAction('null ?? undefined ?? 0'); }); + it('should parse typeof expression', () => { + checkAction(`typeof {} === "object"`); + }); + it('should parse grouped expressions', () => { checkAction('(1 + 2) * 3', '1 + 2 * 3'); }); diff --git a/packages/compiler/test/expression_parser/utils/unparser.ts b/packages/compiler/test/expression_parser/utils/unparser.ts index f6ebda8a5ace..f8a2f892cec3 100644 --- a/packages/compiler/test/expression_parser/utils/unparser.ts +++ b/packages/compiler/test/expression_parser/utils/unparser.ts @@ -192,6 +192,11 @@ class Unparser implements AstVisitor { this._visit(ast.expression); } + visitPrefixTypeof(ast: PrefixNot, context: any) { + this._expression += 'typeof '; + this._visit(ast.expression); + } + visitNonNullAssert(ast: NonNullAssert, context: any) { this._visit(ast.expression); this._expression += '!'; diff --git a/packages/compiler/test/expression_parser/utils/validator.ts b/packages/compiler/test/expression_parser/utils/validator.ts index ea5571459690..f75dec21886b 100644 --- a/packages/compiler/test/expression_parser/utils/validator.ts +++ b/packages/compiler/test/expression_parser/utils/validator.ts @@ -22,6 +22,7 @@ import { LiteralPrimitive, ParseSpan, PrefixNot, + PrefixTypeof, PropertyRead, PropertyWrite, RecursiveAstVisitor, @@ -112,6 +113,10 @@ class ASTValidator extends RecursiveAstVisitor { this.validate(ast, () => super.visitPrefixNot(ast, context)); } + override visitPrefixTypeof(ast: PrefixTypeof, context: any): any { + this.validate(ast, () => super.visitPrefixTypeof(ast, context)); + } + override visitPropertyRead(ast: PropertyRead, context: any): any { this.validate(ast, () => super.visitPropertyRead(ast, context)); } diff --git a/packages/compiler/test/render3/util/expression.ts b/packages/compiler/test/render3/util/expression.ts index 1dcbf4181802..1fd8fb0b6c67 100644 --- a/packages/compiler/test/render3/util/expression.ts +++ b/packages/compiler/test/render3/util/expression.ts @@ -87,6 +87,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit this.recordAst(ast); super.visitPrefixNot(ast, null); } + override visitPrefixTypeof(ast: e.PrefixTypeof) { + this.recordAst(ast); + super.visitPrefixTypeof(ast, null); + } override visitPropertyRead(ast: e.PropertyRead) { this.recordAst(ast); super.visitPropertyRead(ast, null); From 992769fdead4f727c59cf7fcdf3a0ee7c4b67cef Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Mon, 14 Oct 2024 05:23:06 -0600 Subject: [PATCH 2/4] fixup! feat(compiler): add support for the `typeof` keyword in template expressions. --- .../src/ngtsc/typecheck/src/expression.ts | 6 +++--- packages/compiler/src/expression_parser/ast.ts | 16 ++++++++-------- packages/compiler/src/expression_parser/lexer.ts | 2 +- .../compiler/src/expression_parser/parser.ts | 4 ++-- .../compiler/src/template/pipeline/src/ingest.ts | 2 +- .../test/expression_parser/parser_spec.ts | 1 + .../test/expression_parser/utils/unparser.ts | 3 ++- .../test/expression_parser/utils/validator.ts | 6 +++--- .../compiler/test/render3/util/expression.ts | 4 ++-- 9 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 3c55db3cfe2e..f6ebcde33b35 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -25,7 +25,7 @@ import { LiteralPrimitive, NonNullAssert, PrefixNot, - PrefixTypeof, + TypeofExpression, PropertyRead, PropertyWrite, SafeCall, @@ -276,7 +276,7 @@ class AstTranslator implements AstVisitor { return node; } - visitPrefixTypeof(ast: PrefixTypeof): ts.Expression { + visitTypeofExpresion(ast: TypeofExpression): ts.Expression { const expression = wrapForDiagnostics(this.translate(ast.expression)); const node = ts.factory.createTypeOfExpression(expression); addParseSpanInfo(node, ast.sourceSpan); @@ -549,7 +549,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor { visitPrefixNot(ast: PrefixNot): boolean { return ast.expression.visit(this); } - visitPrefixTypeof(ast: PrefixNot): boolean { + visitTypeofExpresion(ast: PrefixNot): boolean { return ast.expression.visit(this); } visitNonNullAssert(ast: PrefixNot): boolean { diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index c84010e5943a..9c53ea2b4d9c 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -373,7 +373,7 @@ export class PrefixNot extends AST { } } -export class PrefixTypeof extends AST { +export class TypeofExpression extends AST { constructor( span: ParseSpan, sourceSpan: AbsoluteSourceSpan, @@ -382,7 +382,7 @@ export class PrefixTypeof extends AST { super(span, sourceSpan); } override visit(visitor: AstVisitor, context: any = null): any { - return visitor.visitPrefixTypeof(this, context); + return visitor.visitTypeofExpresion(this, context); } } @@ -547,7 +547,7 @@ export interface AstVisitor { visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any; visitPipe(ast: BindingPipe, context: any): any; visitPrefixNot(ast: PrefixNot, context: any): any; - visitPrefixTypeof(ast: PrefixTypeof, context: any): any; + visitTypeofExpresion(ast: TypeofExpression, context: any): any; visitNonNullAssert(ast: NonNullAssert, context: any): any; visitPropertyRead(ast: PropertyRead, context: any): any; visitPropertyWrite(ast: PropertyWrite, context: any): any; @@ -615,7 +615,7 @@ export class RecursiveAstVisitor implements AstVisitor { visitPrefixNot(ast: PrefixNot, context: any): any { this.visit(ast.expression, context); } - visitPrefixTypeof(ast: PrefixTypeof, context: any) { + visitTypeofExpresion(ast: TypeofExpression, context: any) { this.visit(ast.expression, context); } visitNonNullAssert(ast: NonNullAssert, context: any): any { @@ -732,8 +732,8 @@ export class AstTransformer implements AstVisitor { return new PrefixNot(ast.span, ast.sourceSpan, ast.expression.visit(this)); } - visitPrefixTypeof(ast: PrefixNot, context: any): AST { - return new PrefixTypeof(ast.span, ast.sourceSpan, ast.expression.visit(this)); + visitTypeofExpresion(ast: PrefixNot, context: any): AST { + return new TypeofExpression(ast.span, ast.sourceSpan, ast.expression.visit(this)); } visitNonNullAssert(ast: NonNullAssert, context: any): AST { @@ -912,10 +912,10 @@ export class AstMemoryEfficientTransformer implements AstVisitor { return ast; } - visitPrefixTypeof(ast: PrefixTypeof, context: any): AST { + visitTypeofExpresion(ast: TypeofExpression, context: any): AST { const expression = ast.expression.visit(this); if (expression !== ast.expression) { - return new PrefixTypeof(ast.span, ast.sourceSpan, expression); + return new TypeofExpression(ast.span, ast.sourceSpan, expression); } return ast; } diff --git a/packages/compiler/src/expression_parser/lexer.ts b/packages/compiler/src/expression_parser/lexer.ts index 8445e6ee0adc..25cd2b3cb7f6 100644 --- a/packages/compiler/src/expression_parser/lexer.ts +++ b/packages/compiler/src/expression_parser/lexer.ts @@ -112,7 +112,7 @@ export class Token { } isKeywordTypeof(): boolean { - return this.type == TokenType.Keyword && this.strValue == 'typeof'; + return this.type === TokenType.Keyword && this.strValue === 'typeof'; } isError(): boolean { diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 26f224c8c502..a0c6b047711c 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -37,7 +37,7 @@ import { ParserError, ParseSpan, PrefixNot, - PrefixTypeof, + TypeofExpression, PropertyRead, PropertyWrite, RecursiveAstVisitor, @@ -965,7 +965,7 @@ class _ParseAST { this.advance(); const start = this.inputIndex; let result = this.parsePrefix(); - return new PrefixTypeof(this.span(start), this.sourceSpan(start), result); + return new TypeofExpression(this.span(start), this.sourceSpan(start), result); } return this.parseCallChain(); } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 7caed63f5930..90b56a117ec8 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -1150,7 +1150,7 @@ function convertAst( convertAst(ast.expression, job, baseSourceSpan), convertSourceSpan(ast.span, baseSourceSpan), ); - } else if (ast instanceof e.PrefixTypeof) { + } else if (ast instanceof e.TypeofExpression) { return o.typeofExpr(convertAst(ast.expression, job, baseSourceSpan)); } else { throw new Error( diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index aa9d447a775a..5a2e296f0595 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -100,6 +100,7 @@ describe('parser', () => { it('should parse typeof expression', () => { checkAction(`typeof {} === "object"`); + checkAction('(!(typeof {} === "number"))', '!typeof {} === "number"'); }); it('should parse grouped expressions', () => { diff --git a/packages/compiler/test/expression_parser/utils/unparser.ts b/packages/compiler/test/expression_parser/utils/unparser.ts index f8a2f892cec3..4e6fbab9d977 100644 --- a/packages/compiler/test/expression_parser/utils/unparser.ts +++ b/packages/compiler/test/expression_parser/utils/unparser.ts @@ -24,6 +24,7 @@ import { LiteralPrimitive, NonNullAssert, PrefixNot, + TypeofExpression, PropertyRead, PropertyWrite, RecursiveAstVisitor, @@ -192,7 +193,7 @@ class Unparser implements AstVisitor { this._visit(ast.expression); } - visitPrefixTypeof(ast: PrefixNot, context: any) { + visitTypeofExpresion(ast: TypeofExpression, context: any) { this._expression += 'typeof '; this._visit(ast.expression); } diff --git a/packages/compiler/test/expression_parser/utils/validator.ts b/packages/compiler/test/expression_parser/utils/validator.ts index f75dec21886b..4f915a552d3a 100644 --- a/packages/compiler/test/expression_parser/utils/validator.ts +++ b/packages/compiler/test/expression_parser/utils/validator.ts @@ -22,7 +22,7 @@ import { LiteralPrimitive, ParseSpan, PrefixNot, - PrefixTypeof, + TypeofExpression, PropertyRead, PropertyWrite, RecursiveAstVisitor, @@ -113,8 +113,8 @@ class ASTValidator extends RecursiveAstVisitor { this.validate(ast, () => super.visitPrefixNot(ast, context)); } - override visitPrefixTypeof(ast: PrefixTypeof, context: any): any { - this.validate(ast, () => super.visitPrefixTypeof(ast, context)); + override visitTypeofExpresion(ast: TypeofExpression, context: any): any { + this.validate(ast, () => super.visitTypeofExpresion(ast, context)); } override visitPropertyRead(ast: PropertyRead, context: any): any { diff --git a/packages/compiler/test/render3/util/expression.ts b/packages/compiler/test/render3/util/expression.ts index 1fd8fb0b6c67..69bb4c3dd943 100644 --- a/packages/compiler/test/render3/util/expression.ts +++ b/packages/compiler/test/render3/util/expression.ts @@ -87,9 +87,9 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit this.recordAst(ast); super.visitPrefixNot(ast, null); } - override visitPrefixTypeof(ast: e.PrefixTypeof) { + override visitTypeofExpresion(ast: e.TypeofExpression) { this.recordAst(ast); - super.visitPrefixTypeof(ast, null); + super.visitTypeofExpresion(ast, null); } override visitPropertyRead(ast: e.PropertyRead) { this.recordAst(ast); From 1780abd4ed18b543bac4dab89c52d08364fd7c25 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Mon, 14 Oct 2024 20:14:45 -0600 Subject: [PATCH 3/4] fixup! feat(compiler): add support for the `typeof` keyword in template expressions. --- .../typecheck/test/type_check_block_spec.ts | 8 ++++ .../r3_view_compiler/GOLDEN_PARTIAL.js | 2 + .../test_cases/r3_view_compiler/operators.ts | 1 + .../r3_view_compiler/operators_template.js | 2 +- .../test/ngtsc/template_typecheck_spec.ts | 41 +++++++++++++++++++ .../compiler/src/expression_parser/ast.ts | 2 +- .../compiler/src/expression_parser/parser.ts | 2 +- .../test/acceptance/control_flow_if_spec.ts | 24 +++++++++++ 8 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 4eb9279dfec3..e5390cd2514c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -61,6 +61,14 @@ describe('type check blocks', () => { ); }); + it('should handle typeof expressions', () => { + expect(tcb('{{typeof a}}')).toContain('typeof (((this).a))'); + expect(tcb('{{!(typeof a)}}')).toContain('!(typeof (((this).a)))'); + expect(tcb('{{!(typeof a === "object")}}')).toContain( + '!((typeof (((this).a))) === ("object"))', + ); + }); + it('should handle attribute values for directive inputs', () => { const TEMPLATE = `
`; const DIRECTIVES: TestDeclaration[] = [ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js index 9470f2014c45..a969e57c4891 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js @@ -80,6 +80,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0- {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} {{ typeof {} === 'object' }} + {{ !(typeof {} === 'object') }} `, isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ type: Component, @@ -89,6 +90,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} {{ typeof {} === 'object' }} + {{ !(typeof {} === 'object') }} `, standalone: false }] diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts index f536ba8740df..ba2aaff9a684 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts @@ -6,6 +6,7 @@ import {Component, NgModule} from '@angular/core'; {{ (1 % 2) + 3 / 4 * 5 }} {{ +1 }} {{ typeof {} === 'object' }} + {{ !(typeof {} === 'object') }} `, standalone: false }) diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js index 22aec426c6dd..a3faea8ec2ea 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js @@ -2,7 +2,7 @@ template: function MyApp_Template(rf, $ctx$) { if (rf & 1) { $i0$.ɵɵtext(0); } if (rf & 2) { - i0.ɵɵtextInterpolate4(" ", 1 + 2, " ", 1 % 2 + 3 / 4 * 5, " ", +1, " ", typeof i0.ɵɵpureFunction0(4, _c0) === "object","\n"); + i0.ɵɵtextInterpolate5(" ", 1 + 2, " ", 1 % 2 + 3 / 4 * 5, " ", +1, " ", typeof i0.ɵɵpureFunction0(5, _c0) === "object", " ", !(typeof i0.ɵɵpureFunction0(6, _c0) === "object"), "\n"); } } \ No newline at end of file diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 44066881278d..4ab66ef990e0 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -700,6 +700,47 @@ runInEachFileSystem(() => { expect(diags[0].messageText).toContain(`Property 'input' does not exist on type 'TestCmp'.`); }); + it('should error on non valid typeof expressions', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + template: \` {{typeof {} === 'foobar'}} \`, + }) + class TestCmp { + } + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain(`This comparison appears to be unintentional`); + }); + + it('should error on misused logical not in typeof expressions', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + // should be !(typeof {} === 'object') + template: \` {{!typeof {} === 'object'}} \`, + }) + class TestCmp { + } + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain(`This comparison appears to be unintentional`); + }); + describe('strictInputTypes', () => { beforeEach(() => { env.write( diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 9c53ea2b4d9c..89259539cb48 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -732,7 +732,7 @@ export class AstTransformer implements AstVisitor { return new PrefixNot(ast.span, ast.sourceSpan, ast.expression.visit(this)); } - visitTypeofExpresion(ast: PrefixNot, context: any): AST { + visitTypeofExpresion(ast: TypeofExpression, context: any): AST { return new TypeofExpression(ast.span, ast.sourceSpan, ast.expression.visit(this)); } diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index a0c6b047711c..429626ff5ec2 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -961,7 +961,7 @@ class _ParseAST { result = this.parsePrefix(); return new PrefixNot(this.span(start), this.sourceSpan(start), result); } - } else if (this.next.type == TokenType.Keyword && this.next.strValue === 'typeof') { + } else if (this.next.isKeywordTypeof()) { this.advance(); const start = this.inputIndex; let result = this.parsePrefix(); diff --git a/packages/core/test/acceptance/control_flow_if_spec.ts b/packages/core/test/acceptance/control_flow_if_spec.ts index 69be9e6ef34e..91857ea3aef7 100644 --- a/packages/core/test/acceptance/control_flow_if_spec.ts +++ b/packages/core/test/acceptance/control_flow_if_spec.ts @@ -277,6 +277,30 @@ describe('control flow - if', () => { expect(fixture.nativeElement.textContent).toBe('Something'); }); + it('should support a condition with the a typeof expression', () => { + @Component({ + standalone: true, + template: ` + @if (typeof value === 'string') { + {{value.length}} + } @else { + {{value}} + } + `, + }) + class TestComponent { + value: string | number = 'string'; + } + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent.trim()).toBe('6'); + + fixture.componentInstance.value = 42; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent.trim()).toBe('42'); + }); + describe('content projection', () => { it('should project an @if with a single root node into the root node slot', () => { @Component({ From 65e0fb9382066327e7316150fc7c902f9ebd2da5 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Tue, 15 Oct 2024 06:56:49 -0600 Subject: [PATCH 4/4] fixup! feat(compiler): add support for the `typeof` keyword in template expressions. --- .../r3_view_compiler/GOLDEN_PARTIAL.js | 34 ++++++++++++++++--- .../test_cases/r3_view_compiler/operators.ts | 12 +++++-- .../r3_view_compiler/operators_template.js | 11 +++++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js index a969e57c4891..6cbe7212a943 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js @@ -70,9 +70,12 @@ export declare class TodoModule { /**************************************************************************************************** * PARTIAL FILE: operators.js ****************************************************************************************************/ -import { Component, NgModule } from '@angular/core'; +import { Component, NgModule, Pipe } from '@angular/core'; import * as i0 from "@angular/core"; export class MyApp { + constructor() { + this.foo = { bar: 'baz' }; + } } MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` @@ -81,7 +84,9 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0- {{ +1 }} {{ typeof {} === 'object' }} {{ !(typeof {} === 'object') }} -`, isInline: true }); + {{ typeof foo?.bar === 'string' }} + {{ typeof foo?.bar | identity }} +`, isInline: true, dependencies: [{ kind: "pipe", type: i0.forwardRef(() => IdentityPipe), name: "identity" }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ type: Component, args: [{ @@ -91,18 +96,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE {{ +1 }} {{ typeof {} === 'object' }} {{ !(typeof {} === 'object') }} + {{ typeof foo?.bar === 'string' }} + {{ typeof foo?.bar | identity }} `, standalone: false }] }] }); +export class IdentityPipe { + transform(value) { return value; } +} +IdentityPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); +IdentityPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, name: "identity" }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: IdentityPipe, decorators: [{ + type: Pipe, + args: [{ name: 'identity' }] + }] }); export class MyModule { } MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); -MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp, IdentityPipe] }); MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ type: NgModule, - args: [{ declarations: [MyApp] }] + args: [{ declarations: [MyApp, IdentityPipe] }] }] }); /**************************************************************************************************** @@ -110,12 +126,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE ****************************************************************************************************/ import * as i0 from "@angular/core"; export declare class MyApp { + foo: { + bar?: string; + }; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } +export declare class IdentityPipe { + transform(value: any): any; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵpipe: i0.ɵɵPipeDeclaration; +} export declare class MyModule { static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; static ɵinj: i0.ɵɵInjectorDeclaration; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts index ba2aaff9a684..14d210f26e3c 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts @@ -1,4 +1,4 @@ -import {Component, NgModule} from '@angular/core'; +import {Component, NgModule, Pipe} from '@angular/core'; @Component({ template: ` @@ -7,12 +7,20 @@ import {Component, NgModule} from '@angular/core'; {{ +1 }} {{ typeof {} === 'object' }} {{ !(typeof {} === 'object') }} + {{ typeof foo?.bar === 'string' }} + {{ typeof foo?.bar | identity }} `, standalone: false }) export class MyApp { + foo: {bar?: string} = {bar: 'baz'}; } -@NgModule({declarations: [MyApp]}) +@Pipe ({name: 'identity'}) +export class IdentityPipe { + transform(value: any) { return value; } +} + +@NgModule({declarations: [MyApp, IdentityPipe]}) export class MyModule { } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js index a3faea8ec2ea..baa91bab306b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js @@ -1,8 +1,17 @@ template: function MyApp_Template(rf, $ctx$) { if (rf & 1) { $i0$.ɵɵtext(0); + i0.ɵɵpipe(1, "identity"); } if (rf & 2) { - i0.ɵɵtextInterpolate5(" ", 1 + 2, " ", 1 % 2 + 3 / 4 * 5, " ", +1, " ", typeof i0.ɵɵpureFunction0(5, _c0) === "object", " ", !(typeof i0.ɵɵpureFunction0(6, _c0) === "object"), "\n"); + i0.ɵɵtextInterpolate7(" ", + 1 + 2, " ", + 1 % 2 + 3 / 4 * 5, " ", + +1, " ", + typeof i0.ɵɵpureFunction0(9, _c0) === "object", " ", + !(typeof i0.ɵɵpureFunction0(10, _c0) === "object"), " ", + typeof (ctx.foo == null ? null : ctx.foo.bar) === "string", " ", + i0.ɵɵpipeBind1(1, 7, typeof (ctx.foo == null ? null : ctx.foo.bar)), "\n" + ); } } \ No newline at end of file