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/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/eslint-plugin-internal/src/rules/plugin-test-formatting.ts b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts index 26fcc60d8d2b..78eef0741c86 100644 --- a/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts +++ b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts @@ -284,6 +284,10 @@ export default createRule({ const text = literal.quasis[0].value.cooked; + if (text == null) { + return; + } + if (literal.loc.end.line === literal.loc.start.line) { // don't use template strings for single line tests return context.report({ @@ -448,9 +452,13 @@ export default createRule({ } function checkForUnnecesaryNoFormat( - text: string, + text: string | null, expr: TSESTree.TaggedTemplateExpression, ): void { + if (text == null) { + return; + } + const formatted = getCodeFormatted(text); if (formatted === text) { context.report({ diff --git a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts index c5a858dbdcaa..e413656136d5 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts @@ -56,7 +56,7 @@ export default createRule({ return; } - let value: number | string | undefined; + let value: number | string | null | undefined; if (isStringLiteral(member.initializer)) { value = member.initializer.value; } else if (isNumberLiteral(member.initializer)) { diff --git a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts index 2dd9ca6b5d2e..b7f55ecabad2 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts @@ -200,7 +200,11 @@ export default createRule({ receiverProperty.key.type === AST_NODE_TYPES.TemplateLiteral && receiverProperty.key.quasis.length === 1 ) { - key = receiverProperty.key.quasis[0].value.cooked; + const cooked = receiverProperty.key.quasis[0].value.cooked; + if (cooked == null) { + continue; + } + key = cooked; } else { // can't figure out the name, so skip it continue; diff --git a/packages/parser/tsconfig.build.json b/packages/parser/tsconfig.build.json index ca2c74a7ac78..1c35eaea1cc2 100644 --- a/packages/parser/tsconfig.build.json +++ b/packages/parser/tsconfig.build.json @@ -8,9 +8,6 @@ { "path": "../typescript-estree/tsconfig.build.json" }, - { - "path": "../types/tsconfig.build.json" - }, { "path": "../scope-manager/tsconfig.build.json" } diff --git a/packages/parser/tsconfig.json b/packages/parser/tsconfig.json index 8122a5aaab8a..5f1cd2b68000 100644 --- a/packages/parser/tsconfig.json +++ b/packages/parser/tsconfig.json @@ -9,9 +9,6 @@ { "path": "../typescript-estree" }, - { - "path": "../types" - }, { "path": "../scope-manager" }, diff --git a/packages/project-service/tsconfig.build.json b/packages/project-service/tsconfig.build.json index d42ee3d2bef0..4496a35e63c2 100644 --- a/packages/project-service/tsconfig.build.json +++ b/packages/project-service/tsconfig.build.json @@ -4,9 +4,6 @@ "references": [ { "path": "../tsconfig-utils/tsconfig.build.json" - }, - { - "path": "../types/tsconfig.build.json" } ] } diff --git a/packages/project-service/tsconfig.json b/packages/project-service/tsconfig.json index 8029bc0ac19d..51673a2ff68e 100644 --- a/packages/project-service/tsconfig.json +++ b/packages/project-service/tsconfig.json @@ -6,9 +6,6 @@ { "path": "../tsconfig-utils" }, - { - "path": "../types" - }, { "path": "./tsconfig.build.json" }, diff --git a/packages/scope-manager/tsconfig.build.json b/packages/scope-manager/tsconfig.build.json index 51c7a6dbc40d..48ac0db13dc8 100644 --- a/packages/scope-manager/tsconfig.build.json +++ b/packages/scope-manager/tsconfig.build.json @@ -5,9 +5,6 @@ { "path": "../visitor-keys/tsconfig.build.json" }, - { - "path": "../types/tsconfig.build.json" - }, { "path": "../typescript-estree/tsconfig.build.json" } diff --git a/packages/scope-manager/tsconfig.json b/packages/scope-manager/tsconfig.json index 44cfc868c8d6..56d85c206c52 100644 --- a/packages/scope-manager/tsconfig.json +++ b/packages/scope-manager/tsconfig.json @@ -6,9 +6,6 @@ { "path": "../visitor-keys" }, - { - "path": "../types" - }, { "path": "../typescript-estree" }, diff --git a/packages/type-utils/tsconfig.build.json b/packages/type-utils/tsconfig.build.json index 2c29fdf0c620..1bb2abba12ab 100644 --- a/packages/type-utils/tsconfig.build.json +++ b/packages/type-utils/tsconfig.build.json @@ -2,9 +2,6 @@ "extends": "../../tsconfig.build.json", "compilerOptions": {}, "references": [ - { - "path": "../types/tsconfig.build.json" - }, { "path": "../utils/tsconfig.build.json" }, diff --git a/packages/type-utils/tsconfig.json b/packages/type-utils/tsconfig.json index a57d8d1814a8..2eb8e60114d8 100644 --- a/packages/type-utils/tsconfig.json +++ b/packages/type-utils/tsconfig.json @@ -3,9 +3,6 @@ "files": [], "include": [], "references": [ - { - "path": "../types" - }, { "path": "../utils" }, diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 7ebc034afdec..55c5a036c37f 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,78 @@ export class Converter { } } + #isValidEscape(text: string): boolean { + function isHex(hex: string): boolean { + return /^[0-9a-fA-F]+$/.test(hex); + } + + const validShort = [ + 'f', + 'n', + 'r', + 't', + 'v', + 'b', + '\\', + '"', + "'", + '`', + '0', + '$', + ]; + + for (let index = 0; index < text.length; index++) { + const char = text[index]; + if (char !== '\\') { + continue; + } + + const nextChar = text[index + 1]; + + if (validShort.includes(nextChar)) { + index += 1; + continue; + } + + // unicode + if (nextChar === 'u') { + if (text[index + 2] === '{') { + const closingBraceIndex = text.indexOf('}', index + 3); + if (closingBraceIndex === -1) { + return false; + } + + const hex = text.slice(index + 3, closingBraceIndex); + if (!isHex(hex) || hex.length === 0 || hex.length > 6) { + return false; + } + index += closingBraceIndex; + continue; + } else { + const hex = text.slice(index + 2, index + 6); + if (!isHex(hex) || hex.length !== 4) { + return false; + } + index += 5; + continue; + } + } + + // hex + if (nextChar === 'x') { + const hex = text.slice(index + 2, index + 4); + if (!isHex(hex) || hex.length !== 2) { + return false; + } + index += 3; + continue; + } + + return false; + } + return true; + } + #throwError(node: number | ts.Node, message: string): asserts node is never { let start; let end; @@ -1889,7 +1962,10 @@ export class Converter { type: AST_NODE_TYPES.TemplateElement, tail: true, value: { - cooked: node.text, + cooked: + this.#isInTaggedTemplate && !this.#isValidEscape(node.text) + ? null + : node.text, raw: this.ast.text.slice( node.getStart(this.ast) + 1, node.end - 1, @@ -1924,19 +2000,24 @@ export class Converter { 'Tagged template expressions are not permitted in an optional chain.', ); } - 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, - ), - }); + 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: { @@ -1945,7 +2026,10 @@ export class Converter { type: AST_NODE_TYPES.TemplateElement, tail, value: { - cooked: node.text, + cooked: + this.#isInTaggedTemplate && !this.#isValidEscape(node.text) + ? null + : node.text, raw: this.ast.text.slice( node.getStart(this.ast) + 1, node.end - (tail ? 1 : 2), diff --git a/packages/typescript-estree/tests/lib/convert.test.ts b/packages/typescript-estree/tests/lib/convert.test.ts index 77dea1f5f4b4..ce54f0222436 100644 --- a/packages/typescript-estree/tests/lib/convert.test.ts +++ b/packages/typescript-estree/tests/lib/convert.test.ts @@ -449,4 +449,136 @@ describe('convert', () => { expect(Object.keys(tsMappedType)).toContain('typeParameter'); }); }); + + describe('tagged template literal cooked', () => { + const getTemplateElement = (code: string): TSESTree.TemplateElement => { + const result = convertCode(code); + const converter = new Converter(result); + const program = converter.convertProgram(); + + const taggedTemplate = program.body.find( + b => b.type === AST_NODE_TYPES.ExpressionStatement, + ); + const expression = taggedTemplate?.expression; + if (expression?.type !== AST_NODE_TYPES.TaggedTemplateExpression) { + throw new Error('TaggedTemplateExpression not found'); + } + return expression.quasi.quasis[0]; + }; + + const invalidEscapeSequences = [String.raw`\uXXXX`, String.raw`\xQW`]; + + it('should set cooked to null for invalid escape sequences in tagged template literals', () => { + const code = `tag\`${invalidEscapeSequences[0]}${invalidEscapeSequences[1]}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBeNull(); + }); + + it('should set cooked to null for mixed valid and invalid escape sequences', () => { + const code = `tag\`\n${invalidEscapeSequences[0]}\u{1111}\t\${}${invalidEscapeSequences[1]}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBeNull(); + }); + + it('should not set cooked to null for text without invalid escape sequences', () => { + const code = `tag\`foo\n\\\u1111\t + bar + baz\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBe(`foo\n\u1111\t + bar + baz`); + }); + + it('should not set cooked to null for untagged template literals', () => { + const code = `const foo = \`${invalidEscapeSequences[0]}\``; + const result = convertCode(code); + const converter = new Converter(result); + const program = converter.convertProgram(); + + const variableDeclaration = program.body.find( + b => b.type === AST_NODE_TYPES.VariableDeclaration, + ); + const variableDeclarator = variableDeclaration?.declarations[0]; + if (variableDeclarator?.type !== AST_NODE_TYPES.VariableDeclarator) { + throw new Error('VariableDeclarator not found'); + } + const init = variableDeclarator.init; + if (init?.type !== AST_NODE_TYPES.TemplateLiteral) { + throw new Error('TemplateLiteral not found'); + } + const templateElement = init.quasis[0]; + + expect(templateElement.value.cooked).toBe(`\\uXXXX`); + }); + + describe('validate escape sequences', () => { + it('should return false for invalid unicode', () => { + const invalidUnicodes = [String.raw`\uXXXX`, String.raw`\u12`]; + const codes = invalidUnicodes.map( + invalidUnicode => `tag\`${invalidUnicode}\``, + ); + const templateElements = codes.map(code => getTemplateElement(code)); + + expect(templateElements[0].value.cooked).toBeNull(); + expect(templateElements[1].value.cooked).toBeNull(); + }); + + it('should return false for invalid unicode with braces', () => { + const invalidUnicodes = [String.raw`\u{123`, String.raw`\u{12345678}`]; + const codes = invalidUnicodes.map( + invalidUnicode => `tag\`${invalidUnicode}\``, + ); + const templateElements = codes.map(code => getTemplateElement(code)); + + expect(templateElements[0].value.cooked).toBeNull(); + expect(templateElements[1].value.cooked).toBeNull(); + }); + + it('should return true for valid unicode', () => { + // 002E is . + const validUnicodes = [String.raw`\u{002E}`, String.raw`\u002E`]; + const code = `tag\`${validUnicodes[0]}${validUnicodes[1]}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBe('..'); + }); + + it('should return false for invalid hex', () => { + const invalidHexes = [String.raw`\x1`, String.raw`\xZX`]; + const codes = invalidHexes.map(invalidHex => `tag\`${invalidHex}\``); + const templateElements = codes.map(code => getTemplateElement(code)); + + expect(templateElements[0].value.cooked).toBeNull(); + expect(templateElements[1].value.cooked).toBeNull(); + }); + + it('should return true for valid hex', () => { + const validHex = String.raw`\x2E`; + const code = `tag\`${validHex}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBe('.'); + }); + + it('should return false for invalid short', () => { + const invalidShort = String.raw`\1`; + const code = `tag\`${invalidShort}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBeNull(); + }); + + it('should return true for valid short', () => { + const validShort = String.raw`\"\'`; + const code = `tag\`${validShort}\``; + const templateElement = getTemplateElement(code); + + expect(templateElement.value.cooked).toBe(`"'`); + }); + }); + }); }); diff --git a/packages/utils/tsconfig.build.json b/packages/utils/tsconfig.build.json index f99a3f7e1b40..e1c25e0bedf6 100644 --- a/packages/utils/tsconfig.build.json +++ b/packages/utils/tsconfig.build.json @@ -5,9 +5,6 @@ { "path": "../typescript-estree/tsconfig.build.json" }, - { - "path": "../types/tsconfig.build.json" - }, { "path": "../scope-manager/tsconfig.build.json" } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index bda7a07530ab..1a5d1d3a8729 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -6,9 +6,6 @@ { "path": "../typescript-estree" }, - { - "path": "../types" - }, { "path": "../scope-manager" }, diff --git a/packages/visitor-keys/tsconfig.build.json b/packages/visitor-keys/tsconfig.build.json index deb22cbe20a5..4a1f19accacd 100644 --- a/packages/visitor-keys/tsconfig.build.json +++ b/packages/visitor-keys/tsconfig.build.json @@ -1,9 +1,5 @@ { "extends": "../../tsconfig.build.json", "compilerOptions": {}, - "references": [ - { - "path": "../types/tsconfig.build.json" - } - ] + "references": [] } diff --git a/packages/visitor-keys/tsconfig.json b/packages/visitor-keys/tsconfig.json index d89a8a4ec896..d4d0929e1955 100644 --- a/packages/visitor-keys/tsconfig.json +++ b/packages/visitor-keys/tsconfig.json @@ -3,9 +3,6 @@ "files": [], "include": [], "references": [ - { - "path": "../types" - }, { "path": "./tsconfig.build.json" }, diff --git a/packages/website/tsconfig.build.json b/packages/website/tsconfig.build.json index 94033f10b0be..bdc90c5624ad 100644 --- a/packages/website/tsconfig.build.json +++ b/packages/website/tsconfig.build.json @@ -35,9 +35,6 @@ { "path": "../typescript-estree/tsconfig.build.json" }, - { - "path": "../types/tsconfig.build.json" - }, { "path": "../scope-manager/tsconfig.build.json" }, diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index d3b0dd5bcc67..9e586ad2f711 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -12,9 +12,6 @@ { "path": "../typescript-estree" }, - { - "path": "../types" - }, { "path": "../scope-manager" },