@kernlang/review 3.1.6 → 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/cache.d.ts +1 -1
  2. package/dist/cache.js +5 -3
  3. package/dist/cache.js.map +1 -1
  4. package/dist/call-graph.d.ts +63 -0
  5. package/dist/call-graph.js +380 -0
  6. package/dist/call-graph.js.map +1 -0
  7. package/dist/concept-rules/boundary-mutation.d.ts +1 -1
  8. package/dist/concept-rules/boundary-mutation.js.map +1 -1
  9. package/dist/concept-rules/ignored-error.d.ts +1 -1
  10. package/dist/concept-rules/ignored-error.js.map +1 -1
  11. package/dist/concept-rules/illegal-dependency.d.ts +1 -1
  12. package/dist/concept-rules/illegal-dependency.js.map +1 -1
  13. package/dist/concept-rules/index.js +1 -6
  14. package/dist/concept-rules/index.js.map +1 -1
  15. package/dist/concept-rules/unguarded-effect.d.ts +1 -1
  16. package/dist/concept-rules/unguarded-effect.js.map +1 -1
  17. package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
  18. package/dist/concept-rules/unrecovered-effect.js +2 -1
  19. package/dist/concept-rules/unrecovered-effect.js.map +1 -1
  20. package/dist/confidence.js +12 -8
  21. package/dist/confidence.js.map +1 -1
  22. package/dist/differ.js +3 -7
  23. package/dist/differ.js.map +1 -1
  24. package/dist/external-tools.js +5 -6
  25. package/dist/external-tools.js.map +1 -1
  26. package/dist/file-context.d.ts +21 -0
  27. package/dist/file-context.js +234 -0
  28. package/dist/file-context.js.map +1 -0
  29. package/dist/file-role.js +14 -7
  30. package/dist/file-role.js.map +1 -1
  31. package/dist/graph.d.ts +1 -1
  32. package/dist/graph.js +24 -16
  33. package/dist/graph.js.map +1 -1
  34. package/dist/index.d.ts +44 -35
  35. package/dist/index.js +210 -121
  36. package/dist/index.js.map +1 -1
  37. package/dist/inferrer.d.ts +8 -2
  38. package/dist/inferrer.js +80 -47
  39. package/dist/inferrer.js.map +1 -1
  40. package/dist/kern-lint.d.ts +3 -4
  41. package/dist/kern-lint.js +7 -5
  42. package/dist/kern-lint.js.map +1 -1
  43. package/dist/llm-bridge.d.ts +23 -7
  44. package/dist/llm-bridge.js +267 -31
  45. package/dist/llm-bridge.js.map +1 -1
  46. package/dist/llm-review.d.ts +16 -2
  47. package/dist/llm-review.js +240 -35
  48. package/dist/llm-review.js.map +1 -1
  49. package/dist/mappers/ts-concepts.d.ts +1 -1
  50. package/dist/mappers/ts-concepts.js +303 -32
  51. package/dist/mappers/ts-concepts.js.map +1 -1
  52. package/dist/norm-miner.d.ts +31 -0
  53. package/dist/norm-miner.js +119 -0
  54. package/dist/norm-miner.js.map +1 -0
  55. package/dist/obligations.d.ts +63 -0
  56. package/dist/obligations.js +158 -0
  57. package/dist/obligations.js.map +1 -0
  58. package/dist/quality-rules.d.ts +3 -3
  59. package/dist/quality-rules.js +4 -2
  60. package/dist/quality-rules.js.map +1 -1
  61. package/dist/reporter.d.ts +7 -2
  62. package/dist/reporter.js +82 -51
  63. package/dist/reporter.js.map +1 -1
  64. package/dist/rule-eval.d.ts +1 -2
  65. package/dist/rule-eval.js +5 -9
  66. package/dist/rule-eval.js.map +1 -1
  67. package/dist/rule-loader.js +16 -14
  68. package/dist/rule-loader.js.map +1 -1
  69. package/dist/rules/base.js +153 -69
  70. package/dist/rules/base.js.map +1 -1
  71. package/dist/rules/cli.js +23 -19
  72. package/dist/rules/cli.js.map +1 -1
  73. package/dist/rules/confidence.d.ts +1 -1
  74. package/dist/rules/confidence.js +5 -5
  75. package/dist/rules/confidence.js.map +1 -1
  76. package/dist/rules/dead-code.d.ts +10 -0
  77. package/dist/rules/dead-code.js +75 -0
  78. package/dist/rules/dead-code.js.map +1 -0
  79. package/dist/rules/dead-logic.js +35 -31
  80. package/dist/rules/dead-logic.js.map +1 -1
  81. package/dist/rules/express.d.ts +2 -1
  82. package/dist/rules/express.js +380 -126
  83. package/dist/rules/express.js.map +1 -1
  84. package/dist/rules/fastapi.js +53 -19
  85. package/dist/rules/fastapi.js.map +1 -1
  86. package/dist/rules/ground-layer.js +3 -3
  87. package/dist/rules/ground-layer.js.map +1 -1
  88. package/dist/rules/index.js +574 -105
  89. package/dist/rules/index.js.map +1 -1
  90. package/dist/rules/ink.js +9 -8
  91. package/dist/rules/ink.js.map +1 -1
  92. package/dist/rules/kern-source.js +202 -63
  93. package/dist/rules/kern-source.js.map +1 -1
  94. package/dist/rules/nextjs.js +88 -33
  95. package/dist/rules/nextjs.js.map +1 -1
  96. package/dist/rules/null-safety.js +52 -26
  97. package/dist/rules/null-safety.js.map +1 -1
  98. package/dist/rules/nuxt.js +24 -29
  99. package/dist/rules/nuxt.js.map +1 -1
  100. package/dist/rules/react.js +355 -69
  101. package/dist/rules/react.js.map +1 -1
  102. package/dist/rules/security-v2.js +71 -57
  103. package/dist/rules/security-v2.js.map +1 -1
  104. package/dist/rules/security-v3.js.map +1 -1
  105. package/dist/rules/security-v4.js +54 -27
  106. package/dist/rules/security-v4.js.map +1 -1
  107. package/dist/rules/security.js +35 -5
  108. package/dist/rules/security.js.map +1 -1
  109. package/dist/rules/terminal.js +17 -5
  110. package/dist/rules/terminal.js.map +1 -1
  111. package/dist/rules/vue.js +162 -107
  112. package/dist/rules/vue.js.map +1 -1
  113. package/dist/semantic-diff.d.ts +52 -0
  114. package/dist/semantic-diff.js +342 -0
  115. package/dist/semantic-diff.js.map +1 -0
  116. package/dist/spec-checker.js +11 -10
  117. package/dist/spec-checker.js.map +1 -1
  118. package/dist/suppression/apply-suppression.d.ts +2 -3
  119. package/dist/suppression/apply-suppression.js +3 -3
  120. package/dist/suppression/apply-suppression.js.map +1 -1
  121. package/dist/suppression/index.d.ts +2 -2
  122. package/dist/suppression/index.js +1 -1
  123. package/dist/suppression/index.js.map +1 -1
  124. package/dist/suppression/parse-directives.d.ts +1 -1
  125. package/dist/suppression/parse-directives.js +9 -4
  126. package/dist/suppression/parse-directives.js.map +1 -1
  127. package/dist/taint-ast.d.ts +20 -0
  128. package/dist/taint-ast.js +427 -0
  129. package/dist/taint-ast.js.map +1 -0
  130. package/dist/taint-crossfile.d.ts +28 -0
  131. package/dist/taint-crossfile.js +174 -0
  132. package/dist/taint-crossfile.js.map +1 -0
  133. package/dist/taint-findings.d.ts +17 -0
  134. package/dist/taint-findings.js +131 -0
  135. package/dist/taint-findings.js.map +1 -0
  136. package/dist/taint-regex.d.ts +61 -0
  137. package/dist/taint-regex.js +379 -0
  138. package/dist/taint-regex.js.map +1 -0
  139. package/dist/taint-types.d.ts +128 -0
  140. package/dist/taint-types.js +174 -0
  141. package/dist/taint-types.js.map +1 -0
  142. package/dist/taint.d.ts +13 -107
  143. package/dist/taint.js +16 -1067
  144. package/dist/taint.js.map +1 -1
  145. package/dist/template-detector.d.ts +2 -2
  146. package/dist/template-detector.js +11 -16
  147. package/dist/template-detector.js.map +1 -1
  148. package/dist/types.d.ts +35 -0
  149. package/dist/types.js.map +1 -1
  150. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"nuxt.js","sourceRoot":"","sources":["../../src/rules/nuxt.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,SAAS,IAAI,CAAC,IAAY,EAAE,IAAY,EAAE,GAAG,GAAG,CAAC;IAC/C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CACd,MAAc,EACd,QAAsC,EACtC,QAAmC,EACnC,OAAe,EACf,IAAY,EACZ,IAAY,EACZ,KAA8B;IAE9B,OAAO;QACL,MAAM,EAAE,MAAM;QACd,MAAM;QACN,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QAC7B,WAAW,EAAE,iBAAiB,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,GAAG,KAAK;KACT,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,6FAA6F;AAE7F,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;AAE1G,SAAS,eAAe,CAAC,GAAgB;IACvC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IAE9C,yBAAyB;IACzB,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC5F,sDAAsD;IACtD,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,QAAQ,CAAC;IAEvD,oFAAoF;IACpF,MAAM,UAAU,GAA4B,EAAE,CAAC;IAC/C,MAAM,UAAU,GAAG,oFAAoF,CAAC;IACxG,IAAI,UAAU,CAAC;IACf,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACzD,+EAA+E;QAC/E,MAAM,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7D,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,SAAS;QAC7B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;QAClB,KAAK,IAAI,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxD,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACxB,IAAI,KAAK,KAAK,CAAC;oBAAE,UAAU,GAAG,CAAC,CAAC;gBAChC,KAAK,EAAE,CAAC;YACV,CAAC;YACD,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACxB,KAAK,EAAE,CAAC;gBACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;oBAAC,QAAQ,GAAG,CAAC,CAAC;oBAAC,MAAM;gBAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QACD,IAAI,UAAU,KAAK,CAAC,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;YACzC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,kEAAkE;IAClE,MAAM,YAAY,GAAG,iBAAiB,CAAC;IACvC,IAAI,YAAY,CAAC;IACjB,OAAO,CAAC,YAAY,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,KAAK,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAAC,KAAK,EAAE,CAAC;gBAAC,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;oBAAC,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;oBAAC,MAAM;gBAAC,CAAC;YAAC,CAAC;QAChG,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IAEzF,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,MAAM,4CAA4C,EAAE,GAAG,CAAC,CAAC;QACxF,IAAI,KAAK,CAAC;QACV,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC/C,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC;gBAAE,SAAS;YAEzC,0BAA0B;YAC1B,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YACvG,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAClF,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAE1C,wCAAwC;YACxC,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,SAAS;YACnC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAErB,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;YACnE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,OAAO,EAAE,KAAK,EACvD,IAAI,MAAM,mEAAmE,EAC7E,GAAG,CAAC,QAAQ,EAAE,IAAI,EAClB,EAAE,UAAU,EAAE,wDAAwD,EAAE,CAAC,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2EAA2E;AAC3E,wEAAwE;AAExE,SAAS,eAAe,CAAC,GAAgB;IACvC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IAE9C,oFAAoF;IACpF,MAAM,eAAe,GAAG,4CAA4C,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxF,IAAI,CAAC,eAAe;QAAE,OAAO,QAAQ,CAAC;IAEtC,6DAA6D;IAC7D,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACtG,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,yBAAyB;IACzB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,oBAAoB,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,UAAU,CAAC,UAAU;YAAE,SAAS;QACvD,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,OAAO;YAAE,SAAS;QAEzC,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,SAAS,EAAE,SAAS,EAC7D,uFAAuF,EACvF,GAAG,CAAC,QAAQ,EAAE,IAAI,EAClB,EAAE,UAAU,EAAE,sEAAsE,EAAE,CAAC,CAAC,CAAC;QAC3F,MAAM,CAAC,uBAAuB;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2EAA2E;AAC3E,iEAAiE;AAEjE,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ;IACvF,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe;IACzE,KAAK,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC,CAAC;AAEvC,SAAS,eAAe,CAAC,GAAgB;IACvC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,iCAAiC;IACjC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE3F,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IAE9C,0EAA0E;IAC1E,sFAAsF;IACtF,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,oBAAoB,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAE5B,0EAA0E;QAC1E,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,GAAG,GAAG,CAAC,kBAAkB,EAAE,CAAC;gBACtC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,OAAO,EAAE,KAAK,EACvD,gCAAgC,KAAK,8CAA8C,EACnF,GAAG,CAAC,QAAQ,EAAE,IAAI,EAClB,EAAE,UAAU,EAAE,6FAA6F,EAAE,CAAC,CAAC,CAAC;gBAClH,OAAO,QAAQ,CAAC,CAAC,uBAAuB;YAC1C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2EAA2E;AAE3E,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,eAAe;IACf,eAAe;IACf,eAAe;CAChB,CAAC"}
1
+ {"version":3,"file":"nuxt.js","sourceRoot":"","sources":["../../src/rules/nuxt.ts"],"names":[],"mappings":"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"}
@@ -3,33 +3,41 @@
3
3
  *
4
4
  * Catches React-specific bugs that KERN IR + AST can detect mechanically.
5
5
  */
6
- import { SyntaxKind, Node } from 'ts-morph';
7
- import { createFingerprint } from '../types.js';
8
- function span(file, line, col = 1) {
9
- return { file, startLine: line, startCol: col, endLine: line, endCol: col };
10
- }
11
- function finding(ruleId, severity, category, message, file, line, extra) {
12
- return {
13
- source: 'kern',
14
- ruleId,
15
- severity,
16
- category,
17
- message,
18
- primarySpan: span(file, line),
19
- fingerprint: createFingerprint(ruleId, line, 1),
20
- ...extra,
21
- };
6
+ import { Node, SyntaxKind } from 'ts-morph';
7
+ import { finding } from './utils.js';
8
+ /**
9
+ * Check if a file is actually a React file has JSX syntax or React imports.
10
+ * Backend/utility files in a React-targeted project should not trigger React rules.
11
+ */
12
+ function isReactFile(ctx) {
13
+ if (ctx.sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).length > 0)
14
+ return true;
15
+ if (ctx.sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0)
16
+ return true;
17
+ if (ctx.sourceFile.getImportDeclarations().some((i) => i.getModuleSpecifierValue() === 'react'))
18
+ return true;
19
+ const fullText = ctx.sourceFile.getFullText();
20
+ if (/\buse(?:State|Effect|Ref|Callback|Memo|Reducer|Context)\s*[<(]/.test(fullText))
21
+ return true;
22
+ return false;
22
23
  }
23
24
  // ── Rule 11: async-effect ────────────────────────────────────────────────
24
25
  // useEffect(async () => ...) — React doesn't support async effect callbacks
25
26
  function asyncEffect(ctx) {
26
27
  const findings = [];
27
- const fullText = ctx.sourceFile.getFullText();
28
- const asyncEffectRegex = /useEffect\s*\(\s*async\s/g;
29
- let match;
30
- while ((match = asyncEffectRegex.exec(fullText)) !== null) {
31
- const line = fullText.substring(0, match.index).split('\n').length;
32
- findings.push(finding('async-effect', 'error', 'bug', 'useEffect callback must not be async — use an inner async function instead', ctx.filePath, line, { suggestion: 'useEffect(() => { async function run() { ... } run(); }, [])' }));
28
+ for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
29
+ const callee = call.getExpression().getText();
30
+ if (callee !== 'useEffect' && callee !== 'React.useEffect' && callee !== 'useLayoutEffect')
31
+ continue;
32
+ const args = call.getArguments();
33
+ if (args.length === 0)
34
+ continue;
35
+ const callback = args[0];
36
+ if (Node.isArrowFunction(callback) || Node.isFunctionExpression(callback)) {
37
+ if (callback.isAsync()) {
38
+ findings.push(finding('async-effect', 'error', 'bug', 'useEffect callback must not be async — use an inner async function instead', ctx.filePath, callback.getStartLineNumber(), 1, { suggestion: 'useEffect(() => { async function run() { ... } run(); }, [])' }));
39
+ }
40
+ }
33
41
  }
34
42
  return findings;
35
43
  }
@@ -37,6 +45,9 @@ function asyncEffect(ctx) {
37
45
  // setState or fetch called directly in render body (outside hooks/handlers)
38
46
  function renderSideEffect(ctx) {
39
47
  const findings = [];
48
+ // Gate: skip non-React files
49
+ if (!isReactFile(ctx))
50
+ return findings;
40
51
  function checkBlock(block, name) {
41
52
  for (const stmt of block.getStatements()) {
42
53
  if (stmt.getKind() === SyntaxKind.ReturnStatement)
@@ -52,12 +63,14 @@ function renderSideEffect(ctx) {
52
63
  const exprText = exprStmt.getExpression().getText();
53
64
  if (/\b(useEffect|useLayoutEffect|useCallback|useMemo|useInsertionEffect)\s*\(/.test(exprText))
54
65
  continue;
55
- if (/\bset[A-Z]\w*\(/.test(exprText) && !exprText.includes('useState') &&
66
+ if (/\bset[A-Z]\w*\(/.test(exprText) &&
67
+ !exprText.includes('useState') &&
56
68
  !/\b(setTimeout|setInterval|setImmediate|setAttribute|setProperty|setHeader|setRequestHeader|setItem|setCustomValidity)\s*\(/.test(exprText)) {
57
- findings.push(finding('render-side-effect', 'error', 'bug', `setState called in render body of '${name}' — move to useEffect or event handler`, ctx.filePath, stmt.getStartLineNumber()));
69
+ findings.push(finding('render-side-effect', 'error', 'bug', `setState called in render body of '${name}' — move to useEffect or event handler`, ctx.filePath, stmt.getStartLineNumber(), 1));
58
70
  }
59
- if (/\bfetch\s*\(/.test(exprText)) {
60
- findings.push(finding('render-side-effect', 'error', 'bug', `fetch() called in render body of '${name}' — move to useEffect or event handler`, ctx.filePath, stmt.getStartLineNumber()));
71
+ const expr = exprStmt.getExpression();
72
+ if (Node.isCallExpression(expr) && expr.getExpression().getText() === 'fetch') {
73
+ findings.push(finding('render-side-effect', 'error', 'bug', `fetch() called in render body of '${name}' — move to useEffect or event handler`, ctx.filePath, stmt.getStartLineNumber(), 1));
61
74
  }
62
75
  }
63
76
  }
@@ -108,8 +121,7 @@ function unstableKey(ctx) {
108
121
  if (args.length === 0)
109
122
  continue;
110
123
  const callback = args[0];
111
- if (callback.getKind() !== SyntaxKind.ArrowFunction &&
112
- callback.getKind() !== SyntaxKind.FunctionExpression)
124
+ if (callback.getKind() !== SyntaxKind.ArrowFunction && callback.getKind() !== SyntaxKind.FunctionExpression)
113
125
  continue;
114
126
  // Get the index parameter (second param of the callback)
115
127
  const params = callback.getKind() === SyntaxKind.ArrowFunction
@@ -152,10 +164,12 @@ function unstableKey(ctx) {
152
164
  break;
153
165
  }
154
166
  if (usesIndexKey) {
155
- findings.push(finding('unstable-key', 'warning', 'bug', `key={${indexParam}} uses array index — use a stable identifier instead`, ctx.filePath, line, { suggestion: 'Use a unique ID from the data (e.g., key={item.id})' }));
167
+ findings.push(finding('unstable-key', 'warning', 'bug', `key={${indexParam}} uses array index — use a stable identifier instead`, ctx.filePath, line, 1, { suggestion: 'Use a unique ID from the data (e.g., key={item.id})' }));
156
168
  }
157
169
  else if (!hasKey) {
158
- findings.push(finding('unstable-key', 'warning', 'bug', 'JSX in .map() is missing a key prop', ctx.filePath, line, { suggestion: 'Add key={item.id} to the root JSX element in .map()' }));
170
+ findings.push(finding('unstable-key', 'warning', 'bug', 'JSX in .map() is missing a key prop', ctx.filePath, line, 1, {
171
+ suggestion: 'Add key={item.id} to the root JSX element in .map()',
172
+ }));
159
173
  }
160
174
  }
161
175
  return findings;
@@ -164,28 +178,26 @@ function unstableKey(ctx) {
164
178
  // Timer captures state not in dependency array
165
179
  function staleClosure(ctx) {
166
180
  const findings = [];
167
- // AST-based: find useEffect() calls and analyze deps + timer usage
168
181
  for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
169
- const callee = call.getExpression();
170
- if (callee.getText() !== 'useEffect')
182
+ const callee = call.getExpression().getText();
183
+ if (callee !== 'useEffect' && callee !== 'useLayoutEffect')
171
184
  continue;
172
185
  const args = call.getArguments();
173
186
  if (args.length < 2)
174
187
  continue;
175
- // First arg: the effect callback
176
- const callbackText = args[0].getText();
177
- // Second arg: deps array
188
+ const callback = args[0];
178
189
  const depsArg = args[1];
179
- if (depsArg.getKind() !== SyntaxKind.ArrayLiteralExpression)
180
- continue;
181
- const depsArray = depsArg;
182
- const deps = depsArray.getElements();
183
- // Empty deps [] + timer in callback = stale closure risk
184
- if (deps.length === 0) {
185
- const hasTimer = /\b(?:setInterval|setTimeout)\s*\(/.test(callbackText);
186
- if (hasTimer) {
187
- findings.push(finding('stale-closure', 'warning', 'bug', 'Timer in useEffect with empty deps [] may capture stale state', ctx.filePath, call.getStartLineNumber(), { suggestion: 'Use a ref for the latest value or add dependencies' }));
188
- }
190
+ if (!Node.isArrayLiteralExpression(depsArg))
191
+ continue;
192
+ if (depsArg.getElements().length !== 0)
193
+ continue;
194
+ // Pure AST: find setInterval/setTimeout calls inside the callback
195
+ const timers = callback.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => {
196
+ const name = c.getExpression().getText();
197
+ return name === 'setInterval' || name === 'setTimeout';
198
+ });
199
+ if (timers.length > 0) {
200
+ findings.push(finding('stale-closure', 'warning', 'bug', 'Timer in useEffect with empty deps [] may capture stale state', ctx.filePath, call.getStartLineNumber(), 1, { suggestion: 'Use a ref for the latest value or add dependencies' }));
189
201
  }
190
202
  }
191
203
  return findings;
@@ -194,29 +206,29 @@ function staleClosure(ctx) {
194
206
  // >5 useState calls in a single component — should be useReducer or machine
195
207
  function stateExplosion(ctx) {
196
208
  const findings = [];
197
- const fullText = ctx.sourceFile.getFullText();
209
+ function checkFn(fn, name) {
210
+ const useStates = fn.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => {
211
+ const text = c.getExpression().getText();
212
+ return text === 'useState' || text === 'React.useState';
213
+ });
214
+ if (useStates.length > 5) {
215
+ findings.push(finding('state-explosion', 'warning', 'pattern', `Component '${name}' has ${useStates.length} useState calls — consider useReducer or a state machine`, ctx.filePath, fn.getStartLineNumber(), 1, { suggestion: 'Use useReducer for complex state, or a KERN machine node for state transitions' }));
216
+ }
217
+ }
198
218
  for (const fn of ctx.sourceFile.getFunctions()) {
199
219
  const name = fn.getName() || '';
200
220
  if (!name || name[0] !== name[0].toUpperCase())
201
221
  continue;
202
- const body = fn.getBody()?.getText() || '';
203
- const useStateCount = (body.match(/useState\s*[<(]/g) || []).length;
204
- if (useStateCount > 5) {
205
- findings.push(finding('state-explosion', 'warning', 'pattern', `Component '${name}' has ${useStateCount} useState calls — consider useReducer or a state machine`, ctx.filePath, fn.getStartLineNumber(), { suggestion: 'Use useReducer for complex state, or a KERN machine node for state transitions' }));
206
- }
222
+ checkFn(fn, name);
207
223
  }
208
- // Also check arrow function components
209
224
  for (const stmt of ctx.sourceFile.getVariableStatements()) {
210
225
  for (const decl of stmt.getDeclarations()) {
211
226
  const name = decl.getName();
212
227
  if (!name || name[0] !== name[0].toUpperCase())
213
228
  continue;
214
- const init = decl.getInitializer()?.getText() || '';
215
- if (!init.includes('=>'))
216
- continue;
217
- const useStateCount = (init.match(/useState\s*[<(]/g) || []).length;
218
- if (useStateCount > 5) {
219
- findings.push(finding('state-explosion', 'warning', 'pattern', `Component '${name}' has ${useStateCount} useState calls — consider useReducer or a state machine`, ctx.filePath, stmt.getStartLineNumber()));
229
+ const init = decl.getInitializer();
230
+ if (init && Node.isArrowFunction(init)) {
231
+ checkFn(init, name);
220
232
  }
221
233
  }
222
234
  }
@@ -224,10 +236,23 @@ function stateExplosion(ctx) {
224
236
  }
225
237
  // ── Rule 16: hook-order ──────────────────────────────────────────────────
226
238
  // Conditional hook calls (hooks inside if/loop/early return)
227
- const HOOK_NAMES = new Set(['useState', 'useEffect', 'useCallback', 'useMemo', 'useRef',
228
- 'useContext', 'useReducer', 'useLayoutEffect', 'useImperativeHandle',
229
- 'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
230
- 'useSyncExternalStore', 'useInsertionEffect']);
239
+ const HOOK_NAMES = new Set([
240
+ 'useState',
241
+ 'useEffect',
242
+ 'useCallback',
243
+ 'useMemo',
244
+ 'useRef',
245
+ 'useContext',
246
+ 'useReducer',
247
+ 'useLayoutEffect',
248
+ 'useImperativeHandle',
249
+ 'useDebugValue',
250
+ 'useDeferredValue',
251
+ 'useTransition',
252
+ 'useId',
253
+ 'useSyncExternalStore',
254
+ 'useInsertionEffect',
255
+ ]);
231
256
  function hookOrder(ctx) {
232
257
  const findings = [];
233
258
  // Collect all control-flow nodes (if/for/while/do)
@@ -241,9 +266,9 @@ function hookOrder(ctx) {
241
266
  ];
242
267
  for (const cfNode of controlFlowNodes) {
243
268
  // Only flag hooks inside components (capitalized) or custom hooks (use*)
244
- let enclosingFn = cfNode.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration)
245
- || cfNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction)
246
- || cfNode.getFirstAncestorByKind(SyntaxKind.FunctionExpression);
269
+ const enclosingFn = cfNode.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration) ||
270
+ cfNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction) ||
271
+ cfNode.getFirstAncestorByKind(SyntaxKind.FunctionExpression);
247
272
  if (!enclosingFn)
248
273
  continue;
249
274
  const fnName = enclosingFn.getName?.() || '';
@@ -263,7 +288,7 @@ function hookOrder(ctx) {
263
288
  if (reported.has(hookName))
264
289
  continue;
265
290
  reported.add(hookName);
266
- findings.push(finding('hook-order', 'error', 'bug', `Hook '${hookName}' called inside ${label} — violates Rules of Hooks`, ctx.filePath, cfNode.getStartLineNumber(), { suggestion: 'Move hook call to top level of component' }));
291
+ findings.push(finding('hook-order', 'error', 'bug', `Hook '${hookName}' called inside ${label} — violates Rules of Hooks`, ctx.filePath, cfNode.getStartLineNumber(), 1, { suggestion: 'Move hook call to top level of component' }));
267
292
  }
268
293
  }
269
294
  return findings;
@@ -303,7 +328,7 @@ function effectSelfUpdateLoop(ctx) {
303
328
  continue;
304
329
  if (!Node.isArrayLiteralExpression(depsArg))
305
330
  continue;
306
- const deps = new Set(depsArg.getElements().map(el => el.getText()));
331
+ const deps = new Set(depsArg.getElements().map((el) => el.getText()));
307
332
  // Find setter calls in the effect body
308
333
  for (const innerCall of callbackArg.getDescendantsOfKind(SyntaxKind.CallExpression)) {
309
334
  const expr = innerCall.getExpression();
@@ -325,7 +350,263 @@ function effectSelfUpdateLoop(ctx) {
325
350
  }
326
351
  if (isNested)
327
352
  continue;
328
- findings.push(finding('effect-self-update-loop', 'error', 'bug', `useEffect updates '${stateName}' via ${setterName}() while '${stateName}' is in deps — infinite re-render loop`, ctx.filePath, innerCall.getStartLineNumber(), { suggestion: `Move the write behind a guard or use a ref to break the cycle` }));
353
+ findings.push(finding('effect-self-update-loop', 'error', 'bug', `useEffect updates '${stateName}' via ${setterName}() while '${stateName}' is in deps — infinite re-render loop`, ctx.filePath, innerCall.getStartLineNumber(), 1, { suggestion: `Move the write behind a guard or use a ref to break the cycle` }));
354
+ }
355
+ }
356
+ return findings;
357
+ }
358
+ // ── Rule: missing-effect-cleanup ─────────────────────────────────────────
359
+ // useEffect with setInterval/addEventListener but no cleanup return function
360
+ function missingEffectCleanup(ctx) {
361
+ const findings = [];
362
+ for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
363
+ const callee = call.getExpression().getText();
364
+ if (callee !== 'useEffect' && callee !== 'useLayoutEffect')
365
+ continue;
366
+ const args = call.getArguments();
367
+ if (args.length === 0)
368
+ continue;
369
+ const callback = args[0];
370
+ if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback))
371
+ continue;
372
+ const body = callback.getBody();
373
+ let hasCleanup = false;
374
+ if (Node.isBlock(body)) {
375
+ hasCleanup = body.getStatements().some((s) => {
376
+ if (!Node.isReturnStatement(s))
377
+ return false;
378
+ const expr = s.getExpression();
379
+ return (expr != null && (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr) || Node.isIdentifier(expr)));
380
+ });
381
+ }
382
+ if (hasCleanup)
383
+ continue;
384
+ const leakyCalls = callback.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => {
385
+ const name = c.getExpression().getText();
386
+ return (name === 'setInterval' || name === 'setTimeout' || name.endsWith('.addEventListener') || name.endsWith('.on'));
387
+ });
388
+ if (leakyCalls.length > 0) {
389
+ findings.push(finding('missing-effect-cleanup', 'warning', 'bug', `useEffect uses '${leakyCalls[0].getExpression().getText()}' but is missing a cleanup return function`, ctx.filePath, call.getStartLineNumber(), 1, { suggestion: 'Return a cleanup function: return () => clearInterval(id);' }));
390
+ }
391
+ }
392
+ return findings;
393
+ }
394
+ // ── Rule: inline-context-value ───────────────────────────────────────────
395
+ // <Context.Provider value={{...}}> causes re-renders on every parent render
396
+ function inlineContextValue(ctx) {
397
+ const findings = [];
398
+ for (const jsx of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)) {
399
+ const name = jsx.getTagNameNode().getText();
400
+ if (!name.endsWith('.Provider'))
401
+ continue;
402
+ for (const attr of jsx.getAttributes()) {
403
+ if (!Node.isJsxAttribute(attr) || attr.getNameNode().getText() !== 'value')
404
+ continue;
405
+ const init = attr.getInitializer();
406
+ if (!init || !Node.isJsxExpression(init))
407
+ continue;
408
+ const expr = init.getExpression();
409
+ if (!expr)
410
+ continue;
411
+ if (Node.isObjectLiteralExpression(expr) || Node.isArrayLiteralExpression(expr)) {
412
+ findings.push(finding('inline-context-value', 'warning', 'pattern', 'Inline object/array passed to Context.Provider value — causes all consumers to re-render', ctx.filePath, jsx.getStartLineNumber(), 1, { suggestion: 'Memoize the value with useMemo' }));
413
+ }
414
+ }
415
+ }
416
+ return findings;
417
+ }
418
+ // ── Rule: ref-in-render ──────────────────────────────────────────────────
419
+ // Reading or writing ref.current during render — breaks React purity rules
420
+ // Source: react.dev/reference/react/useRef, eslint-plugin-react-hooks/refs
421
+ function refInRender(ctx) {
422
+ if (!isReactFile(ctx))
423
+ return [];
424
+ const findings = [];
425
+ // Collect useRef variable names: const myRef = useRef(...)
426
+ const refVars = new Set();
427
+ for (const decl of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
428
+ const init = decl.getInitializer();
429
+ if (!init || !Node.isCallExpression(init))
430
+ continue;
431
+ const callee = init.getExpression().getText();
432
+ if (callee === 'useRef' || callee === 'React.useRef') {
433
+ refVars.add(decl.getName());
434
+ }
435
+ }
436
+ if (refVars.size === 0)
437
+ return findings;
438
+ // Identify safe scopes: useEffect/useLayoutEffect/useCallback/event handler callbacks
439
+ const SAFE_CALLEE = new Set(['useEffect', 'useLayoutEffect', 'useCallback', 'useInsertionEffect']);
440
+ function isInSafeScope(node) {
441
+ let cur = node.getParent();
442
+ while (cur) {
443
+ // Inside a useEffect/useCallback callback
444
+ if ((Node.isArrowFunction(cur) || Node.isFunctionExpression(cur)) && cur.getParent()) {
445
+ const parent = cur.getParent();
446
+ if (Node.isCallExpression(parent)) {
447
+ const calleeName = parent.getExpression().getText();
448
+ if (SAFE_CALLEE.has(calleeName))
449
+ return true;
450
+ }
451
+ }
452
+ // Inside an event handler in JSX: onClick={() => ref.current = ...}
453
+ if ((Node.isArrowFunction(cur) || Node.isFunctionExpression(cur)) && cur.getParent()) {
454
+ const parent = cur.getParent();
455
+ if (Node.isJsxExpression(parent))
456
+ return true;
457
+ }
458
+ // Inside a cleanup return function
459
+ if ((Node.isArrowFunction(cur) || Node.isFunctionExpression(cur)) && cur.getParent()) {
460
+ const parent = cur.getParent();
461
+ if (Node.isReturnStatement(parent))
462
+ return true;
463
+ }
464
+ cur = cur.getParent();
465
+ }
466
+ return false;
467
+ }
468
+ // Find .current access on ref variables
469
+ for (const prop of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
470
+ if (prop.getName() !== 'current')
471
+ continue;
472
+ const obj = prop.getExpression();
473
+ if (!Node.isIdentifier(obj))
474
+ continue;
475
+ if (!refVars.has(obj.getText()))
476
+ continue;
477
+ // Skip if inside safe scope (effect, handler, callback)
478
+ if (isInSafeScope(prop))
479
+ continue;
480
+ // Skip lazy initialization pattern: if (ref.current === null) ref.current = x
481
+ // React explicitly allows this during render (react.dev/reference/react/useRef)
482
+ const ifAncestor = prop.getFirstAncestorByKind(SyntaxKind.IfStatement);
483
+ if (ifAncestor) {
484
+ const condText = ifAncestor.getExpression().getText();
485
+ const refName = obj.getText();
486
+ if (condText.includes(`${refName}.current`) &&
487
+ (condText.includes('null') || condText.includes('undefined') || condText.startsWith('!'))) {
488
+ continue;
489
+ }
490
+ }
491
+ // Check if this is a read or write
492
+ const parent = prop.getParent();
493
+ const isWrite = parent &&
494
+ Node.isBinaryExpression(parent) &&
495
+ parent.getLeft() === prop &&
496
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken;
497
+ const action = isWrite ? 'written to' : 'read';
498
+ findings.push(finding('ref-in-render', 'error', 'bug', `ref.current ${action} during render — refs are not tracked by React and may be stale`, ctx.filePath, prop.getStartLineNumber(), 1, {
499
+ suggestion: isWrite
500
+ ? 'Move ref writes to useEffect or event handlers'
501
+ : 'Use useState instead if the value affects rendering',
502
+ }));
503
+ }
504
+ return findings;
505
+ }
506
+ // ── Rule: missing-memo-deps ──────────────────────────────────────────────
507
+ // useMemo/useCallback called without dependency array — recomputes every render
508
+ // Source: react.dev/reference/react/useMemo, react.dev/reference/react/useCallback
509
+ const MEMO_HOOKS = new Set(['useMemo', 'useCallback', 'React.useMemo', 'React.useCallback']);
510
+ function missingMemoDeps(ctx) {
511
+ const findings = [];
512
+ for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
513
+ const callee = call.getExpression().getText();
514
+ if (!MEMO_HOOKS.has(callee))
515
+ continue;
516
+ const args = call.getArguments();
517
+ if (args.length === 0)
518
+ continue;
519
+ // First arg should be the function, second should be deps array
520
+ if (args.length < 2) {
521
+ const hookName = callee.includes('.') ? callee.split('.')[1] : callee;
522
+ findings.push(finding('missing-memo-deps', 'warning', 'bug', `${hookName} called without dependency array — will recompute on every render, defeating memoization`, ctx.filePath, call.getStartLineNumber(), 1, { suggestion: `Add a dependency array as the second argument: ${hookName}(fn, [dep1, dep2])` }));
523
+ }
524
+ }
525
+ return findings;
526
+ }
527
+ // ── Rule: reducer-mutation ──────────────────────────────────────────────
528
+ // Direct state mutation inside useReducer reducer function
529
+ // Source: react.dev/reference/react/useReducer
530
+ function reducerMutation(ctx) {
531
+ const findings = [];
532
+ // Find useReducer calls and get the reducer function
533
+ for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
534
+ const callee = call.getExpression().getText();
535
+ if (callee !== 'useReducer' && callee !== 'React.useReducer')
536
+ continue;
537
+ const args = call.getArguments();
538
+ if (args.length === 0)
539
+ continue;
540
+ const reducer = args[0];
541
+ // Reducer can be inline or a reference — handle both
542
+ let reducerBody;
543
+ let stateParam;
544
+ if (Node.isArrowFunction(reducer) || Node.isFunctionExpression(reducer)) {
545
+ reducerBody = reducer.getBody();
546
+ const params = reducer.getParameters();
547
+ if (params.length > 0)
548
+ stateParam = params[0].getName();
549
+ }
550
+ else if (Node.isIdentifier(reducer)) {
551
+ const name = reducer.getText();
552
+ const fn = ctx.sourceFile.getFunction(name);
553
+ if (fn) {
554
+ reducerBody = fn.getBody();
555
+ const params = fn.getParameters();
556
+ if (params.length > 0)
557
+ stateParam = params[0].getName();
558
+ }
559
+ }
560
+ if (!reducerBody || !stateParam)
561
+ continue;
562
+ // Look for direct mutations: state.prop = ..., state.prop++, state.push(...)
563
+ const mutationMethods = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']);
564
+ for (const bin of reducerBody.getDescendantsOfKind(SyntaxKind.BinaryExpression)) {
565
+ const op = bin.getOperatorToken().getKind();
566
+ if (op !== SyntaxKind.EqualsToken && op !== SyntaxKind.PlusEqualsToken && op !== SyntaxKind.MinusEqualsToken)
567
+ continue;
568
+ const left = bin.getLeft();
569
+ if (!Node.isPropertyAccessExpression(left))
570
+ continue;
571
+ const root = left.getExpression();
572
+ if (Node.isIdentifier(root) && root.getText() === stateParam) {
573
+ findings.push(finding('reducer-mutation', 'error', 'bug', `Reducer mutates '${stateParam}.${left.getName()}' directly — return a new object instead`, ctx.filePath, bin.getStartLineNumber(), 1, { suggestion: `return { ...${stateParam}, ${left.getName()}: newValue }` }));
574
+ break; // One finding per reducer
575
+ }
576
+ }
577
+ // Check for state.method() mutations
578
+ for (const methodCall of reducerBody.getDescendantsOfKind(SyntaxKind.CallExpression)) {
579
+ const expr = methodCall.getExpression();
580
+ if (!Node.isPropertyAccessExpression(expr))
581
+ continue;
582
+ if (!mutationMethods.has(expr.getName()))
583
+ continue;
584
+ const obj = expr.getExpression();
585
+ // state.push() or state.items.push()
586
+ if (Node.isIdentifier(obj) && obj.getText() === stateParam) {
587
+ findings.push(finding('reducer-mutation', 'error', 'bug', `Reducer mutates '${stateParam}' via .${expr.getName()}() — return a new object instead`, ctx.filePath, methodCall.getStartLineNumber(), 1, { suggestion: `Return new state: return { ...${stateParam}, ... }` }));
588
+ break;
589
+ }
590
+ if (Node.isPropertyAccessExpression(obj)) {
591
+ const root = obj.getExpression();
592
+ if (Node.isIdentifier(root) && root.getText() === stateParam) {
593
+ findings.push(finding('reducer-mutation', 'error', 'bug', `Reducer mutates '${stateParam}.${obj.getName()}' via .${expr.getName()}() — use immutable update`, ctx.filePath, methodCall.getStartLineNumber(), 1, {
594
+ suggestion: `return { ...${stateParam}, ${obj.getName()}: [...${stateParam}.${obj.getName()}, newItem] }`,
595
+ }));
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ // Check for state.prop++ / ++state.prop
601
+ for (const postfix of reducerBody.getDescendantsOfKind(SyntaxKind.PostfixUnaryExpression)) {
602
+ const operand = postfix.getOperand();
603
+ if (!Node.isPropertyAccessExpression(operand))
604
+ continue;
605
+ const root = operand.getExpression();
606
+ if (Node.isIdentifier(root) && root.getText() === stateParam) {
607
+ findings.push(finding('reducer-mutation', 'error', 'bug', `Reducer mutates '${stateParam}.${operand.getName()}' via ++ — return a new object instead`, ctx.filePath, postfix.getStartLineNumber(), 1, { suggestion: `return { ...${stateParam}, ${operand.getName()}: ${stateParam}.${operand.getName()} + 1 }` }));
608
+ break;
609
+ }
329
610
  }
330
611
  }
331
612
  return findings;
@@ -339,5 +620,10 @@ export const reactRules = [
339
620
  stateExplosion,
340
621
  hookOrder,
341
622
  effectSelfUpdateLoop,
623
+ missingEffectCleanup,
624
+ inlineContextValue,
625
+ refInRender,
626
+ missingMemoDeps,
627
+ reducerMutation,
342
628
  ];
343
629
  //# sourceMappingURL=react.js.map