From 7af4c37df830bfd1e63c4bcbbef0896dd0d9320c Mon Sep 17 00:00:00 2001 From: Kei Sakamoto Date: Tue, 2 Dec 2025 18:21:36 +0900 Subject: [PATCH] feat: implement no-template-curly-in-string --- internal/config/config.go | 2 + .../no_template_curly_in_string.go | 27 +++++++ .../no_template_curly_in_string_test.go | 74 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 internal/rules/no_template_curly_in_string/no_template_curly_in_string.go create mode 100644 internal/rules/no_template_curly_in_string/no_template_curly_in_string_test.go diff --git a/internal/config/config.go b/internal/config/config.go index c43fab90..3c94eb88 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -95,6 +95,7 @@ import ( "github.com/web-infra-dev/rslint/internal/rules/no_constant_condition" "github.com/web-infra-dev/rslint/internal/rules/no_constructor_return" "github.com/web-infra-dev/rslint/internal/rules/no_debugger" + "github.com/web-infra-dev/rslint/internal/rules/no_template_curly_in_string" ) // RslintConfig represents the top-level configuration array @@ -459,6 +460,7 @@ func registerAllCoreEslintRules() { GlobalRuleRegistry.Register("no-constant-condition", no_constant_condition.NoConstantConditionRule) GlobalRuleRegistry.Register("no-constructor-return", no_constructor_return.NoConstructorReturnRule) GlobalRuleRegistry.Register("no-debugger", no_debugger.NoDebuggerRule) + GlobalRuleRegistry.Register("no-template-curly-in-string", no_template_curly_in_string.NoTemplateCurlyInString) } // getAllTypeScriptEslintPluginRules returns all registered rules (for backward compatibility when no config is provided) diff --git a/internal/rules/no_template_curly_in_string/no_template_curly_in_string.go b/internal/rules/no_template_curly_in_string/no_template_curly_in_string.go new file mode 100644 index 00000000..7cca40e3 --- /dev/null +++ b/internal/rules/no_template_curly_in_string/no_template_curly_in_string.go @@ -0,0 +1,27 @@ +package no_template_curly_in_string + +import ( + "regexp" + + "github.com/microsoft/typescript-go/shim/ast" + "github.com/web-infra-dev/rslint/internal/rule" +) + +// https://eslint.org/docs/latest/rules/no-template-curly-in-string +var NoTemplateCurlyInString = rule.Rule{ + Name: "no-template-curly-in-string", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + regex := regexp.MustCompile(`\$\{[^}]+\}`) + return rule.RuleListeners{ + ast.KindStringLiteral: func(node *ast.Node) { + expr := node.AsStringLiteral() + if regex.MatchString(expr.Text) { + ctx.ReportNode(node, rule.RuleMessage{ + Id: "unexpectedTemplateExpression", + Description: "Template literal placeholder syntax in regular strings is not allowed.", + }) + } + }, + } + }, +} diff --git a/internal/rules/no_template_curly_in_string/no_template_curly_in_string_test.go b/internal/rules/no_template_curly_in_string/no_template_curly_in_string_test.go new file mode 100644 index 00000000..02855724 --- /dev/null +++ b/internal/rules/no_template_curly_in_string/no_template_curly_in_string_test.go @@ -0,0 +1,74 @@ +package no_template_curly_in_string + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +func TestNoTemplateCurlyInString(t *testing.T) { + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &NoTemplateCurlyInString, + // Valid cases - ported from ESLint + []rule_tester.ValidTestCase{ + {Code: "`Hello, ${name}`;"}, + {Code: "templateFunction`Hello, ${name}`;"}, + {Code: "`Hello, name`;"}, + {Code: "'Hello, name';"}, + {Code: "'Hello, ' + name;"}, + {Code: "`Hello, ${index + 1}`"}, + {Code: "`Hello, ${name + \" foo\"}`"}, + {Code: "`Hello, ${name || \"foo\"}`"}, + {Code: "`Hello, ${{foo: \"bar\"}.foo}`"}, + {Code: "'$2'"}, + {Code: "'${'"}, + {Code: "'$}'"}, + {Code: "'{foo}'"}, + {Code: `'{foo: \"bar\"}'`}, + {Code: "const number = 3"}, + }, + // Invalid cases - ported from ESLint + []rule_tester.InvalidTestCase{ + { + Code: "'Hello, ${name}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + { + Code: "'${greeting}, ${name}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + { + Code: "'Hello, ${index + 1}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + { + Code: "'Hello, ${name + \\\" foo\\\"}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + { + Code: "'Hello, ${name || \\\"foo\\\"}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + { + Code: "'Hello, ${{foo: \\\"bar\\\"}.foo}'", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedTemplateExpression", Line: 1, Column: 1}, + }, + }, + }, + ) +}