rules_of_hooks

package
v0.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var RulesOfHooksRule = rule.Rule{
	Name: "react-hooks/rules-of-hooks",
	Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
		additionalRe := getAdditionalEffectHooks(ctx.Settings)
		sf := ctx.SourceFile
		registry := collectEffectEventBindings(sf)
		tc := ctx.TypeChecker

		reportedIdentifiers := map[int]bool{}

		report := func(node *ast.Node, msg string) {
			if hasFlowSuppression(sf, node) {
				return
			}
			ctx.ReportNode(node, rule.RuleMessage{Description: msg})
		}

		return rule.RuleListeners{
			ast.KindCallExpression: func(node *ast.Node) {
				call := node.AsCallExpression()
				callee := call.Expression

				if isUseEffectEventCallee(callee) {
					p := node.Parent
					if p != nil && p.Kind != ast.KindVariableDeclaration && p.Kind != ast.KindExpressionStatement {
						report(node, useEffectEventErrorMessage("", false))
					}
				}

				if !isHookCallee(callee) {
					return
				}

				hookText := makeHookText(callee, sf)
				fn := findEnclosingFunction(node)
				isUse := isUseIdentifier(callee)

				if fn != nil && isPrecededByDirectTerminator(node, fn) {
					return
				}

				if isUse && isInsideTryCatchOfFunction(node, fn) {
					report(callee, fmt.Sprintf(`React Hook "%s" cannot be called in a try/catch block.`, hookText))
				}

				inAnyLoop := false
				if !isUse {
					inAnyLoop = isInsideLoopOfFunction(node, fn)
					if inAnyLoop {
						report(callee, fmt.Sprintf(
							`React Hook "%s" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.`,
							hookText,
						))
					}
				}

				if fn == nil {
					report(callee, fmt.Sprintf(
						`React Hook "%s" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.`,
						hookText,
					))
					return
				}

				if isComponentOrHookFn(fn) {
					if hasAsyncModifier(fn) {
						report(callee, fmt.Sprintf(`React Hook "%s" cannot be called in an async function.`, hookText))
						return
					}
					if isUse || inAnyLoop {
						return
					}
					cond := isConditional(node, fn)
					early := hasEarlyReturnBefore(node, fn)
					labeled := hasLabeledBreakBefore(node, fn)
					tryWithPrior := isInsideTryBlockWithPriorStmt(node, fn)
					if cond || early || labeled || tryWithPrior {
						suffix := ""
						if early {
							suffix = " Did you accidentally call a React Hook after an early return?"
						}
						report(callee, fmt.Sprintf(
							`React Hook "%s" is called conditionally. React Hooks must be called in the exact same order in every component render.%s`,
							hookText, suffix,
						))
					}
					return
				}

				if isClassMember(fn) {
					report(callee, fmt.Sprintf(
						`React Hook "%s" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.`,
						hookText,
					))
					return
				}

				if name := getFunctionName(fn); name != nil {
					report(callee, fmt.Sprintf(
						`React Hook "%s" is called in function "%s" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".`,
						hookText, nameText(name, sf),
					))
					return
				}

				if !isUse && isInsideComponentOrHook(node) {
					report(callee, fmt.Sprintf(
						`React Hook "%s" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.`,
						hookText,
					))
				}
			},

			ast.KindIdentifier: func(node *ast.Node) {
				if !isReferenceIdentifier(node) {
					return
				}
				name := node.AsIdentifier().Text

				container := resolveBindingViaSymbol(tc, node)
				if container == nil {
					container = registry.resolveContainer(node, name)
				}
				if container == nil {
					return
				}
				if isInsideEffectArgument(node, additionalRe) {
					return
				}
				called := false
				if p := node.Parent; p != nil && p.Kind == ast.KindCallExpression {
					if p.AsCallExpression().Expression == node {
						called = true
					}
				}
				if reportedIdentifiers[node.Pos()] {
					return
				}
				reportedIdentifiers[node.Pos()] = true
				if hasFlowSuppression(sf, node) {
					return
				}
				ctx.ReportNode(node, rule.RuleMessage{Description: useEffectEventErrorMessage(name, called)})
			},
		}
	},
}

RulesOfHooksRule is the rslint port of upstream `react-hooks/rules-of-hooks`.

Departure from upstream: rslint has no CodePathAnalyzer, so the conditional / loop / early-return / cycled-segment detections are AST-shape based rather than CFG-based. The implementation matches upstream observably for every test in upstream's `valid` and `invalid` suites that doesn't rely on BigInt-precise path counting (and we add a `hasLabeledBreakBefore` walk for the `label: { if (cond) break label; useFoo(); }` case).

Identifier resolution for `useEffectEvent` references uses the TypeChecker when available (correct under shadowing / aliasing); falls back to a per-container name registry when `ctx.TypeChecker` is nil.

`$FlowFixMe[react-rule-hook]` suppression on the preceding line is honored, mirroring upstream's `hasFlowSuppression` byte-for-byte.

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL