Skip to content

Commit b9b071f

Browse files
committed
feat: pass all test cases
1 parent b4af881 commit b9b071f

File tree

7 files changed

+1771
-1706
lines changed

7 files changed

+1771
-1706
lines changed

internal/plugins/react_hooks/code_path_analysis/break_context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ func (s *CodePathState) MakeReturn() {
133133
returnCtx := s.getReturnContext()
134134
if returnCtx != nil {
135135
returnCtx.returnedForkContext.Add(forkContext.Head())
136+
} else {
137+
s.addReturnedSegments(forkContext.Head())
136138
}
137139
forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
138140
}

internal/plugins/react_hooks/code_path_analysis/code_path_analyzer.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,10 @@ func (analyzer *CodePathAnalyzer) preprocess(node *ast.Node) {
198198
state.MakeForInOfBody()
199199
}
200200

201-
case ast.KindBindingElement:
201+
case ast.KindParameter:
202202
// Handle assignment patterns (destructuring with defaults)
203-
bindingElem := parent.AsBindingElement()
204-
if bindingElem.Initializer == node {
203+
parameterDecl := parent.AsParameterDeclaration()
204+
if parameterDecl.Initializer == node {
205205
state.PushForkContext(nil)
206206
state.ForkBypassPath()
207207
state.ForkPath()
@@ -234,7 +234,10 @@ func (analyzer *CodePathAnalyzer) processCodePathToEnter(node *ast.Node) {
234234
case ast.KindSourceFile:
235235
analyzer.startCodePath("program", node)
236236

237-
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction:
237+
case ast.KindFunctionDeclaration,
238+
ast.KindFunctionExpression,
239+
ast.KindArrowFunction,
240+
ast.KindMethodDeclaration:
238241
analyzer.startCodePath("function", node)
239242

240243
case ast.KindClassStaticBlockDeclaration:
@@ -304,7 +307,7 @@ func (analyzer *CodePathAnalyzer) processCodePathToEnter(node *ast.Node) {
304307
label := getLabel(node)
305308
state.PushLoopContext(ForOfStatement, label)
306309
case ast.KindLabeledStatement:
307-
if !isBreakableType(node.Body().Kind) {
310+
if !isBreakableType(node.AsLabeledStatement().Statement.Kind) {
308311
state.PushBreakContext(false, node.Label().Text())
309312
}
310313
default:
@@ -334,7 +337,7 @@ func (analyzer *CodePathAnalyzer) processCodePathToExit(node *ast.Node) {
334337
binExpr := node.AsBinaryExpression()
335338
if isHandledLogicalOperator(binExpr.OperatorToken.Kind) ||
336339
isLogicalAssignmentOperator(binExpr.OperatorToken.Kind) {
337-
state.PopBreakContext()
340+
state.PopChoiceContext()
338341
}
339342

340343
case ast.KindSwitchStatement:
@@ -398,12 +401,13 @@ func (analyzer *CodePathAnalyzer) processCodePathToExit(node *ast.Node) {
398401
case ast.KindWhileStatement, ast.KindDoStatement, ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement:
399402
state.PopLoopContext()
400403

401-
case ast.KindBindingElement:
402-
state.PopForkContext()
404+
case ast.KindParameter:
405+
if node.Initializer() != nil {
406+
state.PopForkContext()
407+
}
403408

404409
case ast.KindLabeledStatement:
405-
labeledStmt := node.AsLabeledStatement()
406-
if !isBreakableType(labeledStmt.Body().Kind) {
410+
if !isBreakableType(node.AsLabeledStatement().Statement.Kind) {
407411
state.PopBreakContext()
408412
}
409413

@@ -423,7 +427,8 @@ func (analyzer *CodePathAnalyzer) postprocess(node *ast.Node) {
423427
ast.KindFunctionDeclaration,
424428
ast.KindFunctionExpression,
425429
ast.KindArrowFunction,
426-
ast.KindClassStaticBlockDeclaration:
430+
ast.KindClassStaticBlockDeclaration,
431+
ast.KindMethodDeclaration:
427432
analyzer.endCodePath(node)
428433

429434
// The `arguments.length >= 1` case is in `preprocess` function.

internal/plugins/react_hooks/code_path_analysis/code_path_state.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (s *CodePathState) addReturnedSegments(segments []*CodePathSegment) {
134134

135135
func (s *CodePathState) addThrownSegments(segments []*CodePathSegment) {
136136
for _, segment := range segments {
137-
s.returnedSegments = append(s.thrownSegments, segment)
137+
s.thrownSegments = append(s.thrownSegments, segment)
138138

139139
for _, returnSegment := range s.returnedSegments {
140140
if returnSegment == segment {

internal/plugins/react_hooks/code_path_analysis/loop_context.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,22 @@ func NewLoopContextForForOfStatement(state *CodePathState, label string) *LoopCo
9797
}
9898

9999
// Creates a context object of a loop statement and stacks it.
100-
func (s *CodePathState) PushLoopContext(kind LoopStatementKind, label string) *LoopContext {
100+
func (s *CodePathState) PushLoopContext(kind LoopStatementKind, label string) {
101+
s.PushBreakContext(true, label)
101102
switch kind {
102103
case WhileStatement:
103-
return NewLoopContextForWhileStatement(s, label)
104+
s.PushChoiceContext("loop", false)
105+
s.loopContext = NewLoopContextForWhileStatement(s, label)
104106
case DoWhileStatement:
105-
return NewLoopContextForDoWhileStatement(s, label)
107+
s.PushChoiceContext("loop", false)
108+
s.loopContext = NewLoopContextForDoWhileStatement(s, label)
106109
case ForStatement:
107-
return NewLoopContextForForStatement(s, label)
110+
s.PushChoiceContext("loop", false)
111+
s.loopContext = NewLoopContextForForStatement(s, label)
108112
case ForInStatement:
109-
return NewLoopContextForForInStatement(s, label)
113+
s.loopContext = NewLoopContextForForInStatement(s, label)
110114
case ForOfStatement:
111-
return NewLoopContextForForOfStatement(s, label)
115+
s.loopContext = NewLoopContextForForOfStatement(s, label)
112116
default:
113117
panic("unknown statement kind")
114118
}
@@ -127,7 +131,7 @@ func (s *CodePathState) PopLoopContext() {
127131
case WhileStatement, ForStatement:
128132
{
129133
s.PopChoiceContext()
130-
s.MakeLooped(forkContext.Head(), context.upper.continueDestSegments)
134+
s.MakeLooped(forkContext.Head(), context.continueDestSegments)
131135
}
132136
case DoWhileStatement:
133137
{

internal/plugins/react_hooks/code_path_analysis/try_context.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ func NewTryContext(state *CodePathState, hasFinalizer bool) *TryContext {
2727
}
2828

2929
// Creates a context object of TryStatement and stacks it.
30-
func (s *CodePathState) PushTryContext(hasFinalizer bool) *TryContext {
31-
return NewTryContext(s, hasFinalizer)
30+
func (s *CodePathState) PushTryContext(hasFinalizer bool) {
31+
s.tryContext = NewTryContext(s, hasFinalizer)
3232
}
3333

3434
// PopTryContext pops the last context of TryStatement and finalizes it.
@@ -63,12 +63,16 @@ func (s *CodePathState) PopTryContext() {
6363
returnCtx := s.getReturnContext()
6464
if returnCtx != nil {
6565
returnCtx.returnedForkContext.Add(leavingSegments)
66+
} else {
67+
s.addReturnedSegments(leavingSegments)
6668
}
6769
}
6870
if !thrown.IsEmpty() {
6971
throwCtx := s.getThrowContext()
7072
if throwCtx != nil {
7173
throwCtx.thrownForkContext.Add(leavingSegments)
74+
} else {
75+
s.addThrownSegments(leavingSegments)
7276
}
7377
}
7478

@@ -223,6 +227,8 @@ func (s *CodePathState) MakeThrow() {
223227
throwCtx := s.getThrowContext()
224228
if throwCtx != nil {
225229
throwCtx.thrownForkContext.Add(forkContext.Head())
230+
} else {
231+
s.addThrownSegments(forkContext.Head())
226232
}
227233
forkContext.ReplaceHead(forkContext.MakeUnreachable(-1, -1))
228234
}

internal/plugins/react_hooks/rules/rules_of_hooks/rules_of_hooks.go

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ var RulesOfHooksRule = rule.Rule{
6464
}
6565

6666
segmentID := segment.ID()
67-
if paths, exists := countPathsFromStartCache[segmentID]; exists {
67+
if paths, exists := countPathsFromStartCache[segmentID]; exists && paths != nil {
6868
return paths
6969
}
7070

@@ -98,7 +98,9 @@ var RulesOfHooksRule = rule.Rule{
9898
}
9999
}
100100

101-
if segment.Reachable() || paths.Cmp(big.NewInt(0)) > 0 {
101+
if segment.Reachable() && paths.Cmp(big.NewInt(0)) == 0 {
102+
countPathsFromStartCache[segmentID] = nil
103+
} else {
102104
countPathsFromStartCache[segmentID] = paths
103105
}
104106

@@ -318,14 +320,16 @@ var RulesOfHooksRule = rule.Rule{
318320
// Check if we're in a valid context for hooks
319321
if isDirectlyInsideComponentOrHook {
320322
// Check for async function
321-
if isAsyncFunction(codePathNode) {
323+
if isInsideAsyncFunction(codePathNode) {
322324
ctx.ReportNode(hook, buildAsyncComponentHookMessage(hookText))
323325
continue
324326
}
325327

328+
pathsCmp := pathsFromStartToEnd.Cmp(allPathsFromStartToEnd)
329+
326330
// Check for conditional calls (except use() and do-while loops)
327331
if !isCyclic &&
328-
pathsFromStartToEnd.Cmp(allPathsFromStartToEnd) != 0 &&
332+
pathsCmp != 0 &&
329333
!isUseHook &&
330334
!isInsideDoWhileLoop(hook) {
331335
var message rule.RuleMessage
@@ -341,10 +345,18 @@ var RulesOfHooksRule = rule.Rule{
341345
if isInsideClass(codePathNode) {
342346
ctx.ReportNode(hook, buildClassHookMessage(hookText))
343347
} else if codePathFunctionName != "" {
348+
// Custom message if we found an invalid function name.kj
344349
ctx.ReportNode(hook, buildFunctionHookMessage(hookText, codePathFunctionName))
345350
} else if isTopLevel(codePathNode) {
351+
// These are dangerous if you have inline requires enabled.
346352
ctx.ReportNode(hook, buildTopLevelHookMessage(hookText))
347353
} else if isSomewhereInsideComponentOrHook && !isUseHook {
354+
// Assume in all other cases the user called a hook in some
355+
// random function callback. This should usually be true for
356+
// anonymous function expressions. Hopefully this is clarifying
357+
// enough in the common case that the incorrect message in
358+
// uncommon cases doesn't matter.
359+
// `use(...)` can be called in callbacks.
348360
ctx.ReportNode(hook, buildGenericHookMessage(hookText))
349361
}
350362
}
@@ -569,16 +581,52 @@ func getFunctionName(node *ast.Node) string {
569581
// Function declaration or function expression names win over any
570582
// assignment statements or other renames.
571583
return node.AsFunctionDeclaration().Name().Text()
584+
case ast.KindFunctionExpression:
585+
name := node.AsFunctionExpression().Name()
586+
if name != nil {
587+
return node.AsFunctionExpression().Name().Text()
588+
}
572589
case ast.KindArrowFunction:
573-
// const useHook = () => {};
574-
return node.AsArrowFunction().Text()
590+
if node.Parent != nil {
591+
switch node.Parent.Kind {
592+
case ast.KindVariableDeclaration, // const useHook = () => {};
593+
ast.KindShorthandPropertyAssignment, // ({k = () => { useState(); }} = {});
594+
ast.KindBindingElement, // const {j = () => { useState(); }} = {};
595+
ast.KindPropertyAssignment: // ({f: () => { useState(); }});
596+
if ast.IsInExpressionContext(node) {
597+
return node.Parent.Name().Text()
598+
}
599+
case ast.KindBinaryExpression:
600+
if node.Parent.AsBinaryExpression().Right == node {
601+
left := node.Parent.AsBinaryExpression().Left
602+
switch left.Kind {
603+
case ast.KindIdentifier:
604+
// e = () => { useState(); };
605+
return left.AsIdentifier().Text
606+
case ast.KindPropertyAccessExpression:
607+
// Namespace.useHook = () => { useState(); };
608+
return left.AsPropertyAccessExpression().Name().Text()
609+
}
610+
}
611+
}
612+
}
613+
return ""
575614
case ast.KindMethodDeclaration:
615+
// NOTE: We could also support `ClassProperty` and `MethodDefinition`
616+
// here to be pedantic. However, hooks in a class are an anti-pattern. So
617+
// we don't allow it to error early.
618+
//
619+
// class {useHook = () => {}}
620+
// class {useHook() {}}
621+
if ast.GetContainingClass(node) != nil {
622+
return ""
623+
}
624+
576625
// {useHook: () => {}}
577626
// {useHook() {}}
578-
return node.AsMethodDeclaration().Text()
579-
default:
580-
return ""
627+
return node.AsMethodDeclaration().Name().Text()
581628
}
629+
return ""
582630
}
583631

584632
// Helper function to check if node is inside a component or hook
@@ -588,10 +636,10 @@ func isInsideComponentOrHook(node *ast.Node) bool {
588636
current := node
589637
for current != nil {
590638
functionName := getFunctionName(current)
591-
if isComponentName(functionName) || isHookName(functionName) {
639+
if functionName != "" && (isComponentName(functionName) || isHookName(functionName)) {
592640
return true
593641
}
594-
if isForwardRefCallback(node) || isMemoCallback(node) {
642+
if isForwardRefCallback(current) || isMemoCallback(current) {
595643
return true
596644
}
597645
current = current.Parent
@@ -666,22 +714,10 @@ func isInsideClass(node *ast.Node) bool {
666714

667715
// Helper function to check if node is inside an async function
668716
func isInsideAsyncFunction(node *ast.Node) bool {
669-
current := node.Parent
717+
current := node
670718
for current != nil {
671-
if isFunctionLike(current) {
672-
// TODO: Check if function has async modifier
673-
// This requires checking the modifiers array
674-
// For now, check specific function types
675-
if current.Kind == ast.KindFunctionDeclaration {
676-
funcDecl := current.AsFunctionDeclaration()
677-
if funcDecl != nil {
678-
// TODO: Check for async modifier in modifiers
679-
return false // placeholder
680-
}
681-
} else if current.Kind == ast.KindArrowFunction {
682-
// TODO: Check for async modifier
683-
return false // placeholder
684-
}
719+
if isAsyncFunction(current) {
720+
return true
685721
}
686722
current = current.Parent
687723
}
@@ -756,14 +792,15 @@ func isHookCall(node *ast.Node) (bool, string) {
756792

757793
// Helper function to check if node is at top level
758794
func isTopLevel(node *ast.Node) bool {
759-
current := node.Parent
760-
for current != nil {
761-
if isFunctionLike(current) {
762-
return false
763-
}
764-
current = current.Parent
765-
}
766-
return true
795+
// current := node.Parent
796+
// for current != nil {
797+
// if isFunctionLike(current) {
798+
// return false
799+
// }
800+
// current = current.Parent
801+
// }
802+
// return true
803+
return node.Kind == ast.KindSourceFile
767804
}
768805

769806
// Helper function to check if a call expression is a React function
@@ -864,8 +901,9 @@ func isInsideDoWhileLoop(node *ast.Node) bool {
864901

865902
// Helper function to check if function is async
866903
func isAsyncFunction(node *ast.Node) bool {
867-
// This is a simplified implementation
868-
// You would check the modifiers for async keyword
904+
if isFunctionLike(node) {
905+
return ast.HasSyntacticModifier(node, ast.ModifierFlagsAsync)
906+
}
869907
return false
870908
}
871909

@@ -919,8 +957,7 @@ func getScope(node *ast.Node) *ast.Node {
919957

920958
// Helper function to record all useEffectEvent functions (simplified)
921959
func recordAllUseEffectEventFunctions(scope *ast.Node) {
922-
// This is a simplified implementation
923-
// In a real implementation, you would traverse the scope and find all useEffectEvent declarations
960+
// !!! useEffectEvent
924961
}
925962

926963
// Helper function to check if we're inside a component or hook (from scope context)

0 commit comments

Comments
 (0)