diff --git a/internal/linter/linter.go b/internal/linter/linter.go
index f5e50722..c8b5a5c1 100644
--- a/internal/linter/linter.go
+++ b/internal/linter/linter.go
@@ -177,6 +177,7 @@ func RunLinterInProgram(program *compiler.Program, allowFiles []string, skipFile
var childVisitor ast.Visitor
var patternVisitor func(node *ast.Node)
patternVisitor = func(node *ast.Node) {
+ runListeners(rule.WildcardTokenKind, node)
runListeners(node.Kind, node)
kind := rule.ListenerOnAllowPattern(node.Kind)
runListeners(kind, node)
@@ -200,8 +201,10 @@ func RunLinterInProgram(program *compiler.Program, allowFiles []string, skipFile
runListeners(rule.ListenerOnExit(kind), node)
runListeners(rule.ListenerOnExit(node.Kind), node)
+ runListeners(rule.WildcardExitTokenKind, node)
}
childVisitor = func(node *ast.Node) bool {
+ runListeners(rule.WildcardTokenKind, node)
runListeners(node.Kind, node)
switch node.Kind {
@@ -222,10 +225,11 @@ func RunLinterInProgram(program *compiler.Program, allowFiles []string, skipFile
}
runListeners(rule.ListenerOnExit(node.Kind), node)
+ runListeners(rule.WildcardExitTokenKind, node)
return false
}
- file.Node.ForEachChild(childVisitor)
+ patternVisitor(&file.Node)
clear(registeredListeners)
}
diff --git a/internal/plugins/import/plugin.go b/internal/plugins/import/plugin.go
index 187adec4..1baaec4f 100644
--- a/internal/plugins/import/plugin.go
+++ b/internal/plugins/import/plugin.go
@@ -1,2 +1,3 @@
package import_plugin
-const PLUGIN_NAME = "eslint-plugin-import"
\ No newline at end of file
+
+const PLUGIN_NAME = "eslint-plugin-import"
diff --git a/internal/plugins/react_hooks/all.go b/internal/plugins/react_hooks/all.go
new file mode 100644
index 00000000..aaef3d57
--- /dev/null
+++ b/internal/plugins/react_hooks/all.go
@@ -0,0 +1,12 @@
+package react_hooks_plugin
+
+import (
+ "github.com/web-infra-dev/rslint/internal/plugins/react_hooks/rules/rules_of_hooks"
+ "github.com/web-infra-dev/rslint/internal/rule"
+)
+
+func GetAllRules() []rule.Rule {
+ return []rule.Rule{
+ rules_of_hooks.RulesOfHooksRule,
+ }
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/break_context.go b/internal/plugins/react_hooks/code_path_analysis/break_context.go
new file mode 100644
index 00000000..446239f9
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/break_context.go
@@ -0,0 +1,141 @@
+package code_path_analysis
+
+type BreakContext struct {
+ upper *BreakContext
+ breakable bool
+ label string
+ brokenForkContext *ForkContext
+}
+
+func NewBreakContext(state *CodePathState, breakable bool, label string) *BreakContext {
+ return &BreakContext{
+ upper: state.breakContext,
+ breakable: breakable,
+ label: label,
+ brokenForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ }
+}
+
+// Creates new context for BreakStatement.
+func (s *CodePathState) PushBreakContext(breakable bool, label string) *BreakContext {
+ s.breakContext = NewBreakContext(s, breakable, label)
+ return s.breakContext
+}
+
+// Removes the top item of the break context stack.
+func (s *CodePathState) PopBreakContext() *BreakContext {
+ context := s.breakContext
+ forkContext := s.forkContext
+
+ s.breakContext = context.upper
+
+ // Process this context here for other than switches and loops.
+ if !context.breakable {
+ brokenForkContext := context.brokenForkContext
+
+ if !brokenForkContext.IsEmpty() {
+ brokenForkContext.Add(forkContext.Head())
+ forkContext.ReplaceHead(brokenForkContext.MakeNext(0, -1))
+ }
+ }
+
+ return context
+}
+
+// Makes a path for a `break` statement.
+// It registers the head segment to a context of `break`.
+// It makes new unreachable segment, then it set the head with the segment.
+func (s *CodePathState) MakeBreak(label string) {
+ forkContext := s.forkContext
+
+ if !forkContext.IsReachable() {
+ return
+ }
+
+ context := s.getBreakContext(label)
+
+ if context != nil {
+ context.brokenForkContext.Add(forkContext.Head())
+ }
+
+ forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
+}
+
+func (s *CodePathState) getBreakContext(label string) *BreakContext {
+ context := s.breakContext
+
+ for context != nil {
+ if label == "" && context.breakable {
+ return context
+ } else if context.label == label {
+ return context
+ }
+
+ context = context.upper
+ }
+
+ return nil
+}
+
+// Makes a path for a `continue` statement.
+//
+// It makes a looping path.
+// It makes new unreachable segment, then it set the head with the segment.
+func (s *CodePathState) MakeContinue(label string) {
+ forkContext := s.forkContext
+
+ if !forkContext.IsReachable() {
+ return
+ }
+
+ context := s.getContinueContext(label)
+
+ if context != nil {
+ if context.continueDestSegments != nil {
+ s.MakeLooped(forkContext.Head(), context.continueDestSegments)
+
+ // If the context is a for-in/of loop, this effects a break also.
+ if context.kind == ForInStatement || context.kind == ForOfStatement {
+ context.brokenForkContext.Add(forkContext.Head())
+ }
+ } else {
+ context.continueForkContext.Add(forkContext.Head())
+ }
+ }
+ forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
+}
+
+// Gets a loop-context for a `continue` statement.
+func (s *CodePathState) getContinueContext(label string) *LoopContext {
+ if label == "" {
+ return s.loopContext
+ }
+
+ context := s.loopContext
+ for context != nil {
+ if context.label == label {
+ return context
+ }
+ context = context.upper
+ }
+
+ return nil
+}
+
+// Makes a path for a `return` statement.
+//
+// It registers the head segment to a context of `return`.
+// It makes new unreachable segment, then it set the head with the segment.
+func (s *CodePathState) MakeReturn() {
+ forkContext := s.forkContext
+
+ if forkContext.IsReachable() {
+ returnCtx := s.getReturnContext()
+ if returnCtx != nil {
+ returnCtx.returnedForkContext.Add(forkContext.Head())
+ } else {
+ s.addReturnedSegments(forkContext.Head())
+ }
+ forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
+ }
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/chain_context.go b/internal/plugins/react_hooks/code_path_analysis/chain_context.go
new file mode 100644
index 00000000..1a4b5963
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/chain_context.go
@@ -0,0 +1,51 @@
+package code_path_analysis
+
+type ChainContext struct {
+ upper *ChainContext
+ countChoiceContext int
+}
+
+func NewChainContext(state *CodePathState) *ChainContext {
+ return &ChainContext{
+ upper: state.chainContext,
+ countChoiceContext: 0,
+ }
+}
+
+// Push a new `ChainExpression` context to the stack.
+// This method is called on entering to each `ChainExpression` node.
+// This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
+func (s *CodePathState) PushChainContext() {
+ s.chainContext = NewChainContext(s)
+}
+
+// Pop a `ChainExpression` context from the stack.
+// This method is called on exiting from each `ChainExpression` node.
+// This merges all forks of the last optional chaining.
+func (s *CodePathState) PopChainContext() {
+ context := s.chainContext
+ s.chainContext = context.upper
+
+ // pop all choice contexts of this.
+ for i := context.countChoiceContext; i > 0; i-- {
+ s.PopChoiceContext()
+ }
+}
+
+// Create a choice context for optional access.
+// This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
+// This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
+func (s *CodePathState) MakeOptionalNode() {
+ if s.chainContext != nil {
+ s.chainContext.countChoiceContext += 1
+ s.PushChoiceContext("??", false)
+ }
+}
+
+// Create a fork.
+// This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
+func (s *CodePathState) MakeOptionalRight() {
+ if s.chainContext != nil {
+ s.MakeLogicalRight()
+ }
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/choice_context.go b/internal/plugins/react_hooks/code_path_analysis/choice_context.go
new file mode 100644
index 00000000..39a01e54
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/choice_context.go
@@ -0,0 +1,198 @@
+package code_path_analysis
+
+// A context for ConditionalExpression, LogicalExpression,
+// AssignmentExpression (logical assignments only), IfStatement, WhileStatement,
+// DoWhileStatement, or ForStatement.
+//
+// LogicalExpressions have cases that it goes different paths between the
+// true case and the false case.
+//
+// For Example:
+//
+// if (a || b) {
+// foo();
+// } else {
+// bar();
+// }
+//
+// In this case, `b` is evaluated always in the code path of the `else`
+// block, but it's not so in the code path of the `if` block.
+// So there are 3 paths:
+//
+// a -> foo();
+// a -> b -> foo();
+// a -> b -> bar();
+type ChoiceContext struct {
+ upper *ChoiceContext
+ kind string
+ isForkingAsResult bool
+ trueForkContext *ForkContext
+ falseForkContext *ForkContext
+ qqForkContext *ForkContext
+ processed bool
+}
+
+func NewChoiceContext(state *CodePathState, kind string, isForkingAsResult bool) *ChoiceContext {
+ return &ChoiceContext{
+ upper: state.choiceContext,
+ kind: kind,
+ isForkingAsResult: isForkingAsResult,
+ trueForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ falseForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ qqForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ processed: false,
+ }
+}
+
+func (s *CodePathState) PushChoiceContext(kind string, isForkingAsResult bool) {
+ s.choiceContext = NewChoiceContext(s, kind, isForkingAsResult)
+}
+
+// Pops the last choice context and finalizes it.
+func (s *CodePathState) PopChoiceContext() *ChoiceContext {
+ context := s.choiceContext
+
+ s.choiceContext = context.upper
+
+ forkContext := s.forkContext
+ headSegments := forkContext.Head()
+
+ switch context.kind {
+ case "&&", "||", "??":
+ {
+ // If any result were not transferred from child contexts,
+ // this sets the head segments to both cases.
+ // The head segments are the path of the right-hand operand.
+ if !context.processed {
+ context.trueForkContext.Add(headSegments)
+ context.falseForkContext.Add(headSegments)
+ context.qqForkContext.Add(headSegments)
+ }
+
+ // Transfers results to upper context if this context is in
+ // test chunk.
+ if context.isForkingAsResult {
+ parentContext := s.choiceContext
+ parentContext.trueForkContext.AddAll(context.trueForkContext)
+ parentContext.falseForkContext.AddAll(context.falseForkContext)
+ parentContext.qqForkContext.AddAll(context.qqForkContext)
+ parentContext.processed = true
+
+ return context
+ }
+ }
+ case "test":
+ {
+ if !context.processed {
+ // The head segments are the path of the `if` block here.
+ // Updates the `true` path with the end of the `if` block.
+ context.trueForkContext.Clear()
+ context.trueForkContext.Add(headSegments)
+ } else {
+ // The head segments are the path of the `else` block here.
+ // Updates the `false` path with the end of the `else`
+ // block.
+ context.falseForkContext.Clear()
+ context.falseForkContext.Add(headSegments)
+ }
+ }
+ case "loop":
+ {
+ // Loops are addressed in popLoopContext().
+ // This is called from popLoopContext().
+
+ return context
+ }
+ default:
+ {
+ panic("Unreachable")
+ }
+ }
+
+ // Merges all paths.
+ prevForkContext := context.trueForkContext
+
+ prevForkContext.AddAll(context.falseForkContext)
+ forkContext.ReplaceHead(prevForkContext.MakeNext(0, -1))
+
+ return context
+}
+
+// Makes a code path segment of the right-hand operand of a logical expression.
+func (s *CodePathState) MakeLogicalRight() {
+ context := s.choiceContext
+ forkContext := s.forkContext
+
+ if context.processed {
+ // This got segments already from the child choice context.
+ // Creates the next path from own true/false fork context.
+ var prevForkContext *ForkContext
+
+ switch context.kind {
+ case "&&": // if true then go to the right-hand side.
+ prevForkContext = context.trueForkContext
+ case "||": // if false then go to the right-hand side.
+ prevForkContext = context.falseForkContext
+ case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
+ prevForkContext = context.qqForkContext
+ default:
+ panic("Unreachable")
+ }
+
+ forkContext.ReplaceHead(prevForkContext.MakeNext(0, -1))
+ prevForkContext.Clear()
+ context.processed = false
+ } else {
+ // This did not get segments from the child choice context.
+ // So addresses the head segments.
+ // The head segments are the path of the left-hand operand.
+ switch context.kind {
+ case "&&": // the false path can short-circuit.
+ context.falseForkContext.Add(forkContext.Head())
+ case "||": // the true path can short-circuit.
+ context.trueForkContext.Add(forkContext.Head())
+ case "??": // both can short-circuit.
+ context.trueForkContext.Add(forkContext.Head())
+ context.falseForkContext.Add(forkContext.Head())
+ default:
+ panic("Unreachable")
+ }
+
+ forkContext.ReplaceHead(forkContext.MakeNext(-1, -1))
+ }
+}
+
+// Makes a code path segment of the `if` block.
+func (s *CodePathState) MakeIfConsequent() {
+ context := s.choiceContext
+ forkContext := s.forkContext
+
+ // If any result were not transferred from child contexts,
+ // this sets the head segments to both cases.
+ // The head segments are the path of the test expression.
+ if !context.processed {
+ context.trueForkContext.Add(forkContext.Head())
+ context.falseForkContext.Add(forkContext.Head())
+ context.qqForkContext.Add(forkContext.Head())
+ }
+
+ context.processed = false
+
+ // Creates new path from the `true` case.
+ forkContext.ReplaceHead(context.trueForkContext.MakeNext(0, -1))
+}
+
+// Makes a code path segment of the `else` block.
+func (s *CodePathState) MakeIfAlternate() {
+ context := s.choiceContext
+ forkContext := s.forkContext
+
+ // The head segments are the path of the `if` block.
+ // Updates the `true` path with the end of the `if` block.
+ context.trueForkContext.Clear()
+ context.trueForkContext.Add(forkContext.Head())
+ context.processed = true
+
+ // Creates new path from the `false` case.
+ forkContext.ReplaceHead(context.falseForkContext.MakeNext(0, -1))
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/code_path.go b/internal/plugins/react_hooks/code_path_analysis/code_path.go
new file mode 100644
index 00000000..b90e1fad
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/code_path.go
@@ -0,0 +1,84 @@
+package code_path_analysis
+
+type CodePath struct {
+ id string // An identifier
+ origin string // The type of code path origin
+ upper *CodePath // The code path of the upper function scope
+ onLooped func(fromSegment *CodePathSegment, toSegment *CodePathSegment) // A callback function to notify looping
+ childCodePaths []*CodePath // The code paths of nested function scopes
+ state *CodePathState // The state of the code path
+}
+
+func NewCodePath(id string, origin string, upper *CodePath, onLooped func(fromSegment *CodePathSegment, toSegment *CodePathSegment)) *CodePath {
+ codePath := &CodePath{
+ id: id,
+ origin: origin,
+ upper: upper,
+ onLooped: onLooped,
+ childCodePaths: make([]*CodePath, 0),
+ state: NewCodePathState(NewIdGenerator(id+"_"), onLooped),
+ }
+ // Adds this into `childCodePaths` of `upper`.
+ if upper != nil {
+ upper.childCodePaths = append(upper.childCodePaths, codePath)
+ }
+ return codePath
+}
+
+// Getter methods for accessing private fields
+
+func (cp *CodePath) ID() string {
+ return cp.id
+}
+
+func (cp *CodePath) Origin() string {
+ return cp.origin
+}
+
+func (cp *CodePath) Upper() *CodePath {
+ return cp.upper
+}
+
+func (cp *CodePath) ChildCodePaths() []*CodePath {
+ return cp.childCodePaths
+}
+
+func (cp *CodePath) State() *CodePathState {
+ return cp.state
+}
+
+func (cp *CodePath) InitialSegment() *CodePathSegment {
+ if cp.state != nil {
+ return cp.state.InitialSegment()
+ }
+ return nil
+}
+
+func (cp *CodePath) FinalSegments() []*CodePathSegment {
+ if cp.state != nil {
+ return cp.state.FinalSegments()
+ }
+ return nil
+}
+
+func (cp *CodePath) ThrownSegments() []*CodePathSegment {
+ if cp.state != nil {
+ return cp.state.ThrownSegments()
+ }
+ return nil
+}
+
+// Helper function to check if a segment is in thrown segments
+func (cp *CodePath) HasThrownSegment(segment *CodePathSegment) bool {
+ thrownSegments := cp.ThrownSegments()
+ if thrownSegments == nil {
+ return false
+ }
+
+ for _, thrownSegment := range thrownSegments {
+ if thrownSegment.ID() == segment.ID() {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/code_path_analyzer.go b/internal/plugins/react_hooks/code_path_analysis/code_path_analyzer.go
new file mode 100644
index 00000000..2c4835c4
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/code_path_analyzer.go
@@ -0,0 +1,694 @@
+package code_path_analysis
+
+import (
+ "math"
+
+ "github.com/microsoft/typescript-go/shim/ast"
+)
+
+type CodePathAnalyzer struct {
+ currentNode *ast.Node
+ codePath *CodePath
+ idGenerator *IdGenerator
+
+ // Maintain code segment path stack as we traverse.
+ onCodePathSegmentStart func(segment *CodePathSegment, node *ast.Node)
+ onCodePathSegmentEnd func(segment *CodePathSegment, node *ast.Node)
+
+ // Maintain code path stack as we traverse.
+ onCodePathStart func(codePath *CodePath, node *ast.Node)
+ onCodePathEnd func(codePath *CodePath, node *ast.Node)
+
+ onCodePathSegmentLoop func(fromSegment *CodePathSegment, toSegment *CodePathSegment, node *ast.Node)
+}
+
+func NewCodePathAnalyzer(
+ onCodePathSegmentStart func(segment *CodePathSegment, node *ast.Node),
+ onCodePathSegmentEnd func(segment *CodePathSegment, node *ast.Node),
+ onCodePathStart func(codePath *CodePath, node *ast.Node),
+ onCodePathEnd func(codePath *CodePath, node *ast.Node),
+ onCodePathSegmentLoop func(fromSegment *CodePathSegment, toSegment *CodePathSegment, node *ast.Node),
+) *CodePathAnalyzer {
+ return &CodePathAnalyzer{
+ currentNode: nil,
+ codePath: nil,
+ idGenerator: NewIdGenerator("s"),
+ onCodePathSegmentStart: onCodePathSegmentStart,
+ onCodePathSegmentEnd: onCodePathSegmentEnd,
+ onCodePathStart: onCodePathStart,
+ onCodePathEnd: onCodePathEnd,
+ onCodePathSegmentLoop: onCodePathSegmentLoop,
+ }
+}
+
+func (analyzer *CodePathAnalyzer) State() *CodePathState {
+ codePath := analyzer.codePath
+ var state *CodePathState
+ if codePath != nil {
+ state = codePath.State()
+ }
+ return state
+}
+
+// Does the process to enter a given AST node.
+// This updates state of analysis and calls `enterNode` of the wrapped.
+func (analyzer *CodePathAnalyzer) EnterNode(node *ast.Node) {
+ analyzer.currentNode = node
+
+ // Updates the code path due to node's position in its parent node.
+ if node.Parent != nil {
+ analyzer.preprocess(node)
+ }
+
+ // Updates the code path.
+ // And emits onCodePathStart/onCodePathSegmentStart events.
+ analyzer.processCodePathToEnter(node)
+
+ analyzer.currentNode = nil
+}
+
+// Does the process to leave a given AST node.
+// This updates state of analysis and calls `leaveNode` of the wrapped.
+func (analyzer *CodePathAnalyzer) LeaveNode(node *ast.Node) {
+ analyzer.currentNode = node
+
+ analyzer.processCodePathToExit(node)
+
+ analyzer.postprocess(node)
+
+ analyzer.currentNode = nil
+}
+
+// Updates the code path due to the position of a given node in the parent node thereof.
+//
+// For example, if the node is `parent.consequent`, this creates a fork from the current path.
+func (analyzer *CodePathAnalyzer) preprocess(node *ast.Node) {
+ state := analyzer.State()
+ parent := node.Parent
+
+ switch parent.Kind {
+ // The `arguments.length == 0` case is in `postprocess` function.
+ case ast.KindCallExpression:
+ if ast.IsOptionalChain(parent) && len(parent.Arguments()) >= 1 && parent.Arguments()[0] == node {
+ state.MakeOptionalRight()
+ }
+
+ case ast.KindPropertyAccessExpression:
+ // Corresponds to ESLint's MemberExpression
+ expr := parent.AsPropertyAccessExpression()
+ if ast.IsOptionalChain(parent) && len(expr.Properties()) > 0 && expr.Properties()[0] == node {
+ state.MakeOptionalRight()
+ }
+
+ case ast.KindBinaryExpression:
+ // Handle LogicalExpression (&&, ||, ??)
+ binExpr := parent.AsBinaryExpression()
+ if binExpr.Right == node && isHandledLogicalOperator(binExpr.OperatorToken.Kind) {
+ state.MakeLogicalRight()
+ }
+
+ case ast.KindConditionalExpression:
+ // Handle ternary operator: condition ? consequent : alternate
+ condExpr := parent.AsConditionalExpression()
+ if condExpr.WhenTrue == node {
+ state.MakeIfConsequent()
+ } else if condExpr.WhenFalse == node {
+ state.MakeIfAlternate()
+ }
+
+ case ast.KindIfStatement:
+ // Handle if-else statements
+ ifStmt := parent.AsIfStatement()
+ if ifStmt.ThenStatement == node {
+ state.MakeIfConsequent()
+ } else if ifStmt.ElseStatement == node {
+ state.MakeIfAlternate()
+ }
+
+ case ast.KindCaseClause:
+ // Handle switch case body
+ caseStmts := parent.AsCaseOrDefaultClause().Statements.Nodes
+ if caseStmts[0] == node {
+ state.MakeSwitchCaseBody(false, false)
+ }
+
+ case ast.KindDefaultClause:
+ defaultStmts := parent.AsCaseOrDefaultClause().Statements.Nodes
+ if defaultStmts[0] == node {
+ state.MakeSwitchCaseBody(false, true)
+ }
+
+ case ast.KindTryStatement:
+ // Handle try-catch-finally
+ tryStmt := parent.AsTryStatement()
+ if tryStmt.CatchClause == node {
+ state.MakeCatchBlock()
+ } else if tryStmt.FinallyBlock == node {
+ state.MakeFinallyBlock()
+ }
+
+ case ast.KindWhileStatement:
+ // Handle while loops
+ whileStmt := parent.AsWhileStatement()
+ if whileStmt.Expression == node {
+ state.MakeWhileTest(getBooleanValueIfSimpleConstant(node))
+ } else if whileStmt.Statement == node {
+ state.MakeWhileBody()
+ }
+
+ case ast.KindDoStatement:
+ // Handle do-while loops
+ doStmt := parent.AsDoStatement()
+ if doStmt.Statement == node {
+ state.MakeDoWhileBody()
+ } else if doStmt.Expression == node {
+ state.MakeDoWhileTest(getBooleanValueIfSimpleConstant(node))
+ }
+
+ case ast.KindForStatement:
+ // Handle for loops
+ forStmt := parent.AsForStatement()
+ if forStmt.Condition == node {
+ state.MakeForTest(getBooleanValueIfSimpleConstant(node))
+ } else if forStmt.Incrementor == node {
+ state.MakeForUpdate()
+ } else if forStmt.Statement == node {
+ state.MakeForBody()
+ }
+
+ case ast.KindForInStatement:
+ // Handle for-in loops
+ forInStmt := parent.AsForInOrOfStatement()
+ if forInStmt.Initializer == node {
+ state.MakeForInOfLeft()
+ } else if forInStmt.Expression == node {
+ state.MakeForInOfRight()
+ } else if forInStmt.Statement == node {
+ state.MakeForInOfBody()
+ }
+
+ case ast.KindForOfStatement:
+ // Handle for-of loops
+ forOfStmt := parent.AsForInOrOfStatement()
+ if forOfStmt.Initializer == node {
+ state.MakeForInOfLeft()
+ } else if forOfStmt.Expression == node {
+ state.MakeForInOfRight()
+ } else if forOfStmt.Statement == node {
+ state.MakeForInOfBody()
+ }
+
+ case ast.KindParameter:
+ // Handle assignment patterns (destructuring with defaults)
+ parameterDecl := parent.AsParameterDeclaration()
+ if parameterDecl.Initializer == node {
+ state.PushForkContext(nil)
+ state.ForkBypassPath()
+ state.ForkPath()
+ }
+ }
+}
+
+func (analyzer *CodePathAnalyzer) processCodePathToEnter(node *ast.Node) {
+ // Special case: The right side of class field initializer is considered
+ // to be its own function, so we need to start a new code path in this case.
+ if isPropertyDefinitionValue(node) {
+ analyzer.startCodePath("class-field-initializer", node)
+
+ /*
+ * Intentional fall through because `node` needs to also be
+ * processed by the code below. For example, if we have:
+ *
+ * class Foo {
+ * a = () => {}
+ * }
+ *
+ * In this case, we also need start a second code path.
+ */
+ }
+
+ state := analyzer.State()
+ parent := node.Parent
+
+ switch node.Kind {
+ case ast.KindSourceFile:
+ analyzer.startCodePath("program", node)
+
+ case ast.KindFunctionDeclaration,
+ ast.KindFunctionExpression,
+ ast.KindArrowFunction,
+ ast.KindMethodDeclaration:
+ analyzer.startCodePath("function", node)
+
+ case ast.KindClassStaticBlockDeclaration:
+ analyzer.startCodePath("class-static-block", node)
+
+ case ast.KindCallExpression:
+ if ast.IsOptionalChain(node) {
+ state.MakeOptionalNode()
+ }
+
+ case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
+ if ast.IsOptionalChain(node) {
+ state.MakeOptionalNode()
+ }
+
+ case ast.KindBinaryExpression:
+ // Handle LogicalExpression (&&, ||, ??)
+ binExpr := node.AsBinaryExpression()
+ if isHandledLogicalOperator(binExpr.OperatorToken.Kind) {
+ state.PushChoiceContext(tokenToText[binExpr.OperatorToken.Kind], isForkingByTrueOrFalse(node))
+ } else if isLogicalAssignmentOperator(binExpr.OperatorToken.Kind) {
+ text := tokenToText[binExpr.OperatorToken.Kind]
+ // removes `=` from the end
+ text = text[:len(text)-1]
+ state.PushChoiceContext(text, isForkingByTrueOrFalse(node))
+ }
+
+ case ast.KindConditionalExpression, ast.KindIfStatement:
+ state.PushChoiceContext("test", false)
+
+ case ast.KindSwitchStatement:
+ switchStmt := node.AsSwitchStatement()
+ hasDefaultCase := false
+ for _, clause := range switchStmt.CaseBlock.AsCaseBlock().Clauses.Nodes {
+ if ast.IsDefaultClause(clause) {
+ hasDefaultCase = true
+ break
+ }
+ }
+ label := getLabel(node)
+ state.PushSwitchContext(hasDefaultCase, label)
+
+ case ast.KindTryStatement:
+ tryStmt := node.AsTryStatement()
+ hasFinalizer := tryStmt.FinallyBlock != nil
+ state.PushTryContext(hasFinalizer)
+
+ case ast.KindCaseClause:
+ // Fork if this node is after the 1st node in `cases`.
+ if parent != nil && parent.Kind == ast.KindSwitchStatement {
+ state.ForkPath()
+ }
+
+ case ast.KindWhileStatement:
+ label := getLabel(node)
+ state.PushLoopContext(WhileStatement, label)
+ case ast.KindDoStatement:
+ label := getLabel(node)
+ state.PushLoopContext(DoWhileStatement, label)
+ case ast.KindForStatement:
+ label := getLabel(node)
+ state.PushLoopContext(ForStatement, label)
+ case ast.KindForInStatement:
+ label := getLabel(node)
+ state.PushLoopContext(ForInStatement, label)
+ case ast.KindForOfStatement:
+ label := getLabel(node)
+ state.PushLoopContext(ForOfStatement, label)
+ case ast.KindLabeledStatement:
+ if !isBreakableType(node.AsLabeledStatement().Statement.Kind) {
+ state.PushBreakContext(false, node.Label().Text())
+ }
+ default:
+ // No special handling needed
+ }
+
+ // Emits onCodePathSegmentStart events if updated.
+ analyzer.forwardCurrentToHead(node)
+}
+
+// Updates the code path due to the type of a given node in leaving.
+func (analyzer *CodePathAnalyzer) processCodePathToExit(node *ast.Node) {
+ state := analyzer.State()
+ if state == nil {
+ return
+ }
+
+ dontForward := false
+
+ switch node.Kind {
+ // !!! ChainExpression
+ case ast.KindIfStatement, ast.KindConditionalExpression:
+ state.PopChoiceContext()
+
+ case ast.KindBinaryExpression:
+ // Handle LogicalExpression (&&, ||, ??)
+ binExpr := node.AsBinaryExpression()
+ if isHandledLogicalOperator(binExpr.OperatorToken.Kind) ||
+ isLogicalAssignmentOperator(binExpr.OperatorToken.Kind) {
+ state.PopChoiceContext()
+ }
+
+ case ast.KindSwitchStatement:
+ state.PopSwitchContext()
+
+ case ast.KindCaseClause:
+ // This is the same as the process at the 1st `consequent` node in preprocess function.
+ // Must do if this `consequent` is empty.
+ caseClause := node.AsCaseOrDefaultClause()
+ if len(caseClause.Statements.Nodes) == 0 {
+ isDefault := caseClause.Expression == nil
+ state.MakeSwitchCaseBody(true, isDefault)
+ }
+ if state.forkContext.IsReachable() {
+ dontForward = true
+ }
+
+ case ast.KindTryStatement:
+ state.PopTryContext()
+
+ case ast.KindBreakStatement:
+ analyzer.forwardCurrentToHead(node)
+ breakStmt := node.AsBreakStatement()
+ label := ""
+ if breakStmt.Label != nil {
+ label = breakStmt.Label.Text()
+ }
+ state.MakeBreak(label)
+ dontForward = true
+
+ case ast.KindContinueStatement:
+ analyzer.forwardCurrentToHead(node)
+ continueStmt := node.AsContinueStatement()
+ label := ""
+ if continueStmt.Label != nil {
+ label = continueStmt.Label.Text()
+ }
+ state.MakeContinue(label)
+ dontForward = true
+
+ case ast.KindReturnStatement:
+ analyzer.forwardCurrentToHead(node)
+ state.MakeReturn()
+ dontForward = true
+
+ case ast.KindThrowStatement:
+ analyzer.forwardCurrentToHead(node)
+ state.MakeThrow()
+ dontForward = true
+
+ case ast.KindIdentifier:
+ // TODO: Implement isIdentifierReference check
+ // if analyzer.isIdentifierReference(node) {
+ // state.MakeFirstThrowablePathInTryBlock()
+ // dontForward = true
+ // }
+
+ case ast.KindCallExpression, ast.KindPropertyAccessExpression, ast.KindElementAccessExpression, ast.KindNewExpression, ast.KindYieldExpression:
+ state.MakeFirstThrowablePathInTryBlock()
+
+ case ast.KindWhileStatement, ast.KindDoStatement, ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement:
+ state.PopLoopContext()
+
+ case ast.KindParameter:
+ if node.Initializer() != nil {
+ state.PopForkContext()
+ }
+
+ case ast.KindLabeledStatement:
+ if !isBreakableType(node.AsLabeledStatement().Statement.Kind) {
+ state.PopBreakContext()
+ }
+
+ default:
+ // No special handling needed
+ }
+
+ // Emits onCodePathSegmentStart events if updated.
+ if !dontForward {
+ analyzer.forwardCurrentToHead(node)
+ }
+}
+
+func (analyzer *CodePathAnalyzer) postprocess(node *ast.Node) {
+ switch node.Kind {
+ case ast.KindSourceFile,
+ ast.KindFunctionDeclaration,
+ ast.KindFunctionExpression,
+ ast.KindArrowFunction,
+ ast.KindClassStaticBlockDeclaration,
+ ast.KindMethodDeclaration:
+ analyzer.endCodePath(node)
+
+ // The `arguments.length >= 1` case is in `preprocess` function.
+ case ast.KindCallExpression:
+ callExpr := node.AsCallExpression()
+ if ast.IsOptionalChain(node) && len(callExpr.Arguments.Nodes) == 0 {
+ if analyzer.codePath != nil {
+ analyzer.codePath.state.MakeOptionalRight()
+ }
+ }
+
+ default:
+ // No special handling needed
+ }
+
+ // Special case: The right side of class field initializer is considered
+ // to be its own function, so we need to end a code path in this case.
+ if isPropertyDefinitionValue(node) {
+ analyzer.endCodePath(node)
+ }
+}
+
+func (analyzer *CodePathAnalyzer) startCodePath(origin string, node *ast.Node) {
+ codePath := analyzer.codePath
+ if codePath != nil {
+ // Emits onCodePathSegmentStart events if updated.
+ analyzer.forwardCurrentToHead(node)
+ }
+
+ // Create the code path of this scope.
+ analyzer.codePath = NewCodePath(
+ analyzer.idGenerator.Next(),
+ origin,
+ codePath,
+ analyzer.onLooped,
+ )
+
+ if analyzer.onCodePathStart != nil {
+ analyzer.onCodePathStart(codePath, node)
+ }
+}
+
+// Ends the code path for the current node.
+func (analyzer *CodePathAnalyzer) endCodePath(node *ast.Node) {
+ codePath := analyzer.codePath
+ if codePath == nil {
+ return
+ }
+
+ // Mark the current path as the final node.
+ codePath.state.MakeFinal()
+
+ // Emits onCodePathSegmentEnd event of the current segments.
+ analyzer.leaveFromCurrentSegment(node)
+
+ // Emits onCodePathEnd event of this code path.
+ if analyzer.onCodePathEnd != nil {
+ analyzer.onCodePathEnd(codePath, node)
+ }
+
+ analyzer.codePath = codePath.upper
+}
+
+func (analyzer *CodePathAnalyzer) onLooped(fromSegment *CodePathSegment, toSegment *CodePathSegment) {
+ if fromSegment.reachable && toSegment.reachable {
+ if analyzer.onCodePathSegmentLoop != nil {
+ analyzer.onCodePathSegmentLoop(fromSegment, toSegment, analyzer.currentNode)
+ }
+ }
+}
+
+// Updates the current segment with the head segment.
+// This is similar to local branches and tracking branches of git.
+//
+// To separate the current and the head is in order to not make useless segments.
+//
+// In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
+// events are fired.
+func (analyzer *CodePathAnalyzer) forwardCurrentToHead(node *ast.Node) {
+ state := analyzer.State()
+ currentSegments := state.currentSegments
+ headSegments := state.HeadSegments()
+ end := int(math.Max(float64(len(currentSegments)), float64(len(headSegments))))
+
+ if analyzer.onCodePathSegmentEnd != nil {
+ for i := range end {
+ var currentSegment *CodePathSegment
+ var headSegment *CodePathSegment
+
+ if i < len(currentSegments) {
+ currentSegment = currentSegments[i]
+ }
+ if i < len(headSegments) {
+ headSegment = headSegments[i]
+ }
+
+ if currentSegment != headSegment && currentSegment != nil {
+ if currentSegment.reachable {
+ analyzer.onCodePathSegmentEnd(currentSegment, node)
+ }
+ }
+ }
+ }
+
+ // Update state.
+ state.currentSegments = headSegments
+
+ if analyzer.onCodePathSegmentStart != nil {
+ for i := range end {
+ var currentSegment *CodePathSegment
+ var headSegment *CodePathSegment
+
+ if i < len(currentSegments) {
+ currentSegment = currentSegments[i]
+ }
+ if i < len(headSegments) {
+ headSegment = headSegments[i]
+ }
+
+ if currentSegment != headSegment && headSegment != nil {
+ markUsed(headSegment)
+ if headSegment.reachable {
+ analyzer.onCodePathSegmentStart(headSegment, node)
+ }
+ }
+ }
+ }
+}
+
+// Updates the current segment with empty.
+// This is called at the last of functions or the program.
+func (analyzer *CodePathAnalyzer) leaveFromCurrentSegment(node *ast.Node) {
+ state := analyzer.State()
+ currentSegments := state.currentSegments
+
+ for _, currentSegment := range currentSegments {
+ if currentSegment.reachable {
+ analyzer.onCodePathSegmentEnd(currentSegment, node)
+ }
+ }
+
+ state.currentSegments = make([]*CodePathSegment, 0)
+}
+
+// Checks if a given node appears as the value of a PropertyDefinition node.
+func isPropertyDefinitionValue(node *ast.Node) bool {
+ parent := node.Parent
+
+ return parent != nil && ast.IsPropertyDeclaration(parent) && parent.AsPropertyDeclaration().Initializer == node
+}
+
+// Checks whether the given logical operator is taken into account for the code path analysis.
+func isHandledLogicalOperator(operatorKind ast.Kind) bool {
+ return operatorKind == ast.KindBarBarToken || operatorKind == ast.KindAmpersandAmpersandToken || operatorKind == ast.KindQuestionQuestionToken
+}
+
+// Checks whether the given assignment operator is a logical assignment operator.
+// Logical assignments are taken into account for the code path analysis
+// because of their short-circuiting semantics.
+func isLogicalAssignmentOperator(operatorKind ast.Kind) bool {
+ return operatorKind == ast.KindAmpersandAmpersandEqualsToken || operatorKind == ast.KindBarBarEqualsToken || operatorKind == ast.KindQuestionQuestionEqualsToken
+}
+
+// Checks whether or not a given logical expression node goes different path
+// between the `true` case and the `false` case.
+func isForkingByTrueOrFalse(node *ast.Node) bool {
+ parent := node.Parent
+ if parent == nil {
+ return false
+ }
+
+ switch parent.Kind {
+ case ast.KindConditionalExpression:
+ condExpr := parent.AsConditionalExpression()
+ return condExpr.Condition == node
+
+ case ast.KindIfStatement:
+ ifStmt := parent.AsIfStatement()
+ return ifStmt.Expression == node
+
+ case ast.KindWhileStatement:
+ whileStmt := parent.AsWhileStatement()
+ return whileStmt.Expression == node
+
+ case ast.KindDoStatement:
+ doStmt := parent.AsDoStatement()
+ return doStmt.Expression == node
+
+ case ast.KindForStatement:
+ forStmt := parent.AsForStatement()
+ return forStmt.Condition == node
+
+ case ast.KindBinaryExpression:
+ binExpr := parent.AsBinaryExpression()
+ return isHandledLogicalOperator(binExpr.OperatorToken.Kind) || isLogicalAssignmentOperator(binExpr.OperatorToken.Kind)
+
+ default:
+ return false
+ }
+}
+
+// Gets the boolean value of a given literal node.
+//
+// This is used to detect infinity loops (e.g. `while (true) {}`).
+// Statements preceded by an infinity loop are unreachable if the loop didn't
+// have any `break` statement.
+func getBooleanValueIfSimpleConstant(node *ast.Node) bool {
+ if node.Kind == ast.KindTrueKeyword {
+ return true
+ }
+ if node.Kind == ast.KindFalseKeyword {
+ return false
+ }
+ if node.Kind == ast.KindNumericLiteral {
+ numLiteral := node.AsNumericLiteral()
+ // In JavaScript, any non-zero number is truthy, zero is falsy
+ if numLiteral.Text == "0" {
+ return false
+ } else {
+ return true
+ }
+ }
+ if node.Kind == ast.KindStringLiteral {
+ strLiteral := node.AsStringLiteral()
+ // In JavaScript, empty string is falsy, non-empty string is truthy
+ if strLiteral.Text == `""` || strLiteral.Text == `''` {
+ return false
+ } else {
+ return true
+ }
+ }
+ if node.Kind == ast.KindNullKeyword {
+ return false
+ }
+ // Return nil for non-literal nodes or literals we can't determine
+ return false
+}
+
+// Gets the label if the parent node of a given node is a LabeledStatement.
+func getLabel(node *ast.Node) string {
+ if ast.IsLabeledStatement(node.Parent) {
+ return node.Parent.Label().Text()
+ }
+ return ""
+}
+
+func isBreakableType(kind ast.Kind) bool {
+ switch kind {
+ case ast.KindWhileStatement, ast.KindDoStatement, ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindSwitchStatement:
+ return true
+ default:
+ return false
+ }
+}
+
+var tokenToText = map[ast.Kind]string{
+ ast.KindAmpersandAmpersandToken: "&&",
+ ast.KindBarBarToken: "||",
+ ast.KindQuestionQuestionToken: "??",
+ ast.KindAmpersandAmpersandEqualsToken: "&&=",
+ ast.KindBarBarEqualsToken: "||=",
+ ast.KindQuestionQuestionEqualsToken: "??=",
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/code_path_segment.go b/internal/plugins/react_hooks/code_path_analysis/code_path_segment.go
new file mode 100644
index 00000000..9fcfbbda
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/code_path_segment.go
@@ -0,0 +1,162 @@
+package code_path_analysis
+
+import "slices"
+
+type internalData struct {
+ used bool
+ loopedPrevSegments []*CodePathSegment
+}
+
+// A code path segment.
+type CodePathSegment struct {
+ id string // The identifier of this code path. Rules use it to store additional information of each rule.
+ nextSegments []*CodePathSegment // An array of the next segments.
+ prevSegments []*CodePathSegment // An array of the previous segments.
+ allNextSegments []*CodePathSegment // An array of the next segments. This array includes unreachable segments.
+ allPrevSegments []*CodePathSegment // An array of the previous segments. This array includes unreachable segments.
+ reachable bool // A flag which shows this is reachable.
+ internal *internalData // Internal data.
+}
+
+func NewCodePathSegment(id string, allPrevSegments []*CodePathSegment, reachable bool) *CodePathSegment {
+ segment := &CodePathSegment{
+ id: id,
+ nextSegments: make([]*CodePathSegment, 0),
+ prevSegments: make([]*CodePathSegment, 0),
+ allNextSegments: make([]*CodePathSegment, 0),
+ allPrevSegments: allPrevSegments,
+ reachable: reachable,
+ internal: &internalData{
+ used: false,
+ loopedPrevSegments: make([]*CodePathSegment, 0),
+ },
+ }
+
+ for _, prevSegment := range segment.allPrevSegments {
+ if prevSegment.reachable {
+ segment.prevSegments = append(segment.prevSegments, prevSegment)
+ }
+ }
+
+ return segment
+}
+
+// Creates the root segment.
+func NewRootCodePathSegment(id string) *CodePathSegment {
+ return NewCodePathSegment(id, []*CodePathSegment{} /*allPrevSegments*/, true /*reachable*/)
+}
+
+// Creates a segment that follows given segments.
+func NewNextCodePathSegment(id string, allPrevSegments []*CodePathSegment) *CodePathSegment {
+ reachable := false
+ for _, segment := range allPrevSegments {
+ if segment.reachable {
+ reachable = true
+ break
+ }
+ }
+ return NewCodePathSegment(id, flattenUnusedSegments(allPrevSegments), reachable)
+}
+
+// Creates an unreachable segment that follows given segments.
+func NewUnreachableCodePathSegment(id string, allPrevSegments []*CodePathSegment) *CodePathSegment {
+ segment := NewCodePathSegment(id, flattenUnusedSegments(allPrevSegments), false /*reachable*/)
+
+ // In `if (a) return a; foo();` case, the unreachable segment preceded by
+ // the return statement is not used but must not be remove.
+ markUsed(segment)
+ return segment
+}
+
+// Creates a segment that follows given segments. This factory method does not connect with `allPrevSegments`. But this inherits `reachable` flag.
+func NewDisconnectedCodePathSegment(id string, allPrevSegments []*CodePathSegment) *CodePathSegment {
+ isReachable := false
+ for _, prevSegment := range allPrevSegments {
+ if prevSegment.reachable {
+ isReachable = true
+ break
+ }
+ }
+ return NewCodePathSegment(id, []*CodePathSegment{}, isReachable)
+}
+
+// Checks a given previous segment is coming from the end of a loop.
+func (cps *CodePathSegment) IsLoopedPrevSegment(segment *CodePathSegment) bool {
+ return slices.Contains(cps.internal.loopedPrevSegments, segment)
+}
+
+// Replaces unused segments with the previous segments of each unused segment.
+func flattenUnusedSegments(segments []*CodePathSegment) []*CodePathSegment {
+ done := make(map[string]bool)
+ retv := make([]*CodePathSegment, 0)
+ for _, segment := range segments {
+ // Ignores duplicated.
+ if done[segment.id] {
+ continue
+ }
+
+ if !segment.internal.used {
+ for _, prevSegment := range segment.allPrevSegments {
+ if !done[prevSegment.id] {
+ done[prevSegment.id] = true
+ retv = append(retv, prevSegment)
+ }
+ }
+ } else {
+ done[segment.id] = true
+ retv = append(retv, segment)
+ }
+ }
+ return retv
+}
+
+// Makes a given segment being used.
+func markUsed(segment *CodePathSegment) {
+ if segment.internal.used {
+ return
+ }
+
+ segment.internal.used = true
+
+ if segment.reachable {
+ for _, prevSegment := range segment.allPrevSegments {
+ prevSegment.allNextSegments = append(prevSegment.allNextSegments, segment)
+ prevSegment.nextSegments = append(prevSegment.nextSegments, segment)
+ }
+ } else {
+ for _, prevSegment := range segment.allPrevSegments {
+ prevSegment.allNextSegments = append(prevSegment.allNextSegments, segment)
+ }
+ }
+}
+
+// Marks a previous segment as looped.
+func markPrevSegmentAsLooped(segment *CodePathSegment, prevSegment *CodePathSegment) {
+ segment.internal.loopedPrevSegments = append(segment.internal.loopedPrevSegments, prevSegment)
+}
+
+// Getter methods for accessing private fields
+
+func (cps *CodePathSegment) ID() string {
+ return cps.id
+}
+
+func (cps *CodePathSegment) NextSegments() []*CodePathSegment {
+ return cps.nextSegments
+}
+
+func (cps *CodePathSegment) PrevSegments() []*CodePathSegment {
+ return cps.prevSegments
+}
+
+func (cps *CodePathSegment) AllNextSegments() []*CodePathSegment {
+ return cps.allNextSegments
+}
+
+func (cps *CodePathSegment) AllPrevSegments() []*CodePathSegment {
+ return cps.allPrevSegments
+}
+
+func (cps *CodePathSegment) Reachable() bool {
+ return cps.reachable
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/code_path_state.go b/internal/plugins/react_hooks/code_path_analysis/code_path_state.go
new file mode 100644
index 00000000..5ca6def5
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/code_path_state.go
@@ -0,0 +1,147 @@
+package code_path_analysis
+
+type CodePathState struct {
+ idGenerator *IdGenerator // idGenerator An id generator to generate id for code
+ forkContext *ForkContext
+ notifyLooped func(fromSegment *CodePathSegment, toSegment *CodePathSegment)
+ choiceContext *ChoiceContext
+ chainContext *ChainContext
+ breakContext *BreakContext
+ switchContext *SwitchContext
+ tryContext *TryContext
+ loopContext *LoopContext
+
+ currentSegments []*CodePathSegment
+ initialSegment *CodePathSegment
+ finalSegments []*CodePathSegment
+ returnedSegments []*CodePathSegment
+ thrownSegments []*CodePathSegment
+}
+
+func NewCodePathState(idGenerator *IdGenerator, onLooped func(fromSegment *CodePathSegment, toSegment *CodePathSegment)) *CodePathState {
+ forkContext := NewRootForkContext(idGenerator)
+ return &CodePathState{
+ idGenerator: idGenerator,
+ notifyLooped: onLooped,
+ forkContext: forkContext,
+ currentSegments: make([]*CodePathSegment, 0),
+ initialSegment: forkContext.Head()[0],
+ finalSegments: make([]*CodePathSegment, 0),
+ returnedSegments: make([]*CodePathSegment, 0),
+ thrownSegments: make([]*CodePathSegment, 0),
+ }
+}
+
+// The head segments.
+func (s *CodePathState) HeadSegments() []*CodePathSegment {
+ return s.forkContext.Head()
+}
+
+// The parent forking context. This is used for the root of new forks.
+func (s *CodePathState) ParentForkContext() *ForkContext {
+ current := s.forkContext
+ if current == nil {
+ return nil
+ }
+ return current.upper
+}
+
+// Creates and stacks new forking context.
+func (s *CodePathState) PushForkContext(forkLeavingPath *ForkContext) *ForkContext {
+ s.forkContext = NewEmptyForkContext(s.forkContext, forkLeavingPath)
+ return s.forkContext
+}
+
+// Pops and merges the last forking context.
+func (s *CodePathState) PopForkContext() *ForkContext {
+ lastContext := s.forkContext
+
+ s.forkContext = lastContext.upper
+ s.forkContext.ReplaceHead(lastContext.MakeNext(0, -1))
+
+ return lastContext
+}
+
+// Creates a new path.
+func (s *CodePathState) ForkPath() {
+ s.forkContext.Add(s.ParentForkContext().MakeNext(-1, -1))
+}
+
+// Creates a bypass path.
+func (s *CodePathState) ForkBypassPath() {
+ s.forkContext.Add(s.ParentForkContext().Head())
+}
+
+// Creates looping path.
+func (s *CodePathState) MakeLooped(unflattenedFromSegments []*CodePathSegment, unflattenedToSegments []*CodePathSegment) {
+ fromSegments := flattenUnusedSegments(
+ unflattenedFromSegments,
+ )
+ toSegments := flattenUnusedSegments(
+ unflattenedToSegments,
+ )
+
+ end := min(len(toSegments), len(fromSegments))
+
+ for i := range end {
+ fromSegment := fromSegments[i]
+ toSegment := toSegments[i]
+
+ if toSegment.reachable {
+ fromSegment.nextSegments = append(fromSegment.nextSegments, toSegment)
+ }
+ if fromSegment.reachable {
+ toSegment.prevSegments = append(toSegment.prevSegments, fromSegment)
+ }
+ fromSegment.allNextSegments = append(fromSegment.allNextSegments, toSegment)
+ toSegment.allPrevSegments = append(toSegment.allPrevSegments, fromSegment)
+
+ if len(toSegment.allPrevSegments) >= 2 {
+ markPrevSegmentAsLooped(toSegment, fromSegment)
+ }
+
+ s.notifyLooped(fromSegment, toSegment)
+ }
+}
+
+// Getter methods for accessing private fields
+
+func (s *CodePathState) InitialSegment() *CodePathSegment {
+ return s.initialSegment
+}
+
+func (s *CodePathState) FinalSegments() []*CodePathSegment {
+ return s.finalSegments
+}
+
+func (s *CodePathState) ThrownSegments() []*CodePathSegment {
+ return s.thrownSegments
+}
+
+func (s *CodePathState) addReturnedSegments(segments []*CodePathSegment) {
+ for _, segment := range segments {
+ s.returnedSegments = append(s.returnedSegments, segment)
+
+ for _, thrownSegment := range s.thrownSegments {
+ if thrownSegment == segment {
+ continue
+ }
+ }
+
+ s.finalSegments = append(s.finalSegments, segment)
+ }
+}
+
+func (s *CodePathState) addThrownSegments(segments []*CodePathSegment) {
+ for _, segment := range segments {
+ s.thrownSegments = append(s.thrownSegments, segment)
+
+ for _, returnSegment := range s.returnedSegments {
+ if returnSegment == segment {
+ continue
+ }
+ }
+
+ s.finalSegments = append(s.finalSegments, segment)
+ }
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/fork_context.go b/internal/plugins/react_hooks/code_path_analysis/fork_context.go
new file mode 100644
index 00000000..8b846304
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/fork_context.go
@@ -0,0 +1,197 @@
+package code_path_analysis
+
+// Manage forking
+type ForkContext struct {
+ idGenerator *IdGenerator // idGenerator An identifier generator for segments.
+ upper *ForkContext // upper An upper fork context
+ count int // count A number of parallel segments
+ segmentsList [][]*CodePathSegment
+}
+
+func NewForkContext(idGenerator *IdGenerator, upper *ForkContext, count int) *ForkContext {
+ return &ForkContext{
+ idGenerator: idGenerator,
+ upper: upper,
+ count: count,
+ segmentsList: make([][]*CodePathSegment, 0),
+ }
+}
+
+func NewEmptyForkContext(parentContext *ForkContext, forkLeavingPath *ForkContext) *ForkContext {
+ count := parentContext.count
+ if forkLeavingPath != nil {
+ count = count * 2
+ }
+ return NewForkContext(
+ parentContext.idGenerator,
+ parentContext,
+ count,
+ )
+}
+
+func NewRootForkContext(idgenerator *IdGenerator) *ForkContext {
+ context := NewForkContext(idgenerator, nil, 1)
+
+ context.Add([]*CodePathSegment{
+ NewRootCodePathSegment(idgenerator.Next()),
+ })
+
+ return context
+}
+
+// The head segments.
+func (fc *ForkContext) Head() []*CodePathSegment {
+ if len(fc.segmentsList) == 0 {
+ return []*CodePathSegment{}
+ }
+
+ return fc.segmentsList[len(fc.segmentsList)-1]
+}
+
+// A flag which shows empty.
+func (fc *ForkContext) IsEmpty() bool {
+ return len(fc.segmentsList) == 0
+}
+
+// A flag which shows reachable.
+func (fc *ForkContext) IsReachable() bool {
+ isReachable := false
+ segments := fc.Head()
+ for _, segment := range segments {
+ if segment.reachable {
+ isReachable = true
+ break
+ }
+ }
+ return isReachable
+}
+
+// Creates new segments from this context.
+func (fc *ForkContext) MakeNext(begin int, end int) []*CodePathSegment {
+ return fc.makeSegments(begin, end, NewNextCodePathSegment)
+}
+
+// Creates new segments from this context. The new segments is always unreachable.
+func (fc *ForkContext) MakeUnreachable(begin int, end int) []*CodePathSegment {
+ return fc.makeSegments(begin, end, NewUnreachableCodePathSegment)
+}
+
+// Creates new segments from this context.
+// The new segments don't have connections for previous segments.
+// But these inherit the reachable flag from this context.
+func (fc *ForkContext) MakeDisconnected(begin int, end int) []*CodePathSegment {
+ return fc.makeSegments(begin, end, NewDisconnectedCodePathSegment)
+}
+
+// Creates new segments from the specific range of `context.segmentsList`.
+//
+// When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
+// `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
+// This `h` is from `b`, `d`, and `f`.
+func (fc *ForkContext) makeSegments(begin int, end int, create func(id string, allPrevSegments []*CodePathSegment) *CodePathSegment) []*CodePathSegment {
+ list := fc.segmentsList
+
+ normalizedBegin := begin
+ if begin < 0 {
+ normalizedBegin = len(list) + begin
+ }
+ normalizedEnd := end
+ if end < 0 {
+ normalizedEnd = len(list) + end
+ }
+
+ segments := make([]*CodePathSegment, 0)
+
+ for i := range fc.count {
+ allPrevSegments := make([]*CodePathSegment, 0)
+ for j := normalizedBegin; j <= normalizedEnd; j++ {
+ allPrevSegments = append(allPrevSegments, list[j][i])
+ }
+
+ segment := create(fc.idGenerator.Next(), allPrevSegments)
+ segments = append(segments, segment)
+ }
+
+ return segments
+}
+
+func (fc *ForkContext) mergeExtraSegments(segments []*CodePathSegment) []*CodePathSegment {
+ currentSegments := segments
+
+ for len(segments) > fc.count {
+ merged := make([]*CodePathSegment, 0)
+
+ length := len(currentSegments) / 2
+ for i := range length {
+ segment := NewNextCodePathSegment(
+ fc.idGenerator.Next(),
+ []*CodePathSegment{
+ currentSegments[i],
+ currentSegments[i+length],
+ },
+ )
+ merged = append(merged, segment)
+ }
+
+ currentSegments = merged
+ }
+
+ return currentSegments
+}
+
+// Adds segments into this context. The added segments become the head.
+func (fc *ForkContext) Add(segments []*CodePathSegment) {
+ fc.segmentsList = append(
+ fc.segmentsList,
+ fc.mergeExtraSegments(segments),
+ )
+}
+
+// Replaces the head segments with given segments. The current head segments are removed.
+func (fc *ForkContext) ReplaceHead(segments []*CodePathSegment) {
+ if len(fc.segmentsList) == 0 {
+ fc.Add(segments)
+ return
+ }
+
+ mergedSegments := fc.mergeExtraSegments(segments)
+ fc.segmentsList[len(fc.segmentsList)-1] = mergedSegments
+}
+
+// Adds all segments of a given fork context into this context.
+func (fc *ForkContext) AddAll(context *ForkContext) {
+ source := context.segmentsList
+
+ fc.segmentsList = append(fc.segmentsList, source...)
+}
+
+// Clears all segments in this context.
+func (fc *ForkContext) Clear() {
+ fc.segmentsList = make([][]*CodePathSegment, 0)
+}
+
+func removeSegment(segments []*CodePathSegment, target *CodePathSegment) []*CodePathSegment {
+ for i, segment := range segments {
+ if segment == target {
+ return append(segments[:i], segments[i+1:]...)
+ }
+ }
+ return segments
+}
+
+// Disconnect given segments.
+//
+// This is used in a process for switch statements.
+// If there is the "default" chunk before other cases, the order is different
+// between node's and running's.
+func RemoveConnection(prevSegments []*CodePathSegment, nextSegments []*CodePathSegment) {
+ for i := range prevSegments {
+ prevSegment := prevSegments[i]
+ nextSegment := nextSegments[i]
+
+ prevSegment.nextSegments = removeSegment(prevSegment.nextSegments, nextSegment)
+ prevSegment.allNextSegments = removeSegment(prevSegment.allNextSegments, nextSegment)
+ nextSegment.prevSegments = removeSegment(nextSegment.prevSegments, prevSegment)
+ nextSegment.allPrevSegments = removeSegment(nextSegment.allPrevSegments, prevSegment)
+ }
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/id_generator.go b/internal/plugins/react_hooks/code_path_analysis/id_generator.go
new file mode 100644
index 00000000..c67d523d
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/id_generator.go
@@ -0,0 +1,28 @@
+package code_path_analysis
+
+import (
+ "strconv"
+ "sync/atomic"
+)
+
+type AutoGeneratedId = uint32
+
+// A generator for unique Ids.
+type IdGenerator struct {
+ prefix string
+ n AutoGeneratedId
+}
+
+func NewIdGenerator(prefix string) *IdGenerator {
+ return &IdGenerator{
+ prefix: prefix,
+ n: 0,
+ }
+}
+
+func (g *IdGenerator) Next() string {
+ next := atomic.AddUint32(&g.n, 1)
+ g.n = next
+
+ return g.prefix + strconv.FormatUint(uint64(next), 10)
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/loop_context.go b/internal/plugins/react_hooks/code_path_analysis/loop_context.go
new file mode 100644
index 00000000..3244e7c6
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/loop_context.go
@@ -0,0 +1,362 @@
+package code_path_analysis
+
+type LoopStatementKind = uint
+
+const (
+ WhileStatement LoopStatementKind = iota + 1
+ DoWhileStatement
+ ForStatement
+ ForInStatement
+ ForOfStatement
+)
+
+type LoopContext struct {
+ upper *LoopContext
+ kind LoopStatementKind
+ label string
+ test bool
+ entrySegments []*CodePathSegment
+ continueDestSegments []*CodePathSegment
+ endOfInitSegments []*CodePathSegment
+ testSegments []*CodePathSegment
+ endOfTestSegments []*CodePathSegment
+ updateSegments []*CodePathSegment
+ endOfUpdateSegments []*CodePathSegment
+ leftSegments []*CodePathSegment
+ endOfLeftSegments []*CodePathSegment
+ prevSegments []*CodePathSegment
+ brokenForkContext *ForkContext
+ continueForkContext *ForkContext
+}
+
+func NewLoopContextForWhileStatement(state *CodePathState, label string) *LoopContext {
+ return &LoopContext{
+ upper: state.loopContext,
+ kind: WhileStatement,
+ label: label,
+ test: false,
+ continueDestSegments: nil,
+ brokenForkContext: state.breakContext.brokenForkContext,
+ }
+}
+
+func NewLoopContextForDoWhileStatement(state *CodePathState, label string) *LoopContext {
+ return &LoopContext{
+ upper: state.loopContext,
+ kind: DoWhileStatement,
+ label: label,
+ test: false,
+ entrySegments: nil,
+ continueForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ brokenForkContext: state.breakContext.brokenForkContext,
+ }
+}
+
+func NewLoopContextForForStatement(state *CodePathState, label string) *LoopContext {
+ return &LoopContext{
+ upper: state.loopContext,
+ kind: ForStatement,
+ label: label,
+ test: false,
+ endOfInitSegments: nil,
+ testSegments: nil,
+ endOfTestSegments: nil,
+ updateSegments: nil,
+ endOfUpdateSegments: nil,
+ continueDestSegments: nil,
+ brokenForkContext: state.breakContext.brokenForkContext,
+ }
+}
+
+func NewLoopContextForForInStatement(state *CodePathState, label string) *LoopContext {
+ return &LoopContext{
+ upper: state.loopContext,
+ kind: ForInStatement,
+ label: label,
+ test: false,
+ prevSegments: nil,
+ leftSegments: nil,
+ endOfLeftSegments: nil,
+ continueDestSegments: nil,
+ brokenForkContext: state.breakContext.brokenForkContext,
+ }
+}
+
+func NewLoopContextForForOfStatement(state *CodePathState, label string) *LoopContext {
+ return &LoopContext{
+ upper: state.loopContext,
+ kind: ForOfStatement,
+ label: label,
+ test: false,
+ prevSegments: nil,
+ leftSegments: nil,
+ endOfLeftSegments: nil,
+ continueDestSegments: nil,
+ brokenForkContext: state.breakContext.brokenForkContext,
+ }
+}
+
+// Creates a context object of a loop statement and stacks it.
+func (s *CodePathState) PushLoopContext(kind LoopStatementKind, label string) {
+ s.PushBreakContext(true, label)
+ switch kind {
+ case WhileStatement:
+ s.PushChoiceContext("loop", false)
+ s.loopContext = NewLoopContextForWhileStatement(s, label)
+ case DoWhileStatement:
+ s.PushChoiceContext("loop", false)
+ s.loopContext = NewLoopContextForDoWhileStatement(s, label)
+ case ForStatement:
+ s.PushChoiceContext("loop", false)
+ s.loopContext = NewLoopContextForForStatement(s, label)
+ case ForInStatement:
+ s.loopContext = NewLoopContextForForInStatement(s, label)
+ case ForOfStatement:
+ s.loopContext = NewLoopContextForForOfStatement(s, label)
+ default:
+ panic("unknown statement kind")
+ }
+}
+
+// Pops the last context of a loop statement and finalizes it.
+func (s *CodePathState) PopLoopContext() {
+ context := s.loopContext
+
+ s.loopContext = context.upper
+
+ forkContext := s.forkContext
+ brokenForkContext := s.PopBreakContext().brokenForkContext
+
+ switch context.kind {
+ case WhileStatement, ForStatement:
+ {
+ s.PopChoiceContext()
+ s.MakeLooped(forkContext.Head(), context.continueDestSegments)
+ }
+ case DoWhileStatement:
+ {
+ choiceContext := s.PopChoiceContext()
+
+ if !choiceContext.processed {
+ choiceContext.trueForkContext.Add(forkContext.Head())
+ choiceContext.falseForkContext.Add(forkContext.Head())
+ }
+ if !context.test {
+ brokenForkContext.AddAll(choiceContext.falseForkContext)
+ }
+
+ // `true` paths go to looping.
+ segmentsList := choiceContext.trueForkContext.segmentsList
+
+ for _, segment := range segmentsList {
+ s.MakeLooped(segment, context.entrySegments)
+ }
+ }
+ case ForInStatement, ForOfStatement:
+ {
+ brokenForkContext.Add(forkContext.Head())
+ s.MakeLooped(forkContext.Head(), context.leftSegments)
+ }
+ default:
+ panic("unreachable")
+ }
+
+ // Go next
+ if brokenForkContext.IsEmpty() {
+ forkContext.ReplaceHead((forkContext.MakeUnreachable(-1, -1)))
+ } else {
+ forkContext.ReplaceHead(brokenForkContext.MakeNext(0, -1))
+ }
+}
+
+// Makes a code path segment for the test part of a WhileStatement.
+func (s *CodePathState) MakeWhileTest(test bool) {
+ context := s.loopContext
+ forkContext := s.forkContext
+ testSegments := forkContext.MakeNext(0, -1)
+
+ // Update state.
+ context.test = test
+ context.continueDestSegments = testSegments
+ s.forkContext.ReplaceHead(testSegments)
+}
+
+// Makes a code path segment for the body part of a WhileStatement.
+func (s *CodePathState) MakeWhileBody() {
+ context := s.loopContext
+ choiceContext := s.choiceContext
+ forkContext := s.forkContext
+
+ if !choiceContext.processed {
+ choiceContext.trueForkContext.Add(forkContext.Head())
+ choiceContext.falseForkContext.Add(forkContext.Head())
+ }
+
+ // Update state.
+ if !context.test {
+ context.brokenForkContext.AddAll(choiceContext.falseForkContext)
+ }
+ forkContext.ReplaceHead(choiceContext.trueForkContext.MakeNext(0, -1))
+}
+
+// Makes a code path segment for the body part of a DoWhileStatement.
+func (s *CodePathState) MakeDoWhileBody() {
+ context := s.loopContext
+ forkContext := s.forkContext
+ bodySegments := forkContext.MakeNext(-1, -1)
+
+ // Update state.
+ context.entrySegments = bodySegments
+ forkContext.ReplaceHead(bodySegments)
+}
+
+// Makes a code path segment for the test part of a DoWhileStatement.
+func (s *CodePathState) MakeDoWhileTest(test bool) {
+ context := s.loopContext
+ forkContext := s.forkContext
+
+ context.test = test
+
+ // Creates paths of `continue` statements.
+ if !context.continueForkContext.IsEmpty() {
+ context.continueForkContext.Add(forkContext.Head())
+ testSegments := context.continueForkContext.MakeNext(0, -1)
+
+ forkContext.ReplaceHead(testSegments)
+ }
+}
+
+// Makes a code path segment for the test part of a ForStatement.
+func (s *CodePathState) MakeForTest(test bool) {
+ context := s.loopContext
+ forkContext := s.forkContext
+ endOfInitSegments := forkContext.Head()
+ testSegments := forkContext.MakeNext(-1, -1)
+
+ // Update state.
+ context.test = test
+ context.endOfInitSegments = endOfInitSegments
+ context.continueDestSegments = testSegments
+ context.testSegments = testSegments
+ forkContext.ReplaceHead(testSegments)
+}
+
+// Makes a code path segment for the update part of a ForStatement.
+func (s *CodePathState) MakeForUpdate() {
+ context := s.loopContext
+ choiceContext := s.choiceContext
+ forkContext := s.forkContext
+
+ // Make the next paths of the test.
+ if context.testSegments != nil {
+ finalizeTestSegmentsOfFor(context, choiceContext, forkContext.Head())
+ } else {
+ context.endOfInitSegments = forkContext.Head()
+ }
+
+ // Update state.
+ updateSegments := forkContext.MakeDisconnected(-1, -1)
+
+ context.continueDestSegments = updateSegments
+ context.updateSegments = updateSegments
+ forkContext.ReplaceHead(updateSegments)
+}
+
+// Makes a code path segment for the body part of a ForStatement.
+func (s *CodePathState) MakeForBody() {
+ context := s.loopContext
+ choiceContext := s.choiceContext
+ forkContext := s.forkContext
+
+ // Update state.
+ if context.updateSegments != nil {
+ context.endOfUpdateSegments = forkContext.Head()
+
+ // `update` -> `test`
+ if context.testSegments != nil {
+ s.MakeLooped(context.endOfUpdateSegments, context.testSegments)
+ }
+ } else if context.testSegments != nil {
+ finalizeTestSegmentsOfFor(context, choiceContext, forkContext.Head())
+ } else {
+ context.endOfInitSegments = forkContext.Head()
+ }
+
+ bodySegments := context.endOfTestSegments
+ if bodySegments == nil {
+ /*
+ * If there is not the `test` part, the `body` path comes from the
+ * `init` part and the `update` part.
+ */
+ prevForkContext := NewEmptyForkContext(forkContext, nil)
+
+ prevForkContext.Add(context.endOfInitSegments)
+ if context.endOfUpdateSegments != nil {
+ prevForkContext.Add(context.endOfUpdateSegments)
+ }
+
+ bodySegments = prevForkContext.MakeNext(0, -1)
+ }
+
+ if context.continueDestSegments == nil {
+ context.continueDestSegments = bodySegments
+ }
+ forkContext.ReplaceHead(bodySegments)
+}
+
+// Makes a code path segment for the left part of a ForInStatement and a ForOfStatement.
+func (s *CodePathState) MakeForInOfLeft() {
+ context := s.loopContext
+ forkContext := s.forkContext
+ leftSegments := forkContext.MakeDisconnected(-1, -1)
+
+ // Update state.
+ context.prevSegments = forkContext.Head()
+ context.leftSegments = leftSegments
+ context.continueDestSegments = leftSegments
+ forkContext.ReplaceHead(leftSegments)
+}
+
+// Makes a code path segment for the right part of a ForInStatement and a ForOfStatement.
+func (s *CodePathState) MakeForInOfRight() {
+ context := s.loopContext
+ forkContext := s.forkContext
+ temp := NewEmptyForkContext(forkContext, nil)
+
+ temp.Add(context.prevSegments)
+ rightSegments := temp.MakeNext(-1, -1)
+
+ // Update state.
+ context.endOfLeftSegments = forkContext.Head()
+ forkContext.ReplaceHead(rightSegments)
+}
+
+// Makes a code path segment for the body part of a ForInStatement and a ForOfStatement.
+func (s *CodePathState) MakeForInOfBody() {
+ context := s.loopContext
+ forkContext := s.forkContext
+ temp := NewEmptyForkContext(forkContext, nil)
+
+ temp.Add(context.endOfLeftSegments)
+ bodySegments := temp.MakeNext(-1, -1)
+
+ // Make a path: `right` -> `left`.
+ s.MakeLooped(forkContext.Head(), context.leftSegments)
+
+ // Update state.
+ context.brokenForkContext.Add(forkContext.Head())
+ forkContext.ReplaceHead(bodySegments)
+}
+
+func finalizeTestSegmentsOfFor(loopContext *LoopContext, choiceContext *ChoiceContext, head []*CodePathSegment) {
+ if !choiceContext.processed {
+ choiceContext.trueForkContext.Add(head)
+ choiceContext.falseForkContext.Add(head)
+ choiceContext.qqForkContext.Add(head)
+ }
+
+ if !loopContext.test {
+ loopContext.brokenForkContext.AddAll(choiceContext.falseForkContext)
+ }
+ loopContext.endOfTestSegments = choiceContext.trueForkContext.MakeNext(0, -1)
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/switch_context.go b/internal/plugins/react_hooks/code_path_analysis/switch_context.go
new file mode 100644
index 00000000..b02acfe6
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/switch_context.go
@@ -0,0 +1,127 @@
+package code_path_analysis
+
+type SwitchContext struct {
+ upper *SwitchContext
+ hasCase bool
+ defaultSegments []*CodePathSegment
+ defaultBodySegments []*CodePathSegment
+ foundDefault bool
+ lastIsDefault bool
+ countForks int
+}
+
+func NewSwitchContext(state *CodePathState, hasCase bool, label string) *SwitchContext {
+ return &SwitchContext{
+ upper: state.switchContext,
+ hasCase: hasCase,
+ defaultSegments: nil,
+ defaultBodySegments: nil,
+ foundDefault: false,
+ lastIsDefault: false,
+ countForks: 0,
+ }
+}
+
+// Creates a context object of SwitchStatement and stacks it.
+func (s *CodePathState) PushSwitchContext(hasCase bool, label string) {
+ s.switchContext = NewSwitchContext(s, hasCase, label)
+
+ s.PushBreakContext(true /*breakable*/, label)
+}
+
+// Pops the last context of SwitchStatement and finalizes it.
+//
+// - Disposes all forking stack for `case` and `default`.
+// - Creates the next code path segment from `context.brokenForkContext`.
+// - If the last `SwitchCase` node is not a `default` part, creates a path
+// to the `default` body.
+func (s *CodePathState) PopSwitchContext() {
+ context := s.switchContext
+
+ s.switchContext = context.upper
+
+ forkContext := s.forkContext
+ brokenForkContext := s.PopBreakContext().brokenForkContext
+
+ if context.countForks == 0 {
+ // When there is only one `default` chunk and there is one or more
+ // `break` statements, even if forks are nothing, it needs to merge
+ // those.
+ if !brokenForkContext.IsEmpty() {
+ brokenForkContext.Add(forkContext.MakeNext(-1, -1))
+ forkContext.ReplaceHead(brokenForkContext.MakeNext(0, -1))
+ }
+
+ return
+ }
+
+ lastSegments := forkContext.Head()
+
+ s.ForkBypassPath()
+ lastCaseSegments := forkContext.Head()
+
+ // `brokenForkContext` is used to make the next segment.
+ // It must add the last segment into `brokenForkContext`.
+ brokenForkContext.Add(lastSegments)
+
+ // path which is failed in all case test should be connected to path
+ // of `default` chunk.
+ if !context.lastIsDefault {
+ if context.defaultBodySegments != nil {
+ // Remove a link from `default` label to its chunk.
+ // It's false route.
+ RemoveConnection(context.defaultSegments, context.defaultBodySegments)
+ s.MakeLooped(lastCaseSegments, context.defaultBodySegments)
+ } else {
+ // It handles the last case body as broken if `default` chunk
+ // does not exist.
+ brokenForkContext.Add(lastCaseSegments)
+ }
+ }
+
+ // Pops the segment context stack until the entry segment.
+ for range context.countForks {
+ s.forkContext = s.forkContext.upper
+ }
+
+ // Creates a path from all brokenForkContext paths.
+ // This is a path after switch statement.
+ s.forkContext.ReplaceHead(brokenForkContext.MakeNext(0, -1))
+}
+
+// Makes a code path segment for a `SwitchCase` node.
+func (s *CodePathState) MakeSwitchCaseBody(isEmpty bool, isDefault bool) {
+ context := s.switchContext
+
+ if !context.hasCase {
+ return
+ }
+
+ // Merge forks.
+ // The parent fork context has two segments.
+ // Those are from the current case and the body of the previous case.
+ parentForkContext := s.forkContext
+ forkContext := s.PushForkContext(nil /*forkLeavingPath*/)
+
+ forkContext.Add(parentForkContext.MakeNext(0, -1))
+
+ // Save default chunk info.
+ // If the default label is not at the last, we must make a path from
+ // the last case to the default chunk.
+ if isDefault {
+ context.defaultSegments = parentForkContext.Head()
+ if isEmpty {
+ context.foundDefault = true
+ } else {
+ context.defaultBodySegments = forkContext.Head()
+ }
+ } else {
+ if !isEmpty && context.foundDefault {
+ context.foundDefault = false
+ context.defaultBodySegments = forkContext.Head()
+ }
+ }
+
+ context.lastIsDefault = isDefault
+ context.countForks++
+}
diff --git a/internal/plugins/react_hooks/code_path_analysis/try_context.go b/internal/plugins/react_hooks/code_path_analysis/try_context.go
new file mode 100644
index 00000000..73c300b6
--- /dev/null
+++ b/internal/plugins/react_hooks/code_path_analysis/try_context.go
@@ -0,0 +1,235 @@
+package code_path_analysis
+
+type TryContext struct {
+ upper *TryContext
+ position string
+ hasFinalizer bool
+ returnedForkContext *ForkContext
+ thrownForkContext *ForkContext
+ lastOfTryIsReachable bool
+ lastOfCatchIsReachable bool
+}
+
+func NewTryContext(state *CodePathState, hasFinalizer bool) *TryContext {
+ var returnedForkContext *ForkContext
+ if hasFinalizer {
+ returnedForkContext = NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/)
+ }
+ return &TryContext{
+ upper: state.tryContext,
+ position: "try",
+ hasFinalizer: hasFinalizer,
+ returnedForkContext: returnedForkContext,
+ thrownForkContext: NewEmptyForkContext(state.forkContext, nil /*forkLeavingPath*/),
+ lastOfTryIsReachable: false,
+ lastOfCatchIsReachable: false,
+ }
+}
+
+// Creates a context object of TryStatement and stacks it.
+func (s *CodePathState) PushTryContext(hasFinalizer bool) {
+ s.tryContext = NewTryContext(s, hasFinalizer)
+}
+
+// PopTryContext pops the last context of TryStatement and finalizes it.
+func (s *CodePathState) PopTryContext() {
+ context := s.tryContext
+ s.tryContext = context.upper
+
+ if context.position == "catch" {
+ // Merges two paths from the `try` block and `catch` block merely.
+ s.PopForkContext()
+ return
+ }
+
+ // The following process is executed only when there is the `finally` block.
+ returned := context.returnedForkContext
+ thrown := context.thrownForkContext
+
+ if returned.IsEmpty() && thrown.IsEmpty() {
+ return
+ }
+
+ // Separate head to normal paths and leaving paths.
+ headSegments := s.forkContext.Head()
+ s.forkContext = s.forkContext.upper
+
+ halfLength := len(headSegments) / 2
+ normalSegments := headSegments[:halfLength]
+ leavingSegments := headSegments[halfLength:]
+
+ // Forwards the leaving path to upper contexts.
+ if !returned.IsEmpty() {
+ returnCtx := s.getReturnContext()
+ if returnCtx != nil {
+ returnCtx.returnedForkContext.Add(leavingSegments)
+ } else {
+ s.addReturnedSegments(leavingSegments)
+ }
+ }
+ if !thrown.IsEmpty() {
+ throwCtx := s.getThrowContext()
+ if throwCtx != nil {
+ throwCtx.thrownForkContext.Add(leavingSegments)
+ } else {
+ s.addThrownSegments(leavingSegments)
+ }
+ }
+
+ // Sets the normal path as the next.
+ s.forkContext.ReplaceHead(normalSegments)
+
+ // If both paths of the `try` block and the `catch` block are
+ // unreachable, the next path becomes unreachable as well.
+ if !context.lastOfTryIsReachable && !context.lastOfCatchIsReachable {
+ s.forkContext.ReplaceHead(s.forkContext.MakeUnreachable(-1, -1))
+ }
+}
+
+// Makes a code path segment for a `catch` block.
+func (s *CodePathState) MakeCatchBlock() {
+ context := s.tryContext
+ forkContext := s.forkContext
+ thrown := context.thrownForkContext
+
+ // Update state.
+ context.position = "catch"
+ context.thrownForkContext = NewEmptyForkContext(forkContext, nil)
+ context.lastOfTryIsReachable = forkContext.IsReachable()
+
+ // Merge thrown paths.
+ thrown.Add(forkContext.Head())
+ thrownSegments := thrown.MakeNext(0, -1)
+
+ // Fork to a bypass and the merged thrown path.
+ s.PushForkContext(nil /*forkLeavingPath*/)
+ s.ForkBypassPath()
+ s.forkContext.Add(thrownSegments)
+}
+
+// MakeFinallyBlock makes a code path segment for a `finally` block.
+//
+// In the `finally` block, parallel paths are created. The parallel paths
+// are used as leaving-paths. The leaving-paths are paths from `return`
+// statements and `throw` statements in a `try` block or a `catch` block.
+func (s *CodePathState) MakeFinallyBlock() {
+ context := s.tryContext
+ forkContext := s.forkContext
+ returned := context.returnedForkContext
+ thrown := context.thrownForkContext
+ headOfLeavingSegments := forkContext.Head()
+
+ // Update state.
+ if context.position == "catch" {
+ // Merges two paths from the `try` block and `catch` block.
+ s.PopForkContext()
+ forkContext = s.forkContext
+ context.lastOfCatchIsReachable = forkContext.IsReachable()
+ } else {
+ context.lastOfTryIsReachable = forkContext.IsReachable()
+ }
+ context.position = "finally"
+
+ if returned.IsEmpty() && thrown.IsEmpty() {
+ // This path does not leave.
+ return
+ }
+
+ // Create a parallel segment from merging returned and thrown.
+ // This segment will leave at the end of this finally block.
+ segments := forkContext.MakeNext(-1, -1)
+
+ for i := range forkContext.count {
+ prevSegsOfLeavingSegment := []*CodePathSegment{headOfLeavingSegments[i]}
+
+ for j := range len(returned.segmentsList) {
+ prevSegsOfLeavingSegment = append(prevSegsOfLeavingSegment, returned.segmentsList[j][i])
+ }
+ for j := range len(thrown.segmentsList) {
+ prevSegsOfLeavingSegment = append(prevSegsOfLeavingSegment, thrown.segmentsList[j][i])
+ }
+
+ segments = append(segments, NewNextCodePathSegment(
+ s.idGenerator.Next(),
+ prevSegsOfLeavingSegment,
+ ))
+ }
+
+ s.PushForkContext(nil /*forkLeavingPath*/)
+ s.forkContext.Add(segments)
+}
+
+// Makes a code path segment from the first throwable node
+// to the `catch` block or the `finally` block.
+func (s *CodePathState) MakeFirstThrowablePathInTryBlock() {
+ forkContext := s.forkContext
+
+ if !forkContext.IsReachable() {
+ return
+ }
+
+ context := s.getThrowContext()
+
+ if context == nil ||
+ context.position != "try" ||
+ !context.thrownForkContext.IsEmpty() {
+ return
+ }
+
+ context.thrownForkContext.Add(forkContext.Head())
+ forkContext.ReplaceHead(forkContext.MakeNext(-1, -1))
+}
+
+// Gets a context for a `return` statement.
+func (s *CodePathState) getReturnContext() *TryContext {
+ context := s.tryContext
+ for context != nil {
+ if context.hasFinalizer && context.position != "finally" {
+ return context
+ }
+ context = context.upper
+ }
+
+ return nil
+}
+
+// Gets a context for a `throw` statement.
+func (s *CodePathState) getThrowContext() *TryContext {
+ context := s.tryContext
+ for context != nil {
+ if context.position == "try" ||
+ (context.hasFinalizer && context.position == "catch") {
+ return context
+ }
+ context = context.upper
+ }
+ // If no try context found, return nil (this should be handled by caller)
+ return nil
+}
+
+// Makes the final path.
+func (s *CodePathState) MakeFinal() {
+ segments := s.currentSegments
+
+ if len(segments) > 0 && segments[0].reachable {
+ s.addReturnedSegments(segments)
+ }
+}
+
+// Makes a path for a `throw` statement.
+//
+// It registers the head segment to a context of `throw`.
+// It makes new unreachable segment, then it set the head with the segment.
+func (s *CodePathState) MakeThrow() {
+ forkContext := s.forkContext
+
+ if forkContext.IsReachable() {
+ throwCtx := s.getThrowContext()
+ if throwCtx != nil {
+ throwCtx.thrownForkContext.Add(forkContext.Head())
+ } else {
+ s.addThrownSegments(forkContext.Head())
+ }
+ forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
+ }
+}
diff --git a/internal/plugins/react_hooks/plugin.go b/internal/plugins/react_hooks/plugin.go
new file mode 100644
index 00000000..edb091f9
--- /dev/null
+++ b/internal/plugins/react_hooks/plugin.go
@@ -0,0 +1,3 @@
+package react_hooks_plugin
+
+const PLUGIN_NAME = "eslint-plugin-react-hooks"
diff --git a/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks.go b/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks.go
new file mode 100644
index 00000000..88c890ee
--- /dev/null
+++ b/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks.go
@@ -0,0 +1,868 @@
+package rules_of_hooks
+
+import (
+ "math"
+ "math/big"
+ "regexp"
+
+ "github.com/microsoft/typescript-go/shim/ast"
+ "github.com/microsoft/typescript-go/shim/scanner"
+ analysis "github.com/web-infra-dev/rslint/internal/plugins/react_hooks/code_path_analysis"
+ "github.com/web-infra-dev/rslint/internal/rule"
+)
+
+var RulesOfHooksRule = rule.Rule{
+ Name: "react-hooks/rules-of-hooks",
+ Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
+ codePathReactHooksMapStack := make([]map[*analysis.CodePathSegment][]*ast.Node, 0)
+ codePathSegmentStack := make([]*analysis.CodePathSegment, 0)
+
+ // Track useEffectEvent functions and current effect
+ // This implements the enhanced hook detection for useEffectEvent functions
+ // which can only be called from the same component and within useEffect
+ useEffectEventFunctions := make(map[*ast.Node]bool)
+ var lastEffect *ast.Node
+
+ onCodePathSegmentStart := func(segment *analysis.CodePathSegment, node *ast.Node) {
+ codePathSegmentStack = append(codePathSegmentStack, segment)
+ }
+ onCodePathSegmentEnd := func(segment *analysis.CodePathSegment, node *ast.Node) {
+ codePathSegmentStack = codePathSegmentStack[:len(codePathSegmentStack)-1]
+ }
+ onCodePathStart := func(codePath *analysis.CodePath, node *ast.Node) {
+ codePathReactHooksMapStack = append(
+ codePathReactHooksMapStack,
+ make(map[*analysis.CodePathSegment][]*ast.Node),
+ )
+ }
+ onCodePathEnd := func(codePath *analysis.CodePath, codePathNode *ast.Node) {
+ if len(codePathReactHooksMapStack) == 0 {
+ return
+ }
+
+ // Pop the current hooks map
+ reactHooksMap := codePathReactHooksMapStack[len(codePathReactHooksMapStack)-1]
+ codePathReactHooksMapStack = codePathReactHooksMapStack[:len(codePathReactHooksMapStack)-1]
+
+ if len(reactHooksMap) == 0 {
+ return
+ }
+
+ // Set to track cyclic segments
+ cyclic := make(map[string]bool)
+
+ // Cache for path counting functions
+ countPathsFromStartCache := make(map[string]*big.Int)
+ countPathsToEndCache := make(map[string]*big.Int)
+ shortestPathLengthToStartCache := make(map[string]*int) // nil indicates cycle
+
+ // Count paths from start to a segment
+ var countPathsFromStart func(*analysis.CodePathSegment, []string) *big.Int
+ countPathsFromStart = func(segment *analysis.CodePathSegment, pathHistory []string) *big.Int {
+ if pathHistory == nil {
+ pathHistory = make([]string, 0)
+ }
+
+ segmentID := segment.ID()
+ if paths, exists := countPathsFromStartCache[segmentID]; exists && paths != nil {
+ return paths
+ }
+
+ pathList := pathHistory
+
+ // If `pathList` includes the current segment then we've found a cycle!
+ // We need to fill `cyclic` with all segments inside cycle
+ hasCyclic := false
+ for _, path := range pathList {
+ if path == segmentID || hasCyclic {
+ hasCyclic = true
+ cyclic[path] = true
+ }
+ }
+ if hasCyclic {
+ return big.NewInt(0)
+ }
+
+ pathList = append(pathList, segmentID)
+
+ var paths *big.Int
+ if codePath.HasThrownSegment(segment) {
+ paths = big.NewInt(0)
+ } else if len(segment.PrevSegments()) == 0 {
+ paths = big.NewInt(1)
+ } else {
+ paths = big.NewInt(0)
+ for _, prevSegment := range segment.PrevSegments() {
+ prevPaths := countPathsFromStart(prevSegment, pathList)
+ paths.Add(paths, prevPaths)
+ }
+ }
+
+ if segment.Reachable() && paths.Cmp(big.NewInt(0)) == 0 {
+ countPathsFromStartCache[segmentID] = nil
+ } else {
+ countPathsFromStartCache[segmentID] = paths
+ }
+
+ return paths
+ }
+
+ // countPathsToEnd counts the number of code paths from a given segment to the end of the
+ // function. For example:
+ //
+ // func MyComponent() {
+ // // Segment 1
+ // if condition {
+ // // Segment 2
+ // } else {
+ // // Segment 3
+ // }
+ // }
+ //
+ // Segments 2 and 3 have one path to the end of MyComponent and
+ // segment 1 has two paths to the end of MyComponent since we could
+ // either take the path of segment 2 or segment 3.
+ //
+ // This function also populates the cyclic map with cyclic segments.
+ var countPathsToEnd func(*analysis.CodePathSegment, []string) *big.Int
+ countPathsToEnd = func(segment *analysis.CodePathSegment, pathHistory []string) *big.Int {
+ if pathHistory == nil {
+ pathHistory = make([]string, 0)
+ }
+
+ segmentID := segment.ID()
+ if paths, exists := countPathsToEndCache[segmentID]; exists {
+ return paths
+ }
+
+ pathList := pathHistory
+
+ // If `pathList` includes the current segment then we've found a cycle!
+ // We need to fill `cyclic` with all segments inside cycle
+ hasCyclic := false
+ for _, path := range pathList {
+ if path == segmentID || hasCyclic {
+ hasCyclic = true
+ cyclic[path] = true
+ }
+ }
+ if hasCyclic {
+ return big.NewInt(0)
+ }
+
+ // add the current segment to pathList
+ pathList = append(pathList, segmentID)
+
+ var paths *big.Int
+ if codePath.HasThrownSegment(segment) {
+ paths = big.NewInt(0)
+ } else if len(segment.NextSegments()) == 0 {
+ paths = big.NewInt(1)
+ } else {
+ paths = big.NewInt(0)
+ for _, nextSegment := range segment.NextSegments() {
+ nextPaths := countPathsToEnd(nextSegment, pathList)
+ paths.Add(paths, nextPaths)
+ }
+ }
+
+ countPathsToEndCache[segmentID] = paths
+ return paths
+ }
+
+ // Get shortest path length to start
+ var shortestPathLengthToStart func(*analysis.CodePathSegment) int
+ shortestPathLengthToStart = func(segment *analysis.CodePathSegment) int {
+ segmentID := segment.ID()
+
+ if lengthPtr, exists := shortestPathLengthToStartCache[segmentID]; exists {
+ if lengthPtr == nil {
+ return math.MaxInt32
+ }
+ return *lengthPtr
+ }
+
+ shortestPathLengthToStartCache[segmentID] = nil
+
+ var length int
+ if len(segment.PrevSegments()) == 0 {
+ length = 1
+ } else {
+ length = math.MaxInt32
+ for _, prevSegment := range segment.PrevSegments() {
+ prevLength := shortestPathLengthToStart(prevSegment)
+ if prevLength < length {
+ length = prevLength
+ }
+ }
+ if length < math.MaxInt32 {
+ length++
+ }
+ }
+
+ shortestPathLengthToStartCache[segmentID] = &length
+ return length
+ }
+
+ // Count all paths from start to end
+ allPathsFromStartToEnd := countPathsToEnd(codePath.InitialSegment(), nil)
+
+ // Get function name for this code path
+ codePathFunctionName := getFunctionName(codePathNode)
+
+ // Check if we're inside a component or hook
+ isSomewhereInsideComponentOrHook := isInsideComponentOrHook(codePathNode)
+ isDirectlyInsideComponentOrHook := false
+
+ if codePathFunctionName != "" {
+ isDirectlyInsideComponentOrHook = isComponentName(codePathFunctionName) || isHookName(codePathFunctionName)
+ } else {
+ isDirectlyInsideComponentOrHook = isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode)
+ }
+
+ // Compute shortest final path length
+ shortestFinalPathLength := math.MaxInt32
+ for _, finalSegment := range codePath.FinalSegments() {
+ if !finalSegment.Reachable() {
+ continue
+ }
+ length := shortestPathLengthToStart(finalSegment)
+ if length < shortestFinalPathLength {
+ shortestFinalPathLength = length
+ }
+ }
+
+ // Process each segment with React hooks
+ for segment, reactHooks := range reactHooksMap {
+ // NOTE: We could report here that the hook is not reachable, but
+ // that would be redundant with more general "no unreachable"
+ // lint rules.
+ if !segment.Reachable() {
+ continue
+ }
+
+ // If there are any final segments with a shorter path to start then
+ // we possibly have an early return.
+ //
+ // If our segment is a final segment itself then siblings could
+ // possibly be early returns.
+ possiblyHasEarlyReturn := false
+ if len(segment.NextSegments()) == 0 {
+ possiblyHasEarlyReturn = shortestFinalPathLength <= shortestPathLengthToStart(segment)
+ } else {
+ possiblyHasEarlyReturn = shortestFinalPathLength < shortestPathLengthToStart(segment)
+ }
+
+ // Count all the paths from the start of our code path to the end of
+ // our code path that go _through_ this segment. The critical piece
+ // of this is _through_. If we just call `countPathsToEnd(segment)`
+ // then we neglect that we may have gone through multiple paths to get
+ // to this point! Consider:
+ //
+ // ```js
+ // function MyComponent() {
+ // if (a) {
+ // // Segment 1
+ // } else {
+ // // Segment 2
+ // }
+ // // Segment 3
+ // if (b) {
+ // // Segment 4
+ // } else {
+ // // Segment 5
+ // }
+ // }
+ // ```
+ //
+ // In this component we have four code paths:
+ //
+ // 1. `a = true; b = true`
+ // 2. `a = true; b = false`
+ // 3. `a = false; b = true`
+ // 4. `a = false; b = false`
+ //
+ // From segment 3 there are two code paths to the end through segment
+ // 4 and segment 5. However, we took two paths to get here through
+ // segment 1 and segment 2.
+ //
+ // If we multiply the paths from start (two) by the paths to end (two)
+ // for segment 3 we get four. Which is our desired count.
+ pathsFromStart := countPathsFromStart(segment, nil)
+ pathsToEnd := countPathsToEnd(segment, nil)
+ pathsFromStartToEnd := new(big.Int).Mul(pathsFromStart, pathsToEnd)
+
+ // Is this hook a part of a cyclic segment?
+ isCyclic := cyclic[segment.ID()]
+
+ // Process each hook in this segment
+ for _, hook := range reactHooks {
+ // Skip if flow suppression exists
+ if hasFlowSuppression(hook) {
+ continue
+ }
+
+ hookText := getNodeText(hook)
+ isUseHook := isUseIdentifier(hook)
+
+ // Report error for use() in try/catch
+ if isUseHook && isInsideTryCatch(hook) {
+ ctx.ReportNode(hook, buildTryCatchUseMessage(hookText))
+ continue
+ }
+
+ // Report error for hooks in loops (except use())
+ if (isCyclic || isInsideDoWhileLoop(hook)) && !isUseHook {
+ ctx.ReportNode(hook, buildLoopHookMessage(hookText))
+ continue
+ }
+
+ // Check if we're in a valid context for hooks
+ if isDirectlyInsideComponentOrHook {
+ // Check for async function
+ if isInsideAsyncFunction(codePathNode) {
+ ctx.ReportNode(hook, buildAsyncComponentHookMessage(hookText))
+ continue
+ }
+
+ pathsCmp := pathsFromStartToEnd.Cmp(allPathsFromStartToEnd)
+
+ // Check for conditional calls (except use() and do-while loops)
+ if !isCyclic &&
+ pathsCmp != 0 &&
+ !isUseHook &&
+ !isInsideDoWhileLoop(hook) {
+ var message rule.RuleMessage
+ if possiblyHasEarlyReturn {
+ message = buildConditionalHookWithEarlyReturnMessage(hookText)
+ } else {
+ message = buildConditionalHookMessage(hookText)
+ }
+ ctx.ReportNode(hook, message)
+ }
+ } else {
+ // Handle various invalid contexts
+ if isInsideClass(codePathNode) {
+ ctx.ReportNode(hook, buildClassHookMessage(hookText))
+ } else if codePathFunctionName != "" {
+ // Custom message if we found an invalid function name.
+ ctx.ReportNode(hook, buildFunctionHookMessage(hookText, codePathFunctionName))
+ } else if isTopLevel(codePathNode) {
+ // These are dangerous if you have inline requires enabled.
+ ctx.ReportNode(hook, buildTopLevelHookMessage(hookText))
+ } else if isSomewhereInsideComponentOrHook && !isUseHook {
+ // Assume in all other cases the user called a hook in some
+ // random function callback. This should usually be true for
+ // anonymous function expressions. Hopefully this is clarifying
+ // enough in the common case that the incorrect message in
+ // uncommon cases doesn't matter.
+ // `use(...)` can be called in callbacks.
+ ctx.ReportNode(hook, buildGenericHookMessage(hookText))
+ }
+ }
+ }
+ }
+ }
+ analyzer := analysis.NewCodePathAnalyzer(
+ onCodePathSegmentStart,
+ onCodePathSegmentEnd,
+ onCodePathStart,
+ onCodePathEnd,
+ nil, /*onCodePathSegmentLoop*/
+ )
+ return rule.RuleListeners{
+ rule.WildcardTokenKind: func(node *ast.Node) {
+ analyzer.EnterNode(node)
+ },
+ rule.WildcardExitTokenKind: func(node *ast.Node) {
+ analyzer.LeaveNode(node)
+ },
+ ast.KindCallExpression: func(node *ast.Node) {
+ callExpr := node.AsCallExpression()
+
+ // Check if this is a hook call
+ if isHook(callExpr.Expression) {
+ // Add the hook node to a map keyed by the code path segment
+ if len(codePathReactHooksMapStack) > 0 && len(codePathSegmentStack) > 0 {
+ reactHooksMap := codePathReactHooksMapStack[len(codePathReactHooksMapStack)-1]
+ codePathSegment := codePathSegmentStack[len(codePathSegmentStack)-1]
+
+ reactHooks := reactHooksMap[codePathSegment]
+ if reactHooks == nil {
+ reactHooks = []*ast.Node{}
+ reactHooksMap[codePathSegment] = reactHooks
+ }
+ reactHooksMap[codePathSegment] = append(reactHooksMap[codePathSegment], callExpr.Expression)
+ }
+ }
+
+ // Check for useEffect and useEffectEvent calls
+ nodeWithoutNamespace := getNodeWithoutReactNamespace(callExpr.Expression)
+ if (isUseEffectIdentifier(nodeWithoutNamespace) || isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
+ len(callExpr.Arguments.Nodes) > 0 {
+ lastEffect = node
+ }
+ },
+ // CallExpression exit handler
+ rule.ListenerOnExit(ast.KindCallExpression): func(node *ast.Node) {
+ if node == lastEffect {
+ lastEffect = nil
+ }
+ },
+ ast.KindIdentifier: func(node *ast.Node) {
+ // Check for useEffectEvent function references outside effects
+ if lastEffect == nil && useEffectEventFunctions[node] {
+ nodeText := scanner.GetTextOfNode(node)
+ message := "`" + nodeText + "` is a function created with React Hook \"useEffectEvent\", and can only be called from " +
+ "the same component."
+
+ // Check if it's being called
+ parent := node.Parent
+ if parent == nil || parent.Kind != ast.KindCallExpression {
+ message += " They cannot be assigned to variables or passed down."
+ }
+
+ ctx.ReportNode(node, rule.RuleMessage{
+ Id: "useEffectEventReference",
+ Description: message,
+ })
+ }
+ },
+ ast.KindFunctionDeclaration: func(node *ast.Node) {
+ // function MyComponent() { const onClick = useEffectEvent(...) }
+ if isInsideComponentOrHookFromScope(node) {
+ recordAllUseEffectEventFunctions(getScope(node))
+ }
+ },
+ ast.KindArrowFunction: func(node *ast.Node) {
+ // const MyComponent = () => { const onClick = useEffectEvent(...) }
+ if isInsideComponentOrHookFromScope(node) {
+ recordAllUseEffectEventFunctions(getScope(node))
+ }
+ },
+ }
+ },
+}
+
+// Message functions for different error types
+func buildConditionalHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "conditionalHook",
+ Description: `React Hook "` + hookName + `" is called conditionally. React Hooks must be ` +
+ "called in the exact same order in every component render.",
+ }
+}
+
+func buildConditionalHookWithEarlyReturnMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "conditionalHook",
+ Description: `React Hook "` + hookName + `" is called conditionally. React Hooks must be ` +
+ "called in the exact same order in every component render." +
+ " Did you accidentally call a React Hook after an early return?",
+ }
+}
+
+func buildLoopHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "loopHook",
+ Description: `React Hook "` + hookName + `" 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.",
+ }
+}
+
+func buildFunctionHookMessage(hookName, functionName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "functionHook",
+ Description: `React Hook "` + hookName + `" is called in function "` + functionName + `" 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\".",
+ }
+}
+
+func buildGenericHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "genericHook",
+ Description: `React Hook "` + hookName + `" cannot be called inside a callback. React Hooks ` +
+ "must be called in a React function component or a custom React " +
+ "Hook function.",
+ }
+}
+
+func buildTopLevelHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "topLevelHook",
+ Description: `React Hook "` + hookName + `" cannot be called at the top level. React Hooks ` +
+ "must be called in a React function component or a custom React " +
+ "Hook function.",
+ }
+}
+
+func buildClassHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "classHook",
+ Description: `React Hook "` + hookName + `" cannot be called in a class component. React Hooks ` +
+ "must be called in a React function component or a custom React " +
+ "Hook function.",
+ }
+}
+
+func buildAsyncComponentHookMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "asyncComponentHook",
+ Description: `React Hook "` + hookName + `" cannot be called in an async function.`,
+ }
+}
+
+func buildTryCatchUseMessage(hookName string) rule.RuleMessage {
+ return rule.RuleMessage{
+ Id: "tryCatchUse",
+ Description: `React Hook "` + hookName + `" cannot be called inside a try/catch block.`,
+ }
+}
+
+// Helper function to check if a name follows PascalCase convention
+func isPascalCaseNameSpace(name string) bool {
+ if len(name) == 0 {
+ return false
+ }
+ // PascalCase names start with uppercase letter
+ return name[0] >= 'A' && name[0] <= 'Z'
+}
+
+// Helper function to check if a name is a Hook name
+func isHookName(name string) bool {
+ if name == "use" {
+ return true
+ }
+ // Match "use" followed by uppercase letter
+ matched, _ := regexp.MatchString(`^use[A-Z0-9]`, name)
+ return matched
+}
+
+// Helper function to check if a name is a component name (PascalCase)
+func isComponentName(name string) bool {
+ if len(name) == 0 {
+ return false
+ }
+ // Component names start with uppercase letter
+ return name[0] >= 'A' && name[0] <= 'Z'
+}
+
+// Helper function to check if a function is a hook
+func isHook(node *ast.Node) bool {
+ switch node.Kind {
+ case ast.KindIdentifier:
+ return isHookName(node.Text())
+ case ast.KindPropertyAccessExpression:
+ name := node.AsPropertyAccessExpression().Name()
+ if name == nil || !isHook(name) {
+ return false
+ }
+
+ expr := node.AsPropertyAccessExpression().Expression
+ if expr == nil || !ast.IsIdentifier(expr) {
+ return false
+ }
+
+ return isPascalCaseNameSpace(expr.AsIdentifier().Text)
+ }
+ return false
+}
+
+// Helper function to get function name from AST node
+func getFunctionName(node *ast.Node) string {
+ switch node.Kind {
+ case ast.KindFunctionDeclaration:
+ // function useHook() {}
+ // const whatever = function useHook() {};
+ //
+ // Function declaration or function expression names win over any
+ // assignment statements or other renames.
+ return node.AsFunctionDeclaration().Name().Text()
+ case ast.KindFunctionExpression:
+ name := node.AsFunctionExpression().Name()
+ if name != nil {
+ return node.AsFunctionExpression().Name().Text()
+ }
+ case ast.KindArrowFunction:
+ if node.Parent != nil {
+ switch node.Parent.Kind {
+ case ast.KindVariableDeclaration, // const useHook = () => {};
+ ast.KindShorthandPropertyAssignment, // ({k = () => { useState(); }} = {});
+ ast.KindBindingElement, // const {j = () => { useState(); }} = {};
+ ast.KindPropertyAssignment: // ({f: () => { useState(); }});
+ if ast.IsInExpressionContext(node) {
+ return node.Parent.Name().Text()
+ }
+ case ast.KindBinaryExpression:
+ if node.Parent.AsBinaryExpression().Right == node {
+ left := node.Parent.AsBinaryExpression().Left
+ switch left.Kind {
+ case ast.KindIdentifier:
+ // e = () => { useState(); };
+ return left.AsIdentifier().Text
+ case ast.KindPropertyAccessExpression:
+ // Namespace.useHook = () => { useState(); };
+ return left.AsPropertyAccessExpression().Name().Text()
+ }
+ }
+ }
+ }
+ return ""
+ case ast.KindMethodDeclaration:
+ // NOTE: We could also support `ClassProperty` and `MethodDefinition`
+ // here to be pedantic. However, hooks in a class are an anti-pattern. So
+ // we don't allow it to error early.
+ //
+ // class {useHook = () => {}}
+ // class {useHook() {}}
+ if ast.GetContainingClass(node) != nil {
+ return ""
+ }
+
+ // {useHook: () => {}}
+ // {useHook() {}}
+ return node.AsMethodDeclaration().Name().Text()
+ }
+ return ""
+}
+
+// Helper function to check if node is inside a component or hook
+func isInsideComponentOrHook(node *ast.Node) bool {
+ // Walk up the AST to find function declarations
+ // and check if any of them are components or hooks
+ current := node
+ for current != nil {
+ functionName := getFunctionName(current)
+ if functionName != "" && (isComponentName(functionName) || isHookName(functionName)) {
+ return true
+ }
+ if isForwardRefCallback(current) || isMemoCallback(current) {
+ return true
+ }
+ current = current.Parent
+ }
+ return false
+}
+
+// Helper function to check if node is a function-like construct
+func isFunctionLike(node *ast.Node) bool {
+ kind := node.Kind
+ return kind == ast.KindFunctionDeclaration ||
+ kind == ast.KindFunctionExpression ||
+ kind == ast.KindArrowFunction ||
+ kind == ast.KindMethodDeclaration
+}
+
+// Helper function to check if node is inside a class
+func isInsideClass(node *ast.Node) bool {
+ current := node.Parent
+ for current != nil {
+ if current.Kind == ast.KindClassDeclaration ||
+ current.Kind == ast.KindClassExpression {
+ return true
+ }
+ current = current.Parent
+ }
+ return false
+}
+
+// Helper function to check if node is inside an async function
+func isInsideAsyncFunction(node *ast.Node) bool {
+ current := node
+ for current != nil {
+ if isAsyncFunction(current) {
+ return true
+ }
+ current = current.Parent
+ }
+ return false
+}
+
+// Helper function to check if node is inside try/catch
+func isInsideTryCatch(node *ast.Node) bool {
+ current := node.Parent
+ for current != nil {
+ if current.Kind == ast.KindTryStatement ||
+ current.Kind == ast.KindCatchClause {
+ return true
+ }
+ current = current.Parent
+ }
+ return false
+}
+
+// Helper function to check if identifier is "use"
+func isUseIdentifier(node *ast.Node) bool {
+ return isReactFunction(node, "use")
+}
+
+// Helper function to check if node is at top level
+func isTopLevel(node *ast.Node) bool {
+ return node.Kind == ast.KindSourceFile
+}
+
+// Helper function to check if a call expression is a React function
+func isReactFunction(node *ast.Node, functionName string) bool {
+ if node == nil {
+ return false
+ }
+
+ switch node.Kind {
+ case ast.KindIdentifier:
+ // Direct call: forwardRef()
+ identifier := node.AsIdentifier()
+ if identifier != nil {
+ name := scanner.GetTextOfNode(&identifier.Node)
+ return name == functionName
+ }
+ case ast.KindPropertyAccessExpression:
+ // Property access: React.forwardRef()
+ propAccess := node.AsPropertyAccessExpression()
+ if propAccess != nil {
+ nameNode := propAccess.Name()
+ if nameNode != nil {
+ name := scanner.GetTextOfNode(nameNode)
+ if name == functionName {
+ // Check if the object is React
+ expr := propAccess.Expression
+ if expr != nil && expr.Kind == ast.KindIdentifier {
+ objName := scanner.GetTextOfNode(expr)
+ return objName == "React"
+ }
+ }
+ }
+ }
+ }
+ return false
+}
+
+// Helper function to check if the node is a callback argument of forwardRef
+// This render function should follow the rules of hooks
+func isForwardRefCallback(node *ast.Node) bool {
+ if node == nil || node.Parent == nil {
+ return false
+ }
+
+ parent := node.Parent
+ if parent.Kind == ast.KindCallExpression {
+ callExpr := parent.AsCallExpression()
+ if callExpr != nil && callExpr.Expression != nil {
+ return isReactFunction(callExpr.Expression, "forwardRef")
+ }
+ }
+ return false
+}
+
+// Helper function to check if the node is a callback argument of memo
+func isMemoCallback(node *ast.Node) bool {
+ if node == nil || node.Parent == nil {
+ return false
+ }
+
+ parent := node.Parent
+ if parent.Kind == ast.KindCallExpression {
+ callExpr := parent.AsCallExpression()
+ if callExpr != nil && callExpr.Expression != nil {
+ return isReactFunction(callExpr.Expression, "memo")
+ }
+ }
+ return false
+}
+
+// Helper function to check for flow suppression comments
+func hasFlowSuppression(node *ast.Node) bool {
+ // No need implementation
+ return false
+}
+
+// Helper function to get node text
+func getNodeText(node *ast.Node) string {
+ // This is a simplified implementation
+ // You would extract the text from the source code
+ if node != nil && node.Kind == ast.KindIdentifier {
+ return scanner.GetTextOfNode(node)
+ }
+ return ""
+}
+
+// Helper function to check if node is inside do-while loop
+func isInsideDoWhileLoop(node *ast.Node) bool {
+ current := node.Parent
+ for current != nil {
+ if current.Kind == ast.KindDoStatement {
+ return true
+ }
+ current = current.Parent
+ }
+ return false
+}
+
+// Helper function to check if function is async
+func isAsyncFunction(node *ast.Node) bool {
+ if isFunctionLike(node) {
+ return ast.HasSyntacticModifier(node, ast.ModifierFlagsAsync)
+ }
+ return false
+}
+
+// Helper function to check if node is useEffect identifier
+func isUseEffectIdentifier(node *ast.Node) bool {
+ if node == nil || node.Kind != ast.KindIdentifier {
+ return false
+ }
+ text := scanner.GetTextOfNode(node)
+ return text == "useEffect"
+}
+
+// Helper function to check if node is useEffectEvent identifier
+func isUseEffectEventIdentifier(node *ast.Node) bool {
+ if node == nil || node.Kind != ast.KindIdentifier {
+ return false
+ }
+ text := scanner.GetTextOfNode(node)
+ return text == "useEffectEvent"
+}
+
+// Helper function to get node without React namespace
+func getNodeWithoutReactNamespace(node *ast.Node) *ast.Node {
+ if node == nil {
+ return nil
+ }
+
+ // If it's React.someHook, return someHook
+ if node.Kind == ast.KindPropertyAccessExpression {
+ propAccess := node.AsPropertyAccessExpression()
+ if propAccess != nil {
+ expr := propAccess.Expression
+ if expr != nil && expr.Kind == ast.KindIdentifier {
+ identifier := expr.AsIdentifier()
+ if identifier != nil && scanner.GetTextOfNode(&identifier.Node) == "React" {
+ return propAccess.Name()
+ }
+ }
+ }
+ }
+
+ return node
+}
+
+// Helper function to get scope (simplified implementation)
+func getScope(node *ast.Node) *ast.Node {
+ // This is a simplified implementation
+ // In a real implementation, you would traverse the scope chain
+ return node
+}
+
+// Helper function to record all useEffectEvent functions (simplified)
+func recordAllUseEffectEventFunctions(scope *ast.Node) {
+ // !!! useEffectEvent
+}
+
+// Helper function to check if we're inside a component or hook (from scope context)
+func isInsideComponentOrHookFromScope(node *ast.Node) bool {
+ // This is a simplified implementation based on the existing isInsideComponentOrHook
+ return isInsideComponentOrHook(node)
+}
diff --git a/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks_test.go b/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks_test.go
new file mode 100644
index 00000000..d50e664b
--- /dev/null
+++ b/internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks_test.go
@@ -0,0 +1,1711 @@
+package rules_of_hooks_test
+
+import (
+ "testing"
+
+ "github.com/web-infra-dev/rslint/internal/plugins/import/fixtures"
+ "github.com/web-infra-dev/rslint/internal/plugins/react_hooks/rules/rules_of_hooks"
+ "github.com/web-infra-dev/rslint/internal/rule_tester"
+)
+
+func TestRulesOfHooks(t *testing.T) {
+ rule_tester.RunRuleTester(
+ fixtures.GetRootDir(),
+ "tsconfig.json",
+ t,
+ &rules_of_hooks.RulesOfHooksRule,
+ []rule_tester.ValidTestCase{
+ {
+ Code: `
+ // Valid because components can use hooks.
+ function ComponentWithHook() {
+ useHook();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because components can use hooks.
+ function createComponentWithHook() {
+ return function ComponentWithHook() {
+ useHook();
+ };
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can use hooks.
+ function useHookWithHook() {
+ useHook();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can use hooks.
+ function createHook() {
+ return function useHookWithHook() {
+ useHook();
+ }
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because components can call functions.
+ function ComponentWithNormalFunction() {
+ doSomething();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because functions can call functions.
+ function normalFunctionWithNormalFunction() {
+ doSomething();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because functions can call functions.
+ function normalFunctionWithConditionalFunction() {
+ if (cond) {
+ doSomething();
+ }
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because functions can call functions.
+ function functionThatStartsWithUseButIsntAHook() {
+ if (cond) {
+ userFetch();
+ }
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid although unconditional return doesn't make sense and would fail other rules.
+ // We could make it invalid but it doesn't matter.
+ function useUnreachable() {
+ return;
+ useHook();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function useHook() { useState(); }
+ const whatever = function useHook() { useState(); };
+ const useHook1 = () => { useState(); };
+ let useHook2 = () => useState();
+ useHook2 = () => { useState(); };
+ ({useHook: () => { useState(); }});
+ ({useHook() { useState(); }});
+ const {useHook3 = () => { useState(); }} = {};
+ ({useHook = () => { useState(); }} = {});
+ Namespace.useHook = () => { useState(); };
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function useHook() {
+ useHook1();
+ useHook2();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function createHook() {
+ return function useHook() {
+ useHook1();
+ useHook2();
+ };
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function useHook() {
+ useState() && a;
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function useHook() {
+ return useHook1() + useHook2();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can call hooks.
+ function useHook() {
+ return useHook1(useHook2());
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because hooks can be used in anonymous arrow-function arguments
+ // to forwardRef.
+ const FancyButton = React.forwardRef((props, ref) => {
+ useHook();
+ return
+ });
+ `,
+ Tsx: true,
+ },
+ {
+ Code: `
+ // Valid because hooks can be used in anonymous function arguments to
+ // forwardRef.
+ const FancyButton = React.forwardRef(function (props, ref) {
+ useHook();
+ return
+ });
+ `,
+ Tsx: true,
+ },
+ {
+ Code: `
+ // Valid because hooks can be used in anonymous function arguments to
+ // forwardRef.
+ const FancyButton = forwardRef(function (props, ref) {
+ useHook();
+ return
+ });
+ `,
+ Tsx: true,
+ },
+ {
+ Code: `
+ // Valid because hooks can be used in anonymous function arguments to
+ // React.memo.
+ const MemoizedFunction = React.memo(props => {
+ useHook();
+ return
+ });
+ `,
+ Tsx: true,
+ },
+ {
+ Code: `
+ // Valid because hooks can be used in anonymous function arguments to
+ // memo.
+ const MemoizedFunction = memo(function (props) {
+ useHook();
+ return
+ });
+ `,
+ Tsx: true,
+ },
+ {
+ Code: `
+ // Valid because classes can call functions.
+ // We don't consider these to be hooks.
+ class C {
+ m() {
+ this.useHook();
+ super.useHook();
+ }
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid -- this is a regression test.
+ jest.useFakeTimers();
+ beforeEach(() => {
+ jest.useRealTimers();
+ })
+ `,
+ },
+ {
+ Code: `
+ // Valid because they're not matching use[A-Z].
+ fooState();
+ _use();
+ _useState();
+ use_hook();
+ // also valid because it's not matching the PascalCase namespace
+ jest.useFakeTimer()
+ `,
+ },
+ {
+ Code: `
+ // Regression test for some internal code.
+ // This shows how the "callback rule" is more relaxed,
+ // and doesn't kick in unless we're confident we're in
+ // a component or a hook.
+ function makeListener(instance) {
+ each(pixelsWithInferredEvents, pixel => {
+ if (useExtendedSelector(pixel.id) && extendedButton) {
+ foo();
+ }
+ });
+ }
+ `,
+ },
+ {
+ Code: `
+ // This is valid because "use"-prefixed functions called in
+ // unnamed function arguments are not assumed to be hooks.
+ React.unknownFunction((foo, bar) => {
+ if (foo) {
+ useNotAHook(bar)
+ }
+ });
+ `,
+ },
+ {
+ Code: `
+ // This is valid because "use"-prefixed functions called in
+ // unnamed function arguments are not assumed to be hooks.
+ unknownFunction(function(foo, bar) {
+ if (foo) {
+ useNotAHook(bar)
+ }
+ });
+ `,
+ },
+ {
+ Code: `
+ // Regression test for incorrectly flagged valid code.
+ function RegressionTest() {
+ const foo = cond ? a : b;
+ useState();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because exceptions abort rendering
+ function RegressionTest() {
+ if (page == null) {
+ throw new Error('oh no!');
+ }
+ useState();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because the loop doesn't change the order of hooks calls.
+ function RegressionTest() {
+ const res = [];
+ const additionalCond = true;
+ for (let i = 0; i !== 10 && additionalCond; ++i ) {
+ res.push(i);
+ }
+ React.useLayoutEffect(() => {});
+ }
+ `,
+ },
+ {
+ Code: `
+ // Is valid but hard to compute by brute-forcing
+ function MyComponent() {
+ // 40 conditions
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+ if (c) {} else {}
+
+ // 10 hooks
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ useHook();
+ }
+ `,
+ },
+ {
+ Code: `
+ // Valid because the neither the conditions before or after the hook affect the hook call
+ // Failed prior to implementing BigInt because pathsFromStartToEnd and allPathsFromStartToEnd were too big and had rounding errors
+ const useSomeHook = () => {};
+
+ const SomeName = () => {
+ const filler = FILLER ?? FILLER ?? FILLER;
+ const filler2 = FILLER ?? FILLER ?? FILLER;
+ const filler3 = FILLER ?? FILLER ?? FILLER;
+ const filler4 = FILLER ?? FILLER ?? FILLER;
+ const filler5 = FILLER ?? FILLER ?? FILLER;
+ const filler6 = FILLER ?? FILLER ?? FILLER;
+ const filler7 = FILLER ?? FILLER ?? FILLER;
+ const filler8 = FILLER ?? FILLER ?? FILLER;
+
+ useSomeHook();
+
+ if (anyConditionCanEvenBeFalse) {
+ return null;
+ }
+
+ return (
+