Skip to content

Commit af7e163

Browse files
authored
fix: align react hooks rule options (#1220)
1 parent fb01fc6 commit af7e163

5 files changed

Lines changed: 17 additions & 117 deletions

File tree

internal/plugins/react_hooks/react_hooksutil/react_hooksutil.go

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"regexp"
1818
"strings"
1919

20-
"github.com/dlclark/regexp2"
2120
"github.com/microsoft/typescript-go/shim/ast"
2221
"github.com/web-infra-dev/rslint/internal/utils"
2322
)
@@ -29,10 +28,6 @@ const (
2928
CompilerReactFunctionHook CompilerReactFunctionType = "Hook"
3029
)
3130

32-
type CompilerFunctionOptions struct {
33-
HookPattern *regexp2.Regexp
34-
}
35-
3631
// hookNameTailRegex matches the suffix part of a hook identifier:
3732
// after the leading `use`, the next character must be uppercase Latin
3833
// or a digit. Mirrors upstream's `/^use[A-Z0-9]/`.
@@ -158,24 +153,18 @@ func IsHookCallee(node *ast.Node) bool {
158153

159154
// IsCompilerHookCallee is the React Compiler variant of IsHookCallee:
160155
// it accepts `useFoo` / `Namespace.useFoo`, but not the bare `use`.
161-
func IsCompilerHookCallee(node *ast.Node, hookPattern *regexp2.Regexp) bool {
156+
func IsCompilerHookCallee(node *ast.Node) bool {
162157
if node == nil {
163158
return false
164159
}
165-
isHookName := func(name string) bool {
166-
if hookPattern != nil {
167-
return utils.Regexp2MatchString(hookPattern, name)
168-
}
169-
return IsCompilerHookName(name)
170-
}
171160
n := ast.SkipParentheses(node)
172161
switch n.Kind {
173162
case ast.KindIdentifier:
174-
return isHookName(n.AsIdentifier().Text)
163+
return IsCompilerHookName(n.AsIdentifier().Text)
175164
case ast.KindPropertyAccessExpression:
176165
pae := n.AsPropertyAccessExpression()
177166
prop := pae.Name()
178-
if prop == nil || prop.Kind != ast.KindIdentifier || !isHookName(prop.AsIdentifier().Text) {
167+
if prop == nil || prop.Kind != ast.KindIdentifier || !IsCompilerHookName(prop.AsIdentifier().Text) {
179168
return false
180169
}
181170
obj := ast.SkipParentheses(pae.Expression)
@@ -199,25 +188,25 @@ func IsCompilerFunctionKind(node *ast.Node) bool {
199188
// directly creates JSX or calls a hook, has component-like parameters, and does
200189
// not return obviously non-ReactNode values; a hook-like function must directly
201190
// create JSX or call a hook; memo/forwardRef callbacks are components.
202-
func GetCompilerReactFunctionType(fn *ast.Node, opts CompilerFunctionOptions) CompilerReactFunctionType {
191+
func GetCompilerReactFunctionType(fn *ast.Node) CompilerReactFunctionType {
203192
name := GetFunctionName(fn)
204193
if name != nil && name.Kind == ast.KindIdentifier && IsComponentNameStr(name.AsIdentifier().Text) {
205-
if CallsHooksOrCreatesJsx(fn, opts.HookPattern) &&
194+
if CallsHooksOrCreatesJsx(fn) &&
206195
IsValidCompilerComponentParams(fn) &&
207196
!ReturnsCompilerNonNode(fn) {
208197
return CompilerReactFunctionComponent
209198
}
210199
return ""
211200
}
212-
if name != nil && IsCompilerHookCallee(name, opts.HookPattern) {
213-
if CallsHooksOrCreatesJsx(fn, opts.HookPattern) {
201+
if name != nil && IsCompilerHookCallee(name) {
202+
if CallsHooksOrCreatesJsx(fn) {
214203
return CompilerReactFunctionHook
215204
}
216205
return ""
217206
}
218207
if ast.IsFunctionExpressionOrArrowFunction(fn) {
219208
if IsForwardRefOrMemoCallback(fn, "forwardRef") || IsForwardRefOrMemoCallback(fn, "memo") {
220-
if CallsHooksOrCreatesJsx(fn, opts.HookPattern) {
209+
if CallsHooksOrCreatesJsx(fn) {
221210
return CompilerReactFunctionComponent
222211
}
223212
}
@@ -228,7 +217,7 @@ func GetCompilerReactFunctionType(fn *ast.Node, opts CompilerFunctionOptions) Co
228217
// CallsHooksOrCreatesJsx reports whether `fn` directly creates JSX or directly
229218
// calls a compiler hook. Nested function bodies are skipped, matching React
230219
// Compiler's traversal boundary.
231-
func CallsHooksOrCreatesJsx(fn *ast.Node, hookPattern *regexp2.Regexp) bool {
220+
func CallsHooksOrCreatesJsx(fn *ast.Node) bool {
232221
found := false
233222
var walk func(*ast.Node)
234223
walk = func(node *ast.Node) {
@@ -243,7 +232,7 @@ func CallsHooksOrCreatesJsx(fn *ast.Node, hookPattern *regexp2.Regexp) bool {
243232
return
244233
}
245234
if ast.IsCallExpression(node) {
246-
if IsCompilerHookCallee(node.AsCallExpression().Expression, hookPattern) {
235+
if IsCompilerHookCallee(node.AsCallExpression().Expression) {
247236
found = true
248237
return
249238
}

internal/plugins/react_hooks/rules/component_hook_factories/component_hook_factories.go

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,11 @@ import (
44
"fmt"
55
"strings"
66

7-
"github.com/dlclark/regexp2"
87
"github.com/microsoft/typescript-go/shim/ast"
98
"github.com/web-infra-dev/rslint/internal/plugins/react_hooks/react_hooksutil"
109
"github.com/web-infra-dev/rslint/internal/rule"
11-
"github.com/web-infra-dev/rslint/internal/utils"
1210
)
1311

14-
type componentHookFactoriesOptions struct {
15-
hookPattern *regexp2.Regexp
16-
}
17-
1812
// ComponentHookFactoriesRule is the rslint port of upstream
1913
// `react-hooks/component-hook-factories`.
2014
//
@@ -25,9 +19,8 @@ type componentHookFactoriesOptions struct {
2519
var ComponentHookFactoriesRule = rule.Rule{
2620
Name: "react-hooks/component-hook-factories",
2721
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
28-
opts := parseOptions(options)
2922
check := func(node *ast.Node) {
30-
validateNoDynamicallyCreatedComponentsOrHooks(ctx, node, opts)
23+
validateNoDynamicallyCreatedComponentsOrHooks(ctx, node)
3124
}
3225
return rule.RuleListeners{
3326
ast.KindFunctionDeclaration: check,
@@ -37,27 +30,7 @@ var ComponentHookFactoriesRule = rule.Rule{
3730
},
3831
}
3932

40-
func parseOptions(raw any) componentHookFactoriesOptions {
41-
optsMap := utils.GetOptionsMap(raw)
42-
if optsMap == nil {
43-
return componentHookFactoriesOptions{}
44-
}
45-
environment, ok := optsMap["environment"].(map[string]interface{})
46-
if !ok {
47-
return componentHookFactoriesOptions{}
48-
}
49-
pattern, ok := environment["hookPattern"].(string)
50-
if !ok || pattern == "" {
51-
return componentHookFactoriesOptions{}
52-
}
53-
compiled, err := utils.CompileRegexp2(pattern, utils.JSRegexOptions)
54-
if err != nil {
55-
return componentHookFactoriesOptions{}
56-
}
57-
return componentHookFactoriesOptions{hookPattern: compiled}
58-
}
59-
60-
func validateNoDynamicallyCreatedComponentsOrHooks(ctx rule.RuleContext, fn *ast.Node, opts componentHookFactoriesOptions) {
33+
func validateNoDynamicallyCreatedComponentsOrHooks(ctx rule.RuleContext, fn *ast.Node) {
6134
if isInsideClass(fn) {
6235
return
6336
}
@@ -72,9 +45,7 @@ func validateNoDynamicallyCreatedComponentsOrHooks(ctx rule.RuleContext, fn *ast
7245
}
7346

7447
walkDirectChildren(fn, func(nestedFn *ast.Node) {
75-
nestedType := react_hooksutil.GetCompilerReactFunctionType(nestedFn, react_hooksutil.CompilerFunctionOptions{
76-
HookPattern: opts.hookPattern,
77-
})
48+
nestedType := react_hooksutil.GetCompilerReactFunctionType(nestedFn)
7849
if nestedType == "" {
7950
return
8051
}

internal/plugins/react_hooks/rules/component_hook_factories/component_hook_factories.md

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,36 +45,6 @@ function Button({ color, children }) {
4545
}
4646
```
4747

48-
## Options
49-
50-
```json
51-
{
52-
"react-hooks/component-hook-factories": [
53-
"error",
54-
{
55-
"environment": {
56-
"hookPattern": "^signal[A-Z]"
57-
}
58-
}
59-
]
60-
}
61-
```
62-
63-
- `environment.hookPattern`: A JavaScript regular expression pattern for
64-
custom Hook names. Function names that match this pattern are treated as
65-
Hooks when the rule classifies nested factories. When omitted or invalid,
66-
the rule falls back to React's default Hook naming convention.
67-
68-
Examples of **incorrect** code for this rule with `{ "environment": { "hookPattern": "^signal[A-Z]" } }`:
69-
70-
```javascript
71-
function createSignalHook(source) {
72-
return function signalValue() {
73-
return signalRead(source);
74-
};
75-
}
76-
```
77-
7848
## Original Documentation
7949

8050
- [react.dev — component-hook-factories](https://react.dev/reference/eslint-plugin-react-hooks/lints/component-hook-factories)

internal/plugins/react_hooks/rules/component_hook_factories/component_hook_factories_extras_test.go

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,14 @@ function makeComputedHook() {
123123
};
124124
}
125125
`, Tsx: true},
126-
// ---- Branch lock-in: malformed hookPattern is ignored like upstream option parsing ----
127-
{
128-
Code: `
126+
// ---- Upstream parity: non-use factory names are not custom Hooks. ----
127+
{Code: `
129128
function createSignalHook(source) {
130129
return function signalValue() {
131130
return signalRead(source);
132131
};
133132
}
134-
`,
135-
Tsx: true,
136-
Options: map[string]interface{}{"environment": map[string]interface{}{"hookPattern": "["}},
137-
},
133+
`, Tsx: true},
138134
// ---- Dimension 4: class declarations/expressions are skipped by the React Compiler traversal ----
139135
{Code: `
140136
class Holder {
@@ -366,32 +362,6 @@ function createWrapped(kind) {
366362
{MessageId: "componentHookFactory"},
367363
},
368364
},
369-
// ---- Dimension 4: custom hookPattern option ----
370-
{
371-
Code: `
372-
function createSignalHook(source) {
373-
return function signalValue() {
374-
return signalRead(source);
375-
};
376-
}
377-
`,
378-
Tsx: true,
379-
Options: map[string]interface{}{"environment": map[string]interface{}{"hookPattern": "(?=signal)signal[A-Z]"}},
380-
Errors: []rule_tester.InvalidTestCaseError{{MessageId: "componentHookFactory"}},
381-
},
382-
// ---- Branch lock-in: array-wrapped hookPattern option matches context.options shape ----
383-
{
384-
Code: `
385-
function createTrackedHook(source) {
386-
return function trackValue() {
387-
return trackRead(source);
388-
};
389-
}
390-
`,
391-
Tsx: true,
392-
Options: []interface{}{map[string]interface{}{"environment": map[string]interface{}{"hookPattern": "^track[A-Z]"}}},
393-
Errors: []rule_tester.InvalidTestCaseError{{MessageId: "componentHookFactory"}},
394-
},
395365
// ---- Real-user: library helper that returns an endpoint hook ----
396366
{
397367
Code: `

internal/plugins/react_hooks/rules/globals/globals.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func (state *globalsState) activeFunctionKind(fn *ast.Node) activeFunctionKind {
204204
}
205205

206206
func isCompilerRenderFunction(fn *ast.Node) bool {
207-
return react_hooksutil.GetCompilerReactFunctionType(fn, react_hooksutil.CompilerFunctionOptions{}) != ""
207+
return react_hooksutil.GetCompilerReactFunctionType(fn) != ""
208208
}
209209

210210
func isNonRenderCallback(fn *ast.Node) bool {

0 commit comments

Comments
 (0)