diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml index d191358d6503..1c3356230272 100644 --- a/.github/actions/prepare-build/action.yml +++ b/.github/actions/prepare-build/action.yml @@ -24,7 +24,7 @@ runs: run: | yarn nx run types:build env: - SKIP_AST_SPEC_REBUILD: true + SKIP_AST_SPEC_REBUILD: false - name: Build if: steps['build-cache'].outputs.cache-hit != 'true' @@ -33,4 +33,4 @@ runs: run: | yarn nx run-many --target=build --parallel --exclude=website --exclude=website-eslint env: - SKIP_AST_SPEC_REBUILD: true + SKIP_AST_SPEC_REBUILD: false diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/1-TSESTree-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/1-TSESTree-AST.shot index 88e028508e34..5e0c9090807d 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/1-TSESTree-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/1-TSESTree-AST.shot @@ -26,7 +26,7 @@ Program { type: "TemplateElement", tail: true, value: { - "cooked": "foo", + "cooked": null, "raw": "foo", }, diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/5-AST-Alignment-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/5-AST-Alignment-AST.shot index cdda4e0f0b5b..a24c8bb9f5d6 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/5-AST-Alignment-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-1/snapshots/5-AST-Alignment-AST.shot @@ -2,4 +2,76 @@ exports[`AST Fixtures > legacy-fixtures > types > template-literal-type-1 > AST Alignment - AST`] Snapshot Diff: -Compared values have no visual difference. +- TSESTree ++ Babel + + Program { + type: 'Program', + body: Array [ + TSTypeAliasDeclaration { + type: 'TSTypeAliasDeclaration', + declare: false, + id: Identifier { + type: 'Identifier', + decorators: Array [], + name: 'T', + optional: false, + + range: [78, 79], + loc: { + start: { column: 5, line: 3 }, + end: { column: 6, line: 3 }, + }, + }, + typeAnnotation: TSLiteralType { + type: 'TSLiteralType', + literal: TemplateLiteral { + type: 'TemplateLiteral', + expressions: Array [], + quasis: Array [ + TemplateElement { + type: 'TemplateElement', + tail: true, + value: Object { +- 'cooked': null, ++ 'cooked': 'foo', + 'raw': 'foo', + }, + + range: [82, 87], + loc: { + start: { column: 9, line: 3 }, + end: { column: 14, line: 3 }, + }, + }, + ], + + range: [82, 87], + loc: { + start: { column: 9, line: 3 }, + end: { column: 14, line: 3 }, + }, + }, + + range: [82, 87], + loc: { + start: { column: 9, line: 3 }, + end: { column: 14, line: 3 }, + }, + }, + + range: [73, 88], + loc: { + start: { column: 0, line: 3 }, + end: { column: 15, line: 3 }, + }, + }, + ], + sourceType: 'script', + + range: [73, 89], + loc: { + start: { column: 0, line: 3 }, + end: { column: 0, line: 4 }, + }, + } diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/1-TSESTree-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/1-TSESTree-AST.shot index 1065df8f8712..5c59dc2dffde 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/1-TSESTree-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/1-TSESTree-AST.shot @@ -23,7 +23,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": "foo", + "cooked": null, "raw": "foo", }, @@ -37,7 +37,7 @@ Program { type: "TemplateElement", tail: true, value: { - "cooked": "", + "cooked": null, "raw": "", }, diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/5-AST-Alignment-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/5-AST-Alignment-AST.shot index dc7e97974018..b337acde0a87 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/5-AST-Alignment-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-2/snapshots/5-AST-Alignment-AST.shot @@ -30,7 +30,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': 'foo', +- 'cooked': null, - 'raw': 'foo', - }, + typeAnnotation: TSLiteralType { @@ -55,7 +55,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: true, - value: Object { -- 'cooked': '', +- 'cooked': null, - 'raw': '', - }, + range: [88, 93], diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/1-TSESTree-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/1-TSESTree-AST.shot index 86bdf5a0561f..7c3b8088d938 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/1-TSESTree-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/1-TSESTree-AST.shot @@ -169,7 +169,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": "", + "cooked": null, "raw": "", }, @@ -183,7 +183,7 @@ Program { type: "TemplateElement", tail: true, value: { - "cooked": " fish", + "cooked": null, "raw": " fish", }, diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/5-AST-Alignment-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/5-AST-Alignment-AST.shot index 5a6cfaa69dbb..d6e6c564df8d 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/5-AST-Alignment-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-3/snapshots/5-AST-Alignment-AST.shot @@ -176,7 +176,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': '', +- 'cooked': null, - 'raw': '', - }, + typeAnnotation: TSLiteralType { @@ -205,7 +205,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: true, - value: Object { -- 'cooked': ' fish', +- 'cooked': null, - 'raw': ' fish', - }, - diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/1-TSESTree-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/1-TSESTree-AST.shot index 5a2aee247757..51ecc6523886 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/1-TSESTree-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/1-TSESTree-AST.shot @@ -23,7 +23,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": "", + "cooked": null, "raw": "", }, @@ -37,7 +37,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": " - ", + "cooked": null, "raw": " - ", }, @@ -51,7 +51,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": " - ", + "cooked": null, "raw": " - ", }, @@ -65,7 +65,7 @@ Program { type: "TemplateElement", tail: false, value: { - "cooked": " - ", + "cooked": null, "raw": " - ", }, @@ -79,7 +79,7 @@ Program { type: "TemplateElement", tail: true, value: { - "cooked": "", + "cooked": null, "raw": "", }, diff --git a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/5-AST-Alignment-AST.shot b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/5-AST-Alignment-AST.shot index a2d68af67a6b..0e993488ce92 100644 --- a/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/5-AST-Alignment-AST.shot +++ b/packages/ast-spec/src/legacy-fixtures/types/fixtures/template-literal-type-4/snapshots/5-AST-Alignment-AST.shot @@ -30,7 +30,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': '', +- 'cooked': null, - 'raw': '', - }, - @@ -44,7 +44,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': ' - ', +- 'cooked': null, - 'raw': ' - ', - }, - @@ -58,7 +58,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': ' - ', +- 'cooked': null, - 'raw': ' - ', - }, - @@ -72,7 +72,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: false, - value: Object { -- 'cooked': ' - ', +- 'cooked': null, - 'raw': ' - ', - }, + typeAnnotation: TSLiteralType { @@ -98,7 +98,7 @@ Snapshot Diff: - type: 'TemplateElement', - tail: true, - value: Object { -- 'cooked': '', +- 'cooked': null, - 'raw': '', - }, + range: [124, 133], diff --git a/packages/ast-spec/src/special/TemplateElement/spec.ts b/packages/ast-spec/src/special/TemplateElement/spec.ts index cb5d1c6e76f8..dda44172c500 100644 --- a/packages/ast-spec/src/special/TemplateElement/spec.ts +++ b/packages/ast-spec/src/special/TemplateElement/spec.ts @@ -5,7 +5,7 @@ export interface TemplateElement extends BaseNode { type: AST_NODE_TYPES.TemplateElement; tail: boolean; value: { - cooked: string; + cooked: string | null; raw: string; }; } diff --git a/packages/ast-spec/tests/fixtures-with-differences-ast.shot b/packages/ast-spec/tests/fixtures-with-differences-ast.shot index a64da89a8106..254d24375d39 100644 --- a/packages/ast-spec/tests/fixtures-with-differences-ast.shot +++ b/packages/ast-spec/tests/fixtures-with-differences-ast.shot @@ -215,6 +215,7 @@ exports[`AST Fixtures > List fixtures with AST differences`] "legacy-fixtures/types/fixtures/optional-variance-out/fixture.ts", "legacy-fixtures/types/fixtures/reference-generic-nested/fixture.ts", "legacy-fixtures/types/fixtures/reference-generic/fixture.ts", + "legacy-fixtures/types/fixtures/template-literal-type-1/fixture.ts", "legacy-fixtures/types/fixtures/template-literal-type-2/fixture.ts", "legacy-fixtures/types/fixtures/template-literal-type-3/fixture.ts", "legacy-fixtures/types/fixtures/template-literal-type-4/fixture.ts", diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index f2a504aa4a1d..d31f512eabed 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -90,6 +90,7 @@ function isEntityNameExpression( } export class Converter { + #isInTaggedTemplate = false; private allowPattern = false; private readonly ast: ts.SourceFile; private readonly esTreeNodeToTSNodeMap = new WeakMap(); @@ -401,6 +402,38 @@ export class Converter { } } + #isValidEscape(arg: string): boolean { + const unicode = /\\u([0-9a-fA-F]{4})/g; + const unicodeBracket = /\\u\{([0-9a-fA-F]+)\}/g; // supports ES6+ + const hex = /\\x([0-9a-fA-F]{2})/g; + const validShort = /\\[nrtbfv0\\'"]/g; + + const allEscapes = + /\\(u\{[^}]*\}|u[0-9a-fA-F]{0,4}|x[0-9a-fA-F]{0,2}|[^ux])/g; + + let match: RegExpExecArray | null; + while ((match = allEscapes.exec(arg)) != null) { + const escape = match[0]; + + if ( + unicode.test(escape) || + (unicodeBracket.test(escape) && + (() => { + const cp = parseInt(escape.match(unicodeBracket)![1], 16); + return cp <= 0x10ffff; + })()) || + hex.test(escape) || + validShort.test(escape) + ) { + continue; + } + + return false; + } + + return true; + } + #throwError(node: number | ts.Node, message: string): asserts node is never { let start; let end; @@ -1889,7 +1922,10 @@ export class Converter { type: AST_NODE_TYPES.TemplateElement, tail: true, value: { - cooked: node.text, + cooked: + this.#isValidEscape(node.text) && this.#isInTaggedTemplate + ? node.text + : null, raw: this.ast.text.slice( node.getStart(this.ast) + 1, node.end - 1, @@ -1917,19 +1953,25 @@ export class Converter { return result; } - case SyntaxKind.TaggedTemplateExpression: - return this.createNode(node, { - type: AST_NODE_TYPES.TaggedTemplateExpression, - quasi: this.convertChild(node.template), - tag: this.convertChild(node.tag), - typeArguments: - node.typeArguments && - this.convertTypeArgumentsToTypeParameterInstantiation( - node.typeArguments, - node, - ), - }); - + case SyntaxKind.TaggedTemplateExpression: { + this.#isInTaggedTemplate = true; + const result = this.createNode( + node, + { + type: AST_NODE_TYPES.TaggedTemplateExpression, + quasi: this.convertChild(node.template), + tag: this.convertChild(node.tag), + typeArguments: + node.typeArguments && + this.convertTypeArgumentsToTypeParameterInstantiation( + node.typeArguments, + node, + ), + }, + ); + this.#isInTaggedTemplate = false; + return result; + } case SyntaxKind.TemplateHead: case SyntaxKind.TemplateMiddle: case SyntaxKind.TemplateTail: { @@ -1938,7 +1980,10 @@ export class Converter { type: AST_NODE_TYPES.TemplateElement, tail, value: { - cooked: node.text, + cooked: + this.#isValidEscape(node.text) && this.#isInTaggedTemplate + ? node.text + : null, raw: this.ast.text.slice( node.getStart(this.ast) + 1, node.end - (tail ? 1 : 2), diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index c371a6e9e7ac..a44bb4952b5c 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -926,4 +926,58 @@ describe(parser.parseAndGenerateServices, () => { }); }, ); + + describe('template literal cooked values', () => { + const getTemplateElement = ( + code: string, + ): parser.TSESTree.TemplateElement | null => { + const result = parser.parse(code, { + comment: true, + loc: true, + range: true, + tokens: true, + }); + + const taggedTemplate = result.body.find( + b => b.type === parser.AST_NODE_TYPES.ExpressionStatement, + ); + const expression = taggedTemplate?.expression; + if (expression?.type !== parser.AST_NODE_TYPES.TaggedTemplateExpression) { + return null; + } + return expression.quasi.quasis[0]; + }; + + it('should set cooked to null for invalid escape sequences in tagged template literals', () => { + const code = 'String.raw`\\uXXXX`'; + const templateElement = getTemplateElement(code); + + expect(templateElement?.value.cooked).toBeNull(); + expect(templateElement?.value.raw).toBe('\\uXXXX'); + }); + + it('should set cooked to null for other invalid escape sequences', () => { + const code = 'String.raw`\\unicode and \\u{55}`'; + const templateElement = getTemplateElement(code); + + expect(templateElement?.value.cooked).toBeNull(); + expect(templateElement?.value.raw).toBe('\\unicode and \\u{55}'); + }); + + it('should set cooked to parsed value for valid escape sequences', () => { + const code = 'String.raw`\\n\\t\\u0041`'; + const templateElement = getTemplateElement(code); + + expect(templateElement?.value.cooked).toBe('\n\tA'); + expect(templateElement?.value.raw).toBe('\\n\\t\\u0041'); + }); + + it('should handle mixed valid and invalid escape sequences', () => { + const code = 'String.raw`\\n\\uXXXX\\t`'; + const templateElement = getTemplateElement(code); + + expect(templateElement?.value.cooked).toBeNull(); + expect(templateElement?.value.raw).toBe('\\n\\uXXXX\\t'); + }); + }); });