Skip to content

fix(typescript-estree): if the template literal is tagged and the text has an invalid escape, cooked will be null #11355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cbe8cc9
test: add test case
nayounsang Jun 28, 2025
a1902bb
feat: make flag whether node is inside tag
nayounsang Jun 28, 2025
4938a14
fix: if template literal is tagged and the text has an invalid escape…
nayounsang Jun 28, 2025
77960bf
fix: type error
nayounsang Jun 28, 2025
c11a244
chore: add snapshot
nayounsang Jun 28, 2025
cd7550f
test: add test case
nayounsang Jun 28, 2025
156b384
feat: make flag whether node is inside tag
nayounsang Jun 28, 2025
6103105
fix: if template literal is tagged and the text has an invalid escape…
nayounsang Jun 28, 2025
cbeb8d7
fix: type error
nayounsang Jun 28, 2025
954e84e
chore: add snapshot
nayounsang Jun 28, 2025
b5bc96d
Merge branch 'main' into tag-cook
JamesHenry Jul 1, 2025
8fdb4db
chore: try with SKIP_AST_SPEC_REBUILD false
JamesHenry Jul 1, 2025
2dc7c7b
Merge branch 'main' into tag-cook
nayounsang Jul 23, 2025
2e5857a
fix: fallback when cooked is null
nayounsang Jul 23, 2025
8279f15
Merge branch 'tag-cook' of https://github.com/nayounsang/typescript-e…
nayounsang Jul 23, 2025
49dcf05
chore: temp commit to see ci result
nayounsang Jul 23, 2025
84ec93c
fix: try to change input
nayounsang Jul 23, 2025
814604f
fix: mis config input
nayounsang Jul 23, 2025
888e312
chore: off ast spec env
nayounsang Jul 23, 2025
4c1891c
fix: lint err
nayounsang Jul 24, 2025
2a31006
chore: revert action.yml and package.json
nayounsang Jul 24, 2025
27e9b9a
chore: workspace sync
nayounsang Jul 31, 2025
30ed17f
test: add test snapshot
nayounsang Jul 31, 2025
3d5888d
test: move test to convert and add validate tests
nayounsang Aug 2, 2025
4d5d413
feat: change validate logic
nayounsang Aug 2, 2025
5e6e6b7
fix: (tmp) process cooked is null
nayounsang Aug 2, 2025
1ada433
Merge branch 'main' into tag-cook
nayounsang Aug 2, 2025
a051525
test: fix lint error on convert test
nayounsang Aug 2, 2025
a7311e6
fix: add case for $
nayounsang Aug 2, 2025
8a67d05
refactor: ignore with conditional statement
nayounsang Aug 2, 2025
b85dda1
fix: enhance test coverage and remove code for nextChar is null
nayounsang Aug 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/ast-spec/src/special/TemplateElement/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface TemplateElement extends BaseNode {
type: AST_NODE_TYPES.TemplateElement;
tail: boolean;
value: {
cooked: string;
cooked: string | null;
raw: string;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ export default createRule<Options, MessageIds>({

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({
Expand Down Expand Up @@ -448,9 +452,13 @@ export default createRule<Options, MessageIds>({
}

function checkForUnnecesaryNoFormat(
text: string,
text: string | null,
expr: TSESTree.TaggedTemplateExpression,
): void {
if (text == null) {
Copy link
Contributor Author

@nayounsang nayounsang Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change affects three rules.
Would it be better to simply ignore it? How should I handle it?
Once this is decided, I will also add tests.

return;
}

const formatted = getCodeFormatted(text);
if (formatted === text) {
context.report({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 0 additions & 3 deletions packages/parser/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
{
"path": "../typescript-estree/tsconfig.build.json"
},
{
"path": "../types/tsconfig.build.json"
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of this message by nx,

NX   The workspace is out of sync

[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.

? Would you like to sync the identified changes to get your workspace up to date? … 
Yes, sync the changes and run the tasks
No, run the tasks without syncing the changes

There are a lot of changes like this due to sync, but even after reading the documentation, I'm not sure if this is correct.
I reverted all dependency related changes.

{
"path": "../scope-manager/tsconfig.build.json"
}
Expand Down
3 changes: 0 additions & 3 deletions packages/parser/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
{
"path": "../typescript-estree"
},
{
"path": "../types"
},
{
"path": "../scope-manager"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/project-service/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
"references": [
{
"path": "../tsconfig-utils/tsconfig.build.json"
},
{
"path": "../types/tsconfig.build.json"
}
]
}
3 changes: 0 additions & 3 deletions packages/project-service/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
{
"path": "../tsconfig-utils"
},
{
"path": "../types"
},
{
"path": "./tsconfig.build.json"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/scope-manager/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
{
"path": "../visitor-keys/tsconfig.build.json"
},
{
"path": "../types/tsconfig.build.json"
},
{
"path": "../typescript-estree/tsconfig.build.json"
}
Expand Down
3 changes: 0 additions & 3 deletions packages/scope-manager/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
{
"path": "../visitor-keys"
},
{
"path": "../types"
},
{
"path": "../typescript-estree"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/type-utils/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"extends": "../../tsconfig.build.json",
"compilerOptions": {},
"references": [
{
"path": "../types/tsconfig.build.json"
},
{
"path": "../utils/tsconfig.build.json"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/type-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"files": [],
"include": [],
"references": [
{
"path": "../types"
},
{
"path": "../utils"
},
Expand Down
112 changes: 98 additions & 14 deletions packages/typescript-estree/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function isEntityNameExpression(
}

export class Converter {
#isInTaggedTemplate = false;
private allowPattern = false;
private readonly ast: ts.SourceFile;
private readonly esTreeNodeToTSNodeMap = new WeakMap();
Expand Down Expand Up @@ -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];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't care about if nextChar doesn't exist because of typescript error & parsing error.


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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1924,19 +2000,24 @@ export class Converter {
'Tagged template expressions are not permitted in an optional chain.',
);
}
return this.createNode<TSESTree.TaggedTemplateExpression>(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<TSESTree.TaggedTemplateExpression>(
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: {
Expand All @@ -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),
Expand Down
Loading
Loading