diff --git a/internal/config/config.go b/internal/config/config.go index 0b1b89406..94f982fb1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -77,6 +77,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_readonly" // "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_readonly_parameter_types" // Temporarily disabled - incomplete implementation "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_reduce_type_parameter" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_regexp_exec" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_return_this_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/prefer_string_starts_ends_with" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/promise_function_async" @@ -454,6 +455,7 @@ func registerAllTypeScriptEslintPluginRules() { // detection of readonly arrays, readonly objects, function types, and other edge cases // GlobalRuleRegistry.Register("@typescript-eslint/prefer-readonly-parameter-types", prefer_readonly_parameter_types.PreferReadonlyParameterTypesRule) GlobalRuleRegistry.Register("@typescript-eslint/prefer-reduce-type-parameter", prefer_reduce_type_parameter.PreferReduceTypeParameterRule) + GlobalRuleRegistry.Register("@typescript-eslint/prefer-regexp-exec", prefer_regexp_exec.PreferRegExpExecRule) GlobalRuleRegistry.Register("@typescript-eslint/prefer-return-this-type", prefer_return_this_type.PreferReturnThisTypeRule) GlobalRuleRegistry.Register("@typescript-eslint/prefer-string-starts-ends-with", prefer_string_starts_ends_with.PreferStringStartsEndsWithRule) GlobalRuleRegistry.Register("@typescript-eslint/promise-function-async", promise_function_async.PromiseFunctionAsyncRule) diff --git a/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.go b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.go new file mode 100644 index 000000000..7db7e7493 --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.go @@ -0,0 +1,447 @@ +package prefer_regexp_exec + +import ( + "strings" + + "github.com/dlclark/regexp2" + "github.com/microsoft/typescript-go/shim/ast" + "github.com/microsoft/typescript-go/shim/scanner" + "github.com/web-infra-dev/rslint/internal/rule" + "github.com/web-infra-dev/rslint/internal/utils" +) + +func buildPreferRegExpExecMessage() rule.RuleMessage { + return rule.RuleMessage{ + Id: "regExpExecOverStringMatch", + Description: "Use the `RegExp#exec()` method instead.", + } +} + +func isGlobalRegexLiteral(node *ast.Node) bool { + if node == nil || node.Kind != ast.KindRegularExpressionLiteral { + return false + } + regex := node.AsRegularExpressionLiteral() + if regex == nil { + return false + } + text := regex.Text + lastSlash := strings.LastIndex(text, "/") + if lastSlash < 0 || lastSlash+1 >= len(text) { + return false + } + flags := text[lastSlash+1:] + return strings.Contains(flags, "g") +} + +type staticArgInfo struct { + known bool + global bool +} + +const ( + argumentTypeOther = 0 + argumentTypeString = 1 << iota + argumentTypeRegExp +) + +func unwrapExpression(node *ast.Node) *ast.Node { + node = ast.SkipParentheses(node) + for node != nil { + switch node.Kind { + case ast.KindAsExpression, ast.KindTypeAssertionExpression, ast.KindNonNullExpression: + node = ast.SkipParentheses(node.Expression()) + default: + return node + } + } + return nil +} + +func isNodeParenthesized(node *ast.Node) bool { + if node == nil || node.Parent == nil || !ast.IsParenthesizedExpression(node.Parent) { + return false + } + parent := node.Parent.AsParenthesizedExpression() + return parent != nil && parent.Expression == node +} + +func isWeakPrecedenceParent(node *ast.Node) bool { + if node == nil { + return false + } + parent := node.Parent + if parent == nil { + return false + } + switch parent.Kind { + case ast.KindPostfixUnaryExpression, + ast.KindPrefixUnaryExpression, + ast.KindBinaryExpression, + ast.KindConditionalExpression, + ast.KindAwaitExpression: + return true + } + if ast.IsPropertyAccessExpression(parent) { + return parent.AsPropertyAccessExpression().Expression == node + } + if ast.IsElementAccessExpression(parent) { + return parent.AsElementAccessExpression().Expression == node + } + if ast.IsCallExpression(parent) || ast.IsNewExpression(parent) { + return parent.Expression() == node + } + if ast.IsTaggedTemplateExpression(parent) { + return parent.AsTaggedTemplateExpression().Tag == node + } + return false +} + +func getWrappedNodeText(sourceFile *ast.SourceFile, node *ast.Node) string { + if sourceFile == nil || node == nil { + return "" + } + text := strings.TrimSpace(scanner.GetSourceTextOfNodeFromSourceFile(sourceFile, node, false)) + if text == "" { + return "" + } + if !utils.IsStrongPrecedenceNode(node) { + text = "(" + text + ")" + } + return text +} + +func collectArgumentTypes(ctx rule.RuleContext, argument *ast.Node) int { + argument = unwrapExpression(argument) + if argument == nil { + return argumentTypeOther + } + switch argument.Kind { + case ast.KindStringLiteral: + return argumentTypeString + case ast.KindRegularExpressionLiteral: + return argumentTypeRegExp + } + if ctx.TypeChecker == nil { + return argumentTypeOther + } + argType := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, argument) + result := argumentTypeOther + for _, part := range utils.UnionTypeParts(argType) { + switch utils.GetTypeName(ctx.TypeChecker, part) { + case "RegExp": + result |= argumentTypeRegExp + case "string": + result |= argumentTypeString + default: + return argumentTypeOther + } + } + return result +} + +func regExpFlagInfo(ctx rule.RuleContext, args []*ast.Node) (known bool, global bool) { + patternKnown := len(args) == 0 || args[0] == nil + patternGlobal := false + + // Pattern validity/flags only matter when statically known. + if len(args) > 0 && args[0] != nil { + patternArg := unwrapExpression(args[0]) + switch patternArg.Kind { + case ast.KindStringLiteral: + if _, err := regexp2.Compile(patternArg.AsStringLiteral().Text, regexp2.ECMAScript); err != nil { + return false, false + } + patternKnown = true + case ast.KindRegularExpressionLiteral: + patternKnown = true + patternGlobal = isGlobalRegexLiteral(patternArg) + default: + // For dynamic patterns, only mark as known when type info proves string-only. + if collectArgumentTypes(ctx, patternArg) == argumentTypeString { + patternKnown = true + } + } + } + + if len(args) < 2 || args[1] == nil || isUndefinedLiteral(ctx, args[1]) { + if !patternKnown { + return false, false + } + return true, patternGlobal + } + flagsArg := ast.SkipParentheses(args[1]) + if flagsArg.Kind != ast.KindStringLiteral { + return false, false + } + return true, strings.Contains(flagsArg.AsStringLiteral().Text, "g") +} + +func isUndefinedLiteral(ctx rule.RuleContext, node *ast.Node) bool { + node = ast.SkipParentheses(node) + if node == nil { + return false + } + if node.Kind != ast.KindIdentifier { + return false + } + id := node.AsIdentifier() + if id == nil || id.Text != "undefined" { + return false + } + if ctx.TypeChecker == nil || ctx.Program == nil { + return true + } + sym := ctx.TypeChecker.GetSymbolAtLocation(node) + if sym == nil || sym.Declarations == nil || len(sym.Declarations) == 0 { + return true + } + for _, decl := range sym.Declarations { + if decl == nil { + return false + } + sourceFile := ast.GetSourceFileOfNode(decl) + if sourceFile == nil || !sourceFile.IsDeclarationFile { + return false + } + } + return true +} + +func resolveStaticArgumentInfo(ctx rule.RuleContext, node *ast.Node, seen map[*ast.Symbol]bool) staticArgInfo { + node = unwrapExpression(node) + if node == nil { + return staticArgInfo{} + } + switch node.Kind { + case ast.KindStringLiteral: + return staticArgInfo{known: true} + case ast.KindRegularExpressionLiteral: + return staticArgInfo{known: true, global: isGlobalRegexLiteral(node)} + case ast.KindCallExpression: + call := node.AsCallExpression() + if call != nil && call.Expression != nil && call.Expression.Kind == ast.KindIdentifier && call.Expression.AsIdentifier().Text == "RegExp" && call.Arguments != nil { + known, global := regExpFlagInfo(ctx, call.Arguments.Nodes) + return staticArgInfo{known: known, global: global} + } + case ast.KindNewExpression: + newExpr := node.AsNewExpression() + if newExpr != nil && newExpr.Expression != nil && newExpr.Expression.Kind == ast.KindIdentifier && newExpr.Expression.AsIdentifier().Text == "RegExp" && newExpr.Arguments != nil { + known, global := regExpFlagInfo(ctx, newExpr.Arguments.Nodes) + return staticArgInfo{known: known, global: global} + } + case ast.KindIdentifier: + if ctx.TypeChecker == nil { + return staticArgInfo{} + } + sym := ctx.TypeChecker.GetSymbolAtLocation(node) + if sym == nil { + return staticArgInfo{} + } + if seen[sym] { + return staticArgInfo{} + } + seen[sym] = true + defer delete(seen, sym) + if sym.Declarations == nil { + return staticArgInfo{} + } + for _, decl := range sym.Declarations { + if decl == nil || decl.Kind != ast.KindVariableDeclaration { + continue + } + if !ast.IsVariableDeclarationList(decl.Parent) || decl.Parent.Flags&ast.NodeFlagsConst == 0 { + return staticArgInfo{} + } + varDecl := decl.AsVariableDeclaration() + if varDecl == nil || varDecl.Initializer == nil { + continue + } + info := resolveStaticArgumentInfo(ctx, varDecl.Initializer, seen) + if info.known { + return info + } + } + } + return staticArgInfo{} +} + +func definitelyDoesNotContainGlobalFlag(ctx rule.RuleContext, node *ast.Node) bool { + node = unwrapExpression(node) + if node == nil { + return false + } + switch node.Kind { + case ast.KindCallExpression: + call := node.AsCallExpression() + if call == nil || call.Expression == nil || call.Expression.Kind != ast.KindIdentifier || call.Expression.AsIdentifier().Text != "RegExp" || call.Arguments == nil { + return false + } + known, global := regExpFlagInfo(ctx, call.Arguments.Nodes) + return known && !global + case ast.KindNewExpression: + newExpr := node.AsNewExpression() + if newExpr == nil || newExpr.Expression == nil || newExpr.Expression.Kind != ast.KindIdentifier || newExpr.Expression.AsIdentifier().Text != "RegExp" || newExpr.Arguments == nil { + return false + } + known, global := regExpFlagInfo(ctx, newExpr.Arguments.Nodes) + return known && !global + default: + return false + } +} + +func isStringMatchCall(node *ast.Node) (*ast.CallExpression, bool) { + if node == nil || node.Kind != ast.KindCallExpression { + return nil, false + } + call := node.AsCallExpression() + if call == nil || call.Expression == nil { + return nil, false + } + + switch call.Expression.Kind { + case ast.KindPropertyAccessExpression: + access := call.Expression.AsPropertyAccessExpression() + if access == nil || access.Name() == nil || access.Name().Text() != "match" { + return nil, false + } + return call, true + case ast.KindElementAccessExpression: + access := call.Expression.AsElementAccessExpression() + if access == nil || access.ArgumentExpression == nil || access.ArgumentExpression.Kind != ast.KindStringLiteral { + return nil, false + } + if access.ArgumentExpression.AsStringLiteral().Text != "match" { + return nil, false + } + return call, true + } + return nil, false +} + +func isStringLikeReceiver(ctx rule.RuleContext, receiver *ast.Node) bool { + if receiver == nil || ctx.TypeChecker == nil { + return false + } + receiverType := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, receiver) + return utils.GetTypeName(ctx.TypeChecker, receiverType) == "string" +} + +func buildRegexLiteralFromString(pattern string) (string, bool) { + // Validate using ECMAScript semantics (not Go regexp/RE2). + if _, err := regexp2.Compile(pattern, regexp2.ECMAScript); err != nil { + return "", false + } + var b strings.Builder + b.WriteByte('/') + for _, ch := range pattern { + switch ch { + case '/': + b.WriteString(`\/`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + default: + b.WriteRune(ch) + } + } + b.WriteByte('/') + return b.String(), true +} + +func buildPreferRegExpExecReplacement(ctx rule.RuleContext, callNode *ast.Node, receiver *ast.Node, arg *ast.Node, argumentTypes int) (string, bool) { + if ctx.SourceFile == nil || callNode == nil || receiver == nil || arg == nil { + return "", false + } + receiverText := getWrappedNodeText(ctx.SourceFile, receiver) + argText := getWrappedNodeText(ctx.SourceFile, arg) + if receiverText == "" || argText == "" { + return "", false + } + + var replacement string + if arg.Kind == ast.KindStringLiteral { + regexLiteral, ok := buildRegexLiteralFromString(arg.AsStringLiteral().Text) + if !ok { + return "", false + } + replacement = regexLiteral + ".exec(" + receiverText + ")" + } else { + switch argumentTypes { + case argumentTypeRegExp: + replacement = argText + ".exec(" + receiverText + ")" + case argumentTypeString: + replacement = "RegExp(" + argText + ").exec(" + receiverText + ")" + default: + return "", false + } + } + if isWeakPrecedenceParent(callNode) && !isNodeParenthesized(callNode) { + replacement = "(" + replacement + ")" + } + return replacement, true +} + +var PreferRegExpExecRule = rule.CreateRule(rule.Rule{ + Name: "prefer-regexp-exec", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + return rule.RuleListeners{ + ast.KindCallExpression: func(node *ast.Node) { + call, ok := isStringMatchCall(node) + if !ok || call.Arguments == nil || len(call.Arguments.Nodes) != 1 { + return + } + + var receiver *ast.Node + var reportNode *ast.Node + switch call.Expression.Kind { + case ast.KindPropertyAccessExpression: + access := call.Expression.AsPropertyAccessExpression() + receiver = access.Expression + reportNode = access.Name().AsNode() + case ast.KindElementAccessExpression: + access := call.Expression.AsElementAccessExpression() + receiver = access.Expression + reportNode = access.ArgumentExpression + } + if receiver == nil || reportNode == nil { + return + } + if !isStringLikeReceiver(ctx, receiver) { + return + } + + arg := call.Arguments.Nodes[0] + argumentTypes := collectArgumentTypes(ctx, arg) + if argumentTypes == argumentTypeOther || argumentTypes == argumentTypeString|argumentTypeRegExp { + return + } + staticInfo := resolveStaticArgumentInfo(ctx, arg, map[*ast.Symbol]bool{}) + if staticInfo.known && staticInfo.global { + return + } + if !staticInfo.known && argumentTypes&argumentTypeRegExp != 0 && !definitelyDoesNotContainGlobalFlag(ctx, arg) { + return + } + if arg.Kind == ast.KindStringLiteral { + if _, err := regexp2.Compile(arg.AsStringLiteral().Text, regexp2.ECMAScript); err != nil { + return + } + } + + msg := buildPreferRegExpExecMessage() + if replacement, ok := buildPreferRegExpExecReplacement(ctx, node, receiver, arg, argumentTypes); ok { + if ctx.SourceFile == nil { + ctx.ReportNode(reportNode, msg) + return + } + ctx.ReportNodeWithFixes(reportNode, msg, rule.RuleFixReplaceRange(utils.TrimNodeTextRange(ctx.SourceFile, node), replacement)) + return + } + ctx.ReportNode(reportNode, msg) + }, + } + }, +}) diff --git a/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.md b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.md new file mode 100644 index 000000000..c3067f92d --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec.md @@ -0,0 +1,25 @@ +# prefer-regexp-exec + +## Rule Details + +Prefer `RegExp#exec` over `String#match` when a non-global regex match is used. + +Examples of **incorrect** code for this rule: + +```ts +const value = 'foo'; +value.match(/foo/); +value.match('foo'); +``` + +Examples of **correct** code: + +```ts +const value = 'foo'; +/foo/.exec(value); +value.match(/foo/g); +``` + +## Original Documentation + +- https://typescript-eslint.io/rules/prefer-regexp-exec diff --git a/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec_test.go b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec_test.go new file mode 100644 index 000000000..4803d34ed --- /dev/null +++ b/internal/plugins/typescript/rules/prefer_regexp_exec/prefer_regexp_exec_test.go @@ -0,0 +1,116 @@ +package prefer_regexp_exec + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +func TestPreferRegExpExecRule(t *testing.T) { + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &PreferRegExpExecRule, []rule_tester.ValidTestCase{ + {Code: `const value = "foo"; /foo/g.exec(value);`}, + {Code: `const value = "foo"; value.match(/foo/g);`}, + {Code: `const value = "foo"; value.match(pattern);`}, + {Code: `let search = /foo/; const value = "foo"; value.match(search);`}, + {Code: `const value = "foo"; declare const flags: string; value.match(new RegExp("foo", flags));`}, + {Code: `const value = "foo"; value.match("[a-z");`}, + {Code: `const value = "foo"; value.search(/foo/);`}, + {Code: `const value: { match(v: string): any } = { match: () => null as any }; value.match("foo");`}, + {Code: `const value = "foo"; const pattern = /foo/g as RegExp; value.match(pattern);`}, + {Code: `const value = "foo"; value.match(new RegExp(/foo/g));`}, + {Code: `const value = "foo"; value.match(new RegExp(/foo/g, undefined));`}, + {Code: `function test(value: string, undefined: string) { value.match(new RegExp("foo", undefined)); }`}, + }, []rule_tester.InvalidTestCase{ + { + Code: `const value = "foo"; value.match(/foo/);`, + Output: []string{`const value = "foo"; /foo/.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch", Line: 1, Column: 28}}, + }, + { + Code: `const value = "foo"; value.match("foo");`, + Output: []string{`const value = "foo"; /foo/.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch", Line: 1, Column: 28}}, + }, + { + Code: ` +const value = "foo"; +const reg: RegExp = /foo/; +value.match(reg);`, + Output: []string{` +const value = "foo"; +const reg: RegExp = /foo/; +reg.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch", Line: 4, Column: 7}}, + }, + { + Code: `const value = "foo"; value["match"](/foo/);`, + Output: []string{`const value = "foo"; /foo/.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: `const value = "foo"; value.match(new RegExp("foo", undefined));`, + Output: []string{`const value = "foo"; new RegExp("foo", undefined).exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: `const value = "foo"; value.match("\\d+");`, + Output: []string{`const value = "foo"; /\d+/.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: `const value = "foo"; value.match("a\nb");`, + Output: []string{`const value = "foo"; /a\nb/.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: ` +function test(value: string, pattern: string) { + value.match(pattern); +}`, + Output: []string{` +function test(value: string, pattern: string) { + RegExp(pattern).exec(value); +}`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: ` +function test(value: string, a: string, b: string, cond: boolean) { + value.match(cond ? a : b); +}`, + Output: []string{` +function test(value: string, a: string, b: string, cond: boolean) { + RegExp((cond ? a : b)).exec(value); +}`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: ` +const value = "foo"; +const reg = /foo/ as RegExp; +value.match(reg);`, + Output: []string{` +const value = "foo"; +const reg = /foo/ as RegExp; +reg.exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: ` +const value = "foo"; +const reg: RegExp | null = /foo/; +value.match(reg!);`, + Output: []string{` +const value = "foo"; +const reg: RegExp | null = /foo/; +(reg!).exec(value);`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + { + Code: `const value = "foo"; value.match(/foo/).toString();`, + Output: []string{`const value = "foo"; (/foo/.exec(value)).toString();`}, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "regExpExecOverStringMatch"}}, + }, + }) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index ec546c830..b28a3bf63 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -161,7 +161,7 @@ export default defineConfig({ // './tests/typescript-eslint/rules/prefer-readonly-parameter-types.test.ts', './tests/typescript-eslint/rules/prefer-readonly.test.ts', './tests/typescript-eslint/rules/prefer-reduce-type-parameter.test.ts', - // './tests/typescript-eslint/rules/prefer-regexp-exec.test.ts', + './tests/typescript-eslint/rules/prefer-regexp-exec.test.ts', './tests/typescript-eslint/rules/prefer-return-this-type.test.ts', './tests/typescript-eslint/rules/prefer-string-starts-ends-with.test.ts', // './tests/typescript-eslint/rules/prefer-ts-expect-error.test.ts', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-regexp-exec.test.ts.snap b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-regexp-exec.test.ts.snap new file mode 100644 index 000000000..be708bd35 --- /dev/null +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/__snapshots__/prefer-regexp-exec.test.ts.snap @@ -0,0 +1,354 @@ +// Rstest Snapshot v1 + +exports[`prefer-regexp-exec > invalid 1`] = ` +{ + "code": "'something'.match(/thing/);", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 18, + "line": 1, + }, + "start": { + "column": 13, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "/thing/.exec('something');", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 2`] = ` +{ + "code": "'something'.match('^[a-z]+thing/?$');", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 18, + "line": 1, + }, + "start": { + "column": 13, + "line": 1, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": "/^[a-z]+thing\\/?$/.exec('something');", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 3`] = ` +{ + "code": " +const text = 'something'; +const search = /thing/; +text.match(search); + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 11, + "line": 4, + }, + "start": { + "column": 6, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +const text = 'something'; +const search = /thing/; +search.exec(text); + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 4`] = ` +{ + "code": " +const text = 'something'; +const search = 'thing'; +text.match(search); + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 11, + "line": 4, + }, + "start": { + "column": 6, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +const text = 'something'; +const search = 'thing'; +RegExp(search).exec(text); + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 5`] = ` +{ + "code": " +function f(s: 'a' | 'b') { + s.match('a'); +} + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 10, + "line": 3, + }, + "start": { + "column": 5, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +function f(s: 'a' | 'b') { + /a/.exec(s); +} + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 6`] = ` +{ + "code": " +type SafeString = string & { __HTML_ESCAPED__: void }; +function f(s: SafeString) { + s.match(/thing/); +} + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 10, + "line": 4, + }, + "start": { + "column": 5, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +type SafeString = string & { __HTML_ESCAPED__: void }; +function f(s: SafeString) { + /thing/.exec(s); +} + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 7`] = ` +{ + "code": " +function f(s: T) { + s.match(/thing/); +} + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 10, + "line": 3, + }, + "start": { + "column": 5, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +function f(s: T) { + /thing/.exec(s); +} + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 8`] = ` +{ + "code": " +const text = 'something'; +const search = new RegExp('test', ''); +text.match(search); + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 11, + "line": 4, + }, + "start": { + "column": 6, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +const text = 'something'; +const search = new RegExp('test', ''); +search.exec(text); + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 9`] = ` +{ + "code": " +function test(pattern: string) { + 'check'.match(new RegExp(pattern, undefined)); +} + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 16, + "line": 3, + }, + "start": { + "column": 11, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 1, + "fileCount": 1, + "output": " +function test(pattern: string) { + new RegExp(pattern, undefined).exec('check'); +} + ", + "ruleCount": 1, +} +`; + +exports[`prefer-regexp-exec > invalid 10`] = ` +{ + "code": " +function temp(text: string): void { + text.match(new RegExp(\`\${'hello'}\`)); + text.match(new RegExp(\`\${'hello'.toString()}\`)); +} + ", + "diagnostics": [ + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 13, + "line": 3, + }, + "start": { + "column": 8, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + { + "message": "Use the \`RegExp#exec()\` method instead.", + "messageId": "regExpExecOverStringMatch", + "range": { + "end": { + "column": 13, + "line": 4, + }, + "start": { + "column": 8, + "line": 4, + }, + }, + "ruleName": "@typescript-eslint/prefer-regexp-exec", + }, + ], + "errorCount": 2, + "fileCount": 1, + "output": " +function temp(text: string): void { + new RegExp(\`\${'hello'}\`).exec(text); + new RegExp(\`\${'hello'.toString()}\`).exec(text); +} + ", + "ruleCount": 1, +} +`; diff --git a/rslint.json b/rslint.json index e1f6235c1..f3ba40a8a 100644 --- a/rslint.json +++ b/rslint.json @@ -54,6 +54,7 @@ "@typescript-eslint/require-await": "warn", "@typescript-eslint/prefer-readonly": "warn", "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/prefer-regexp-exec": "off", "no-console": "warn" }, "plugins": ["@typescript-eslint"] diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index 7fe9f2aea..1925962d5 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -160,7 +160,8 @@ marginx marginy Readonlys exclam +dlclark relint relints relinting -lifecycles \ No newline at end of file +lifecycles