codesift-mcp 0.7.0 → 0.8.4
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.
- package/README.md +3 -3
- package/dist/cli/git-hooks-installer.d.ts.map +1 -1
- package/dist/cli/git-hooks-installer.js +18 -5
- package/dist/cli/git-hooks-installer.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +106 -2
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +31 -5
- package/dist/cli/setup.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -1
- package/dist/config.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +6 -1
- package/dist/instructions.js.map +1 -1
- package/dist/parser/extractors/hono.d.ts.map +1 -1
- package/dist/parser/extractors/hono.js +21 -13
- package/dist/parser/extractors/hono.js.map +1 -1
- package/dist/parser/extractors/php.d.ts +12 -0
- package/dist/parser/extractors/php.d.ts.map +1 -1
- package/dist/parser/extractors/php.js +440 -26
- package/dist/parser/extractors/php.js.map +1 -1
- package/dist/register-tool-loaders.d.ts +16 -0
- package/dist/register-tool-loaders.d.ts.map +1 -1
- package/dist/register-tool-loaders.js +26 -0
- package/dist/register-tool-loaders.js.map +1 -1
- package/dist/register-tools.d.ts +3 -1
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +354 -7
- package/dist/register-tools.js.map +1 -1
- package/dist/retrieval/codebase-retrieval.d.ts.map +1 -1
- package/dist/retrieval/codebase-retrieval.js +22 -0
- package/dist/retrieval/codebase-retrieval.js.map +1 -1
- package/dist/retrieval/retrieval-schemas.d.ts +4 -0
- package/dist/retrieval/retrieval-schemas.d.ts.map +1 -1
- package/dist/retrieval/semantic-handlers.js +1 -1
- package/dist/retrieval/semantic-handlers.js.map +1 -1
- package/dist/search/semantic.d.ts +21 -5
- package/dist/search/semantic.d.ts.map +1 -1
- package/dist/search/semantic.js +129 -4
- package/dist/search/semantic.js.map +1 -1
- package/dist/search/tool-ranker.js +1 -1
- package/dist/search/tool-ranker.js.map +1 -1
- package/dist/server-helpers.d.ts.map +1 -1
- package/dist/server-helpers.js +96 -1
- package/dist/server-helpers.js.map +1 -1
- package/dist/storage/index-store.d.ts.map +1 -1
- package/dist/storage/index-store.js +7 -5
- package/dist/storage/index-store.js.map +1 -1
- package/dist/storage/registry.d.ts +28 -4
- package/dist/storage/registry.d.ts.map +1 -1
- package/dist/storage/registry.js +126 -5
- package/dist/storage/registry.js.map +1 -1
- package/dist/storage/usage-stats.d.ts +2 -0
- package/dist/storage/usage-stats.d.ts.map +1 -1
- package/dist/storage/usage-stats.js +6 -0
- package/dist/storage/usage-stats.js.map +1 -1
- package/dist/storage/usage-tracker.js +1 -1
- package/dist/storage/usage-tracker.js.map +1 -1
- package/dist/tools/_helpers.d.ts.map +1 -1
- package/dist/tools/_helpers.js +14 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/conversation-tools.js +1 -1
- package/dist/tools/conversation-tools.js.map +1 -1
- package/dist/tools/index-tools.d.ts +12 -0
- package/dist/tools/index-tools.d.ts.map +1 -1
- package/dist/tools/index-tools.js +52 -5
- package/dist/tools/index-tools.js.map +1 -1
- package/dist/tools/insights-tools.d.ts +137 -0
- package/dist/tools/insights-tools.d.ts.map +1 -0
- package/dist/tools/insights-tools.js +438 -0
- package/dist/tools/insights-tools.js.map +1 -0
- package/dist/tools/pattern-tools.d.ts +7 -0
- package/dist/tools/pattern-tools.d.ts.map +1 -1
- package/dist/tools/pattern-tools.js +287 -15
- package/dist/tools/pattern-tools.js.map +1 -1
- package/dist/tools/php-tools.d.ts +78 -4
- package/dist/tools/php-tools.d.ts.map +1 -1
- package/dist/tools/php-tools.js +824 -42
- package/dist/tools/php-tools.js.map +1 -1
- package/dist/tools/php8-compat-tools.d.ts +62 -0
- package/dist/tools/php8-compat-tools.d.ts.map +1 -0
- package/dist/tools/php8-compat-tools.js +287 -0
- package/dist/tools/php8-compat-tools.js.map +1 -0
- package/dist/tools/php8-migration-candidates-tools.d.ts +68 -0
- package/dist/tools/php8-migration-candidates-tools.d.ts.map +1 -0
- package/dist/tools/php8-migration-candidates-tools.js +476 -0
- package/dist/tools/php8-migration-candidates-tools.js.map +1 -0
- package/dist/tools/phpstan-baseline-tools.d.ts +62 -0
- package/dist/tools/phpstan-baseline-tools.d.ts.map +1 -0
- package/dist/tools/phpstan-baseline-tools.js +263 -0
- package/dist/tools/phpstan-baseline-tools.js.map +1 -0
- package/dist/tools/project-tools.d.ts +4 -2
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +19 -6
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/react-tools.d.ts +24 -0
- package/dist/tools/react-tools.d.ts.map +1 -1
- package/dist/tools/react-tools.js +292 -3
- package/dist/tools/react-tools.js.map +1 -1
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +92 -10
- package/dist/tools/search-tools.js.map +1 -1
- package/dist/tools/symbol-tools.d.ts.map +1 -1
- package/dist/tools/symbol-tools.js +4 -1
- package/dist/tools/symbol-tools.js.map +1 -1
- package/dist/tools/yii-console-tools.d.ts +69 -0
- package/dist/tools/yii-console-tools.d.ts.map +1 -0
- package/dist/tools/yii-console-tools.js +256 -0
- package/dist/tools/yii-console-tools.js.map +1 -0
- package/dist/tools/yii-migrations-tools.d.ts +79 -0
- package/dist/tools/yii-migrations-tools.d.ts.map +1 -0
- package/dist/tools/yii-migrations-tools.js +543 -0
- package/dist/tools/yii-migrations-tools.js.map +1 -0
- package/dist/tools/yii-modules-tools.d.ts +63 -0
- package/dist/tools/yii-modules-tools.d.ts.map +1 -0
- package/dist/tools/yii-modules-tools.js +201 -0
- package/dist/tools/yii-modules-tools.js.map +1 -0
- package/dist/tools/yii-rbac-tools.d.ts +89 -0
- package/dist/tools/yii-rbac-tools.d.ts.map +1 -0
- package/dist/tools/yii-rbac-tools.js +238 -0
- package/dist/tools/yii-rbac-tools.js.map +1 -0
- package/dist/tools/yii3-attribute-candidates-tools.d.ts +72 -0
- package/dist/tools/yii3-attribute-candidates-tools.d.ts.map +1 -0
- package/dist/tools/yii3-attribute-candidates-tools.js +301 -0
- package/dist/tools/yii3-attribute-candidates-tools.js.map +1 -0
- package/dist/tools/yii3-migration-tools.d.ts +74 -0
- package/dist/tools/yii3-migration-tools.d.ts.map +1 -0
- package/dist/tools/yii3-migration-tools.js +440 -0
- package/dist/tools/yii3-migration-tools.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/constant-file-pattern.d.ts +3 -1
- package/dist/utils/constant-file-pattern.d.ts.map +1 -1
- package/dist/utils/constant-file-pattern.js +6 -4
- package/dist/utils/constant-file-pattern.js.map +1 -1
- package/dist/utils/heritage-edges.d.ts +16 -0
- package/dist/utils/heritage-edges.d.ts.map +1 -1
- package/dist/utils/heritage-edges.js +31 -10
- package/dist/utils/heritage-edges.js.map +1 -1
- package/dist/utils/source-stripper.d.ts +23 -0
- package/dist/utils/source-stripper.d.ts.map +1 -0
- package/dist/utils/source-stripper.js +239 -0
- package/dist/utils/source-stripper.js.map +1 -0
- package/dist/utils/tsconfig-paths.d.ts +2 -2
- package/dist/utils/tsconfig-paths.d.ts.map +1 -1
- package/dist/utils/tsconfig-paths.js +10 -4
- package/dist/utils/tsconfig-paths.js.map +1 -1
- package/dist/utils/wall-clock.d.ts +9 -0
- package/dist/utils/wall-clock.d.ts.map +1 -0
- package/dist/utils/wall-clock.js +19 -0
- package/dist/utils/wall-clock.js.map +1 -0
- package/package.json +1 -1
- package/rules/codesift.md +10 -3
- package/rules/codesift.mdc +10 -3
- package/rules/codex.md +10 -3
- package/rules/gemini.md +10 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getCodeIndex } from "./index-tools.js";
|
|
4
|
+
import { stripCommentsAndStrings } from "../utils/source-stripper.js";
|
|
4
5
|
import { isTestFileStrict as isTestFile } from "../utils/test-file.js";
|
|
5
6
|
// Built-in patterns inspired by CQ checklist + common React/TS anti-patterns
|
|
6
7
|
// Exported for direct regex testing in unit tests.
|
|
@@ -8,128 +9,164 @@ export const BUILTIN_PATTERNS = {
|
|
|
8
9
|
"useEffect-no-cleanup": {
|
|
9
10
|
regex: /useEffect\s*\(\s*(?:async\s*)?\(\)\s*=>\s*\{(?:(?!return\s*\(\s*\)\s*=>|return\s+\(\)\s*=>|return\s*\(\s*\)\s*\{|return\s+function)[\s\S])*\}\s*,/,
|
|
10
11
|
description: "useEffect without cleanup return — potential memory leak (CQ22)",
|
|
12
|
+
severity: "warning",
|
|
11
13
|
},
|
|
12
14
|
// --- React anti-patterns (Wave 2) ---
|
|
13
15
|
"hook-in-condition": {
|
|
14
16
|
regex: /\b(?:if|for|while|switch)\s*\([^)]*\)\s*\{[\s\S]{0,500}?\buse[A-Z]\w*\s*\(/,
|
|
15
17
|
description: "React hook called inside if/for/while/switch — violates Rule of Hooks",
|
|
18
|
+
severity: "critical",
|
|
16
19
|
},
|
|
17
20
|
"useEffect-async": {
|
|
18
21
|
regex: /useEffect\s*\(\s*async\s+(?:function\b|\(|[a-z_$])/,
|
|
19
22
|
description: "async function directly in useEffect — use inner async wrapper (CQ22)",
|
|
23
|
+
severity: "warning",
|
|
20
24
|
},
|
|
21
25
|
"useEffect-object-dep": {
|
|
22
26
|
regex: /useEffect\s*\([\s\S]*?,\s*\[[^\]]*[{[]/,
|
|
23
27
|
description: "Object/array literal in useEffect dependency array — causes infinite re-renders",
|
|
28
|
+
severity: "warning",
|
|
24
29
|
},
|
|
25
30
|
"missing-display-name": {
|
|
26
31
|
regex: /(?:React\.)?(?:memo|forwardRef)\s*\((?:(?!displayName)[\s\S]){0,500}$/,
|
|
27
32
|
description: "React.memo/forwardRef without displayName nearby — harder to debug in DevTools",
|
|
33
|
+
severity: "style",
|
|
28
34
|
},
|
|
29
35
|
"index-as-key": {
|
|
30
36
|
regex: /\.map\s*\(\s*\(\s*\w+\s*,\s*(index|idx|i)\b[^)]*\)\s*=>[\s\S]{0,400}?key\s*=\s*\{?\s*\1\b/,
|
|
31
37
|
description: "Array index used as React key — causes incorrect reconciliation on reorder",
|
|
38
|
+
severity: "warning",
|
|
32
39
|
},
|
|
33
40
|
"inline-handler": {
|
|
34
41
|
regex: /\bon[A-Z]\w*\s*=\s*\{\s*(?:\([^)]*\)|[a-z_$][\w$]*)\s*=>/,
|
|
35
42
|
description: "Inline arrow function in JSX event handler — creates new reference every render (memoization killer)",
|
|
43
|
+
severity: "style",
|
|
36
44
|
},
|
|
37
45
|
"conditional-render-hook": {
|
|
38
46
|
regex: /\breturn\s+[^;{]*;\s*\n[\s\S]*?\buse[A-Z]\w*\s*\(/,
|
|
39
47
|
description: "React hook called after early return — violates Rule of Hooks",
|
|
48
|
+
severity: "critical",
|
|
40
49
|
},
|
|
41
50
|
// --- React anti-patterns (Wave 4b — additional) ---
|
|
42
51
|
"dangerously-set-html": {
|
|
43
52
|
regex: /dangerouslySetInnerHTML\s*=\s*\{/,
|
|
44
|
-
description: "dangerouslySetInnerHTML used — XSS risk unless content is sanitized (CQ24)",
|
|
53
|
+
description: "dangerouslySetInnerHTML used — XSS risk unless content is sanitized (CQ24). Comment/string-embedded mentions are stripped before matching.",
|
|
54
|
+
severity: "critical",
|
|
55
|
+
preprocess: "strip-comments-strings",
|
|
45
56
|
},
|
|
46
57
|
"direct-dom-access": {
|
|
47
58
|
regex: /\bdocument\.(getElementById|querySelector|querySelectorAll|getElementsBy)\s*\(/,
|
|
48
|
-
description: "Direct DOM access in React component — use useRef instead (breaks SSR, bypasses virtual DOM)",
|
|
59
|
+
description: "Direct DOM access in React component — use useRef instead (breaks SSR, bypasses virtual DOM). Comment/string-embedded mentions stripped before matching.",
|
|
60
|
+
severity: "warning",
|
|
61
|
+
preprocess: "strip-comments-strings",
|
|
49
62
|
},
|
|
50
63
|
"unstable-default-value": {
|
|
51
64
|
regex: /(?:function\s+[A-Z]\w*|const\s+[A-Z]\w*\s*=\s*(?:\([^)]*\)|[^=]*)\s*=>)\s*[\s\S]{0,100}(?:\{\s*[^}]*=\s*\[\s*\]|\{\s*[^}]*=\s*\{\s*\})/,
|
|
52
65
|
description: "Default prop value [] or {} in component params — creates new reference every render, breaks memo/PureComponent",
|
|
66
|
+
severity: "warning",
|
|
53
67
|
},
|
|
54
68
|
"jsx-falsy-and": {
|
|
55
69
|
regex: /\{\s*(?:count|length|size|num|total|amount)\s*&&\s*</,
|
|
56
70
|
description: "Numeric variable used with && in JSX — renders '0' on screen when falsy. Use ternary or Boolean() (React gotcha)",
|
|
71
|
+
severity: "warning",
|
|
57
72
|
},
|
|
58
73
|
"nested-component-def": {
|
|
59
74
|
regex: /(?:function\s+[A-Z]\w*\s*\([^)]*\)\s*\{|const\s+[A-Z]\w*\s*=\s*(?:\([^)]*\)|[\w$]*)\s*=>\s*\{)[\s\S]{0,1500}?\n\s{2,}(?:function\s+[A-Z]\w*\s*\(|const\s+[A-Z]\w*\s*=\s*\()/,
|
|
60
75
|
description: "Component defined inside another component — remounts on every parent render, loses all state. Hoist to module level.",
|
|
76
|
+
severity: "critical",
|
|
61
77
|
},
|
|
62
78
|
"usecallback-no-deps": {
|
|
63
79
|
regex: /use(?:Callback|Memo)\s*\([\s\S]*?\)\s*\)\s*[;,]/,
|
|
64
80
|
description: "useCallback/useMemo with only one argument (no dependency array) — useless memoization, value recreated every render",
|
|
81
|
+
severity: "warning",
|
|
65
82
|
},
|
|
66
83
|
// --- React 19 features (Tier 4 — Item 19) ---
|
|
67
84
|
"react19-use-without-suspense": {
|
|
68
85
|
regex: /\buse\s*\(\s*[a-zA-Z_$][\w$]*\s*\)/,
|
|
69
86
|
description: "React 19 use(promise) — must be wrapped in <Suspense> or it throws. Verify Suspense boundary exists in parent.",
|
|
70
87
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
88
|
+
severity: "critical",
|
|
71
89
|
},
|
|
72
90
|
"react19-server-action-not-async": {
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
// Tier 7 fix: matches `export function X`, `export const X = (...) =>`,
|
|
92
|
+
// AND `export default function X` (gemini finding — default exports were missed).
|
|
93
|
+
// 2000-char window for actions defined far from the directive.
|
|
94
|
+
regex: /^[\s\S]{0,200}["']use server["'][\s\S]{0,2000}?\bexport\s+(?:(?:const|let|var)\s+\w+\s*=\s+(?!async\b)(?:\([^)]*\)|\w+)\s*=>|default\s+(?!async\b)function(?:\s+\w+)?\s*\(|(?!async\b)function\s+\w+\s*\()/m,
|
|
95
|
+
description: "React 19 Server Action: function in 'use server' file must be async (returns Promise). Pattern detects `export function X`, `export const X = (...) =>` arrow, AND `export default function X` (default exports).",
|
|
75
96
|
fileIncludePattern: /\.(tsx|jsx|ts|js)$/,
|
|
97
|
+
severity: "critical",
|
|
76
98
|
},
|
|
77
99
|
"react19-form-action-non-function": {
|
|
78
100
|
regex: /<form\s+[^>]*\baction\s*=\s*["'][^"']/,
|
|
79
101
|
description: "React 19 form action prop should be a function (Server Action), not a string URL. Use <form action={serverAction}> for progressive enhancement.",
|
|
80
102
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
103
|
+
severity: "warning",
|
|
81
104
|
},
|
|
82
105
|
"react19-useoptimistic-no-transition": {
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
// Tier 7 fix: \b boundary — myUseTransitionWrapper no longer suppresses match.
|
|
107
|
+
// Tier 8: preprocess strips comment/string content before lookahead — closes
|
|
108
|
+
// Tier 7 R-2.1 known limit (transition tokens in JSDoc/comments).
|
|
109
|
+
regex: /\buseOptimistic\s*\((?![\s\S]{0,1000}?\b(?:useTransition|startTransition)\b)/,
|
|
110
|
+
description: "React 19 useOptimistic should be paired with useTransition/startTransition for non-urgent updates. Comment/string-embedded mentions stripped before matching.",
|
|
85
111
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
112
|
+
severity: "warning",
|
|
113
|
+
preprocess: "strip-comments-strings",
|
|
86
114
|
},
|
|
87
115
|
// --- oxlint-inspired React rules (April 2026) ---
|
|
88
116
|
"hook-usestate-destructure": {
|
|
89
117
|
regex: /(?:^|\n)\s*useState\s*(?:<[^>]+>)?\s*\([^)]*\)\s*;/,
|
|
90
118
|
description: "useState() called without destructuring [value, setter] — value is inaccessible. Use: const [value, setValue] = useState(initial). (oxlint react/hook-use-state)",
|
|
91
119
|
fileIncludePattern: /\.(tsx|jsx|ts)$/,
|
|
120
|
+
severity: "critical",
|
|
92
121
|
},
|
|
93
122
|
"prefer-function-component": {
|
|
94
123
|
regex: /class\s+\w+\s+extends\s+(?:React\.)?(?:Component|PureComponent)\b/,
|
|
95
124
|
description: "Class component could be a function component — class components lack hook support, are harder to tree-shake, and React Compiler cannot optimize them. (oxlint react/prefer-function-component)",
|
|
96
125
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
126
|
+
severity: "style",
|
|
97
127
|
},
|
|
98
128
|
// --- React Compiler bailout patterns (GA v1.0, Oct 2025 — Next.js 16 stable) ---
|
|
99
129
|
"compiler-side-effect-in-render": {
|
|
100
130
|
regex: /\b(?:console\.(?:log|warn|error|info)\s*\(|Math\.random\s*\(|Date\.now\s*\(|document\.(?:getElementById|querySelector|createElement)\s*\()/,
|
|
101
131
|
description: "Side effect in render body — React Compiler silently skips memoization. Move to useEffect or event handler.",
|
|
102
132
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
133
|
+
severity: "warning",
|
|
103
134
|
},
|
|
104
135
|
"compiler-ref-read-in-render": {
|
|
105
136
|
regex: /(?:^|\n)\s*(?:const|let|var)\s+\w+\s*=\s*\w+Ref\.current\b/,
|
|
106
137
|
description: "Reading ref.current during render — React Compiler cannot track ref mutations. Read refs in useEffect or event handlers only.",
|
|
107
138
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
139
|
+
severity: "warning",
|
|
108
140
|
},
|
|
109
141
|
"compiler-prop-mutation": {
|
|
110
142
|
regex: /\bprops\.\w+\.(?:push|pop|shift|unshift|splice|sort|reverse|fill)\s*\(/,
|
|
111
143
|
description: "Mutating props object — breaks React Compiler immutability assumption. Clone before mutating: [...props.items, newItem].",
|
|
112
144
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
145
|
+
severity: "warning",
|
|
113
146
|
},
|
|
114
147
|
"compiler-state-mutation": {
|
|
115
148
|
regex: /(?:^|\n)\s*\w+\.(?:push|pop|shift|unshift|splice|sort|reverse|fill)\s*\([\s\S]{0,200}?set[A-Z]\w*\s*\(\s*\w+\s*\)/,
|
|
116
149
|
description: "Direct state mutation then setState with same reference — React Compiler assumes immutable updates. Use spread: setItems([...items, newItem]).",
|
|
117
150
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
151
|
+
severity: "warning",
|
|
118
152
|
},
|
|
119
153
|
"compiler-try-catch-bailout": {
|
|
120
154
|
regex: /(?:function\s+[A-Z]\w*|const\s+[A-Z]\w*\s*=)[\s\S]{0,300}?\btry\s*\{[\s\S]{0,500}?\bcatch\s*\(/,
|
|
121
155
|
description: "try/catch in component body — React Compiler may silently bail out (known issue #35644). Move error handling to useEffect or extract to a hook.",
|
|
122
156
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
157
|
+
severity: "warning",
|
|
123
158
|
},
|
|
124
159
|
"compiler-redundant-memo": {
|
|
125
160
|
regex: /\b(?:React\.)?memo\s*\(\s*(?:function\s+[A-Z]|(?:\([^)]*\)|[A-Z]\w*)\s*=>)/,
|
|
126
161
|
description: "React.memo wrapping — React Compiler auto-memoizes, making manual memo redundant. Safe to remove after compiler adoption.",
|
|
127
162
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
163
|
+
severity: "style",
|
|
128
164
|
},
|
|
129
165
|
"compiler-redundant-usecallback": {
|
|
130
166
|
regex: /\buseCallback\s*\(\s*(?:\([^)]*\)|[a-z_$][\w$]*)\s*=>/,
|
|
131
167
|
description: "useCallback wrapping — React Compiler auto-memoizes callbacks, making manual useCallback redundant. Safe to remove after compiler adoption.",
|
|
132
168
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
169
|
+
severity: "style",
|
|
133
170
|
},
|
|
134
171
|
// --- RSC boundary serializability (Tier 4 — Item 18) ---
|
|
135
172
|
"rsc-non-serializable-prop": {
|
|
@@ -139,22 +176,35 @@ export const BUILTIN_PATTERNS = {
|
|
|
139
176
|
regex: /\b(?:onClick|onChange|onSubmit|onError|callback|handler|render)\s*=\s*\{\s*[a-z_$][\w$]*\s*\}/,
|
|
140
177
|
description: "Function passed as prop across RSC boundary — must be a Server Action ('use server') or component must be Client Component ('use client'). Functions are not serializable.",
|
|
141
178
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
179
|
+
severity: "critical",
|
|
142
180
|
},
|
|
143
181
|
"rsc-date-prop": {
|
|
144
182
|
regex: /\b\w+\s*=\s*\{\s*new\s+Date\s*\(/,
|
|
145
183
|
description: "Date object passed as prop — Date is serializable in JSON but loses prototype across RSC boundary. Use ISO string + parse on client side.",
|
|
146
184
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
185
|
+
severity: "warning",
|
|
147
186
|
},
|
|
148
187
|
// --- useEffect pain points (37% of devs struggle — State of React 2025) ---
|
|
149
188
|
"useEffect-missing-cleanup": {
|
|
150
189
|
regex: /useEffect\s*\(\s*(?:\([^)]*\)|[a-z_$][\w$]*)\s*=>[\s\S]{0,800}?(?:addEventListener|setInterval|setTimeout|subscribe|on\s*\()(?:(?!return\s*(?:\(\s*\)\s*=>|function))[\s\S]){0,800}\}\s*,/,
|
|
151
190
|
description: "useEffect with addEventListener/setInterval/subscribe but no cleanup return — memory leak. Return a cleanup function that removes the listener/clears the interval.",
|
|
152
191
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
192
|
+
severity: "warning",
|
|
153
193
|
},
|
|
154
194
|
"useEffect-setstate-loop": {
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
// Tier 7 fix (multiple gemini findings):
|
|
196
|
+
// 1. Original matched `[` in setState array literal arg → anchored on `}, [`.
|
|
197
|
+
// 2. Cross-effect bridging — non-greedy walk now bails on next `useEffect`.
|
|
198
|
+
// 3. Implicit-return arrows: alternation block-bodied OR concise.
|
|
199
|
+
// 4. `\1\b` matched `count` inside `props.count` — fixed by `(?<!\.)\b\1\b`.
|
|
200
|
+
// 5. Tier 7 review R-3: concise arm's first `\)` could close a NESTED call like
|
|
201
|
+
// `setCount(getY())`. Fix: require concise-arm setState arg has NO inner `(`
|
|
202
|
+
// by using `[^()]*` for the simple case (most real bugs); complex args fall
|
|
203
|
+
// through to the block-bodied arm. Documented as known limit.
|
|
204
|
+
regex: /useEffect\s*\(\s*(?:\([^)]*\)|[a-z_$][\w$]*)\s*=>\s*(?:\{(?:(?!\buseEffect\b)[\s\S]){0,800}?\bset([A-Z]\w*)\s*\((?:(?!\buseEffect\b)[\s\S]){0,300}?\}\s*,\s*\[[^\]]*?(?<!\.)\b\1\b|set([A-Z]\w*)\s*\([^()]{0,200}\)\s*,\s*\[[^\]]*?(?<!\.)\b\2\b)/i,
|
|
205
|
+
description: "setState inside useEffect with same state variable in dependency array — infinite render loop. Block-bodied form `() => { setX(); }, [x]` AND concise form `() => setX(arg), [x]` (concise arm requires non-nested arg). Bails out at next useEffect; rejects `props.count` property chains. Known limit: concise form with nested calls (setX(getY())) is not detected — block form covers it.",
|
|
157
206
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
207
|
+
severity: "critical",
|
|
158
208
|
},
|
|
159
209
|
"useEffect-missing-deps-identifier": {
|
|
160
210
|
// Heuristic: useEffect with empty dep array [] but body references an
|
|
@@ -164,23 +214,27 @@ export const BUILTIN_PATTERNS = {
|
|
|
164
214
|
regex: /useEffect\s*\(\s*\([^)]*\)\s*=>\s*\{[\s\S]{0,400}?\b(?:props\.\w+|[a-z][a-zA-Z]*(?:\.\w+)?)[\s\S]{0,400}?\}\s*,\s*\[\s*\]\s*\)/,
|
|
165
215
|
description: "useEffect with empty deps array [] reads props/state identifiers in body — likely missing dependencies. If intentional, add // eslint-disable-next-line react-hooks/exhaustive-deps with reason.",
|
|
166
216
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
217
|
+
severity: "warning",
|
|
167
218
|
},
|
|
168
219
|
// --- Next.js 16 cache patterns ---
|
|
169
220
|
"nextjs-use-cache-without-tag": {
|
|
170
221
|
regex: /['"]use cache['"](?:(?!cacheTag\s*\()[\s\S]){0,1000}$/,
|
|
171
222
|
description: "Next.js 16 'use cache' directive without cacheTag() call — cache entry is hard to invalidate. Add cacheTag('name') for targeted revalidation.",
|
|
172
223
|
fileIncludePattern: /\.(tsx|jsx|ts)$/,
|
|
224
|
+
severity: "warning",
|
|
173
225
|
},
|
|
174
226
|
"nextjs-revalidatetag-deprecated": {
|
|
175
227
|
regex: /\brevalidateTag\s*\(\s*['"][^'"]+['"]\s*\)/,
|
|
176
228
|
description: "Next.js 16: revalidateTag() without cacheLife profile (second argument). Single-arg form deprecated — add cacheLife profile.",
|
|
177
229
|
fileIncludePattern: /\.(tsx|jsx|ts)$/,
|
|
230
|
+
severity: "warning",
|
|
178
231
|
},
|
|
179
232
|
// --- TanStack Query patterns ---
|
|
180
233
|
"tanstack-missing-invalidation": {
|
|
181
234
|
regex: /\buseMutation\s*\((?:(?!invalidateQueries|invalidateQuery)[\s\S]){0,800}\}\s*\)/,
|
|
182
235
|
description: "useMutation without invalidateQueries in onSuccess/onSettled — stale data remains in cache after mutation. Add queryClient.invalidateQueries() on success.",
|
|
183
236
|
fileIncludePattern: /\.(tsx|jsx|ts)$/,
|
|
237
|
+
severity: "warning",
|
|
184
238
|
},
|
|
185
239
|
// --- React Tier 5 (May 2026) — derived state, stale closures, context perf, security ---
|
|
186
240
|
"derived-state": {
|
|
@@ -228,17 +282,97 @@ export const BUILTIN_PATTERNS = {
|
|
|
228
282
|
severity: "style",
|
|
229
283
|
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
230
284
|
},
|
|
285
|
+
// --- React Tier 6 (May 2026) — extending Tier 5 coverage ---
|
|
286
|
+
"derived-state-reducer": {
|
|
287
|
+
regex: /useReducer\s*\([\s\S]{0,500}?\)[\s\S]{0,2000}?useEffect\s*\([\s\S]{0,500}?dispatch\s*\(\s*\{\s*type\s*:\s*['"][a-zA-Z_-]*sync/i,
|
|
288
|
+
description: "useReducer + useEffect dispatching a sync-typed action — derived state via reducer.",
|
|
289
|
+
severity: "warning",
|
|
290
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
291
|
+
},
|
|
292
|
+
"derived-state-custom-setter": {
|
|
293
|
+
regex: /useState\s*\(\s*props\.(\w+)\s*\)[\s\S]{0,2000}?useEffect\s*\([\s\S]{0,500}?set[A-Z]\w*\s*\(\s*props\.\1\s*\)/,
|
|
294
|
+
description: "useState(props.X) + useEffect with custom-named setter syncing props.X — derived state anti-pattern (custom setter naming variant).",
|
|
295
|
+
severity: "warning",
|
|
296
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
297
|
+
},
|
|
298
|
+
"stale-closure-toggle": {
|
|
299
|
+
regex: /const\s*\[\s*(\w+)\s*,\s*set([A-Z]\w*)\s*\]\s*=\s*useState[\s\S]{0,3000}?\bset\2\s*\(\s*!\s*\1\s*\)/,
|
|
300
|
+
description: "setX(!X) boolean toggle — risks stale closure. Use functional form: setX(prev => !prev).",
|
|
301
|
+
severity: "warning",
|
|
302
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
303
|
+
},
|
|
304
|
+
"stale-closure-broken-functional": {
|
|
305
|
+
// codex Run findings: regex must require updater param NAME ≠ state var name
|
|
306
|
+
// (e.g. `setCount(count => count + 1)` is correct shadowing, not the bug).
|
|
307
|
+
// Three-group form: \1 = state var, \3 = updater param. Match only when \3 != \1
|
|
308
|
+
// by requiring \1 ref AFTER updater param that's a distinct identifier.
|
|
309
|
+
regex: /const\s*\[\s*(\w+)\s*,\s*set([A-Z]\w*)\s*\]\s*=\s*useState[\s\S]{0,3000}?\bset\2\s*\(\s*(?!(?:\1\b))(\w+)\s*=>\s*[\s\S]{0,200}?\b\1\b/,
|
|
310
|
+
description: "Functional updater that references the outer state var instead of the prev parameter (e.g., setCount(prev => count + 1)) — still stale-closure-prone. NOTE: correctly skips intentional shadowing (setCount(count => count + 1)).",
|
|
311
|
+
severity: "warning",
|
|
312
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
313
|
+
},
|
|
314
|
+
"context-provider-value-via-variable": {
|
|
315
|
+
// gemini findings: original lookbehind was syntactically broken; missed arrays.
|
|
316
|
+
// Fix: drop lookbehind (negation handled via word `useMemo` exclusion in identifier
|
|
317
|
+
// value-source), accept both {object} and [array] literal sources.
|
|
318
|
+
regex: /\b(?:const|let|var)\s+(\w+)\s*=\s*(?!useMemo\b)[{\[][\s\S]{0,500}?[}\]]\s*;[\s\S]{0,500}?<\w+\.Provider\s+[^>]*\bvalue\s*=\s*\{\s*\1\s*\}/,
|
|
319
|
+
description: "Context.Provider value passed via local variable assigned to inline object/array literal — new reference every render. Wrap in useMemo: const ctx = useMemo(() => ({...}), [deps]). Detects both {} and [] forms; correctly skips useMemo-wrapped values.",
|
|
320
|
+
severity: "warning",
|
|
321
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
322
|
+
},
|
|
323
|
+
"context-provider-value-inline-destructured": {
|
|
324
|
+
regex: /const\s*\{\s*([A-Z]\w*Provider\w*|Provider)\s*\}\s*=\s*\w+[\s\S]{0,2000}?<\1\s+[^>]*\bvalue\s*=\s*\{\s*[\{\[]/,
|
|
325
|
+
description: "Destructured Provider with inline object/array literal value — same perf problem as <Ctx.Provider value={{...}}>. Wrap in useMemo.",
|
|
326
|
+
severity: "warning",
|
|
327
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
328
|
+
},
|
|
329
|
+
"react-lazy-no-suspense-same-file": {
|
|
330
|
+
// Tier 6 Item 1 — single-file approximation. Cross-file detection (Suspense in router parent)
|
|
331
|
+
// requires interprocedural analysis — deferred to Tier 7.
|
|
332
|
+
// Codex/gemini findings:
|
|
333
|
+
// - <React.Suspense> form must be matched (was bypassable with `import * as React`)
|
|
334
|
+
// - Suspense placed BEFORE lazy() in file was missed (forward-only lookahead)
|
|
335
|
+
// Fix: from-start-of-file negation `((?!<(?:React\.)?Suspense\b)[\s\S])*` + same trailing.
|
|
336
|
+
regex: /^((?!<(?:React\.)?Suspense\b)[\s\S])*(?:const|let|var)\s+[A-Z]\w*\s*=\s*(?:React\.)?lazy\s*\(((?!<(?:React\.)?Suspense\b)[\s\S]){0,3000}?export\s+default/,
|
|
337
|
+
description: "React.lazy() in entrypoint file (has `export default`) without any <Suspense> or <React.Suspense> anywhere in the same file — likely missing Suspense boundary. NOTE: heuristic only — Suspense in router parent file is a known false-positive case (cross-file detection deferred to Tier 7).",
|
|
338
|
+
severity: "style",
|
|
339
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
340
|
+
},
|
|
341
|
+
"error-boundary-incomplete": {
|
|
342
|
+
// Tier 6 Item 14 — ErrorBoundary coverage (partial). True coverage analysis
|
|
343
|
+
// (which routes wrapped) requires cross-file scope — Tier 7. This pattern detects
|
|
344
|
+
// class components that DEFINE one of the two ErrorBoundary lifecycle methods but
|
|
345
|
+
// not the other, indicating incomplete error handling.
|
|
346
|
+
// Match strategy: class with componentDidCatch but no getDerivedStateFromError in same body
|
|
347
|
+
// (or vice versa) is an incomplete ErrorBoundary.
|
|
348
|
+
regex: /class\s+\w+\s+extends\s+(?:React\.)?(?:Component|PureComponent)\b[\s\S]{0,3000}?(?:componentDidCatch\s*\([\s\S]{0,2000}?\}(?![\s\S]{0,2000}?getDerivedStateFromError)|getDerivedStateFromError\s*\([\s\S]{0,2000}?\}(?![\s\S]{0,2000}?componentDidCatch))/,
|
|
349
|
+
description: "ErrorBoundary class component has componentDidCatch but not getDerivedStateFromError (or vice versa). React requires BOTH lifecycle methods for a complete ErrorBoundary: getDerivedStateFromError to render fallback UI, componentDidCatch to log the error.",
|
|
350
|
+
severity: "warning",
|
|
351
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
352
|
+
},
|
|
353
|
+
"rsc-non-serializable-prop-deep": {
|
|
354
|
+
// Tier 6 Item 11 — deep RSC serializability. Detects common non-serializable types
|
|
355
|
+
// passed across RSC boundary: Map, Set, Class instances (PascalCase constructor),
|
|
356
|
+
// Symbol(). Complements rsc-non-serializable-prop which catches function refs only.
|
|
357
|
+
regex: /\b(?:onClick|onChange|onSubmit|onError|callback|handler|render|data|value|state)\s*=\s*\{\s*new\s+(?:Map|Set|WeakMap|WeakSet|Symbol|RegExp|Promise|[A-Z]\w*)\s*\(/,
|
|
358
|
+
description: "Non-serializable type passed as prop across RSC boundary — Map/Set/Class instance/Symbol/RegExp/Promise are NOT JSON-serializable. Convert to plain object/array on server, reconstruct on client.",
|
|
359
|
+
severity: "critical",
|
|
360
|
+
fileIncludePattern: /\.(tsx|jsx)$/,
|
|
361
|
+
},
|
|
231
362
|
"empty-catch": {
|
|
232
363
|
regex: /catch\s*\([^)]*\)\s*\{\s*\}/,
|
|
233
|
-
description: "Empty catch block — swallowed error (CQ8)",
|
|
364
|
+
description: "Empty catch block — swallowed error (CQ8). Comment/string-embedded mentions stripped before matching.",
|
|
365
|
+
preprocess: "strip-comments-strings",
|
|
234
366
|
},
|
|
235
367
|
"any-type": {
|
|
236
368
|
regex: /:\s*any\b|as\s+any\b/,
|
|
237
|
-
description: "Usage of 'any' type — lose type safety",
|
|
369
|
+
description: "Usage of 'any' type — lose type safety. Comment/string-embedded mentions stripped before matching.",
|
|
370
|
+
preprocess: "strip-comments-strings",
|
|
238
371
|
},
|
|
239
372
|
"console-log": {
|
|
240
373
|
regex: /console\.(log|debug|info)\s*\(/,
|
|
241
|
-
description: "console.log in production code — use structured logger (CQ13)",
|
|
374
|
+
description: "console.log in production code — use structured logger (CQ13). Comment/string-embedded mentions stripped before matching.",
|
|
375
|
+
preprocess: "strip-comments-strings",
|
|
242
376
|
},
|
|
243
377
|
"await-in-loop": {
|
|
244
378
|
regex: /for\s*\([\s\S]*?\)\s*\{[\s\S]*?await\s/,
|
|
@@ -340,6 +474,128 @@ export const BUILTIN_PATTERNS = {
|
|
|
340
474
|
regex: /createCommand\s*\(\s*["'][^"']*\$\{?\w+/,
|
|
341
475
|
description: "Yii2 createCommand with string interpolation — SQL injection risk",
|
|
342
476
|
},
|
|
477
|
+
// --- Yii2 / PHP additional security & quality patterns (Sprint 2) ---
|
|
478
|
+
"yii-csrf-disabled": {
|
|
479
|
+
// Property assignment OR rule override that disables CSRF on a controller.
|
|
480
|
+
// Common false positive: test-only configs intentionally disable CSRF.
|
|
481
|
+
// We exclude config/test*.php at the search-pattern level via fileIncludePattern.
|
|
482
|
+
regex: /\benableCsrfValidation\s*=\s*false\b/,
|
|
483
|
+
description: "CSRF validation explicitly disabled on a controller — accepts forged requests for state-changing actions (Yii2)",
|
|
484
|
+
fileIncludePattern: /\.php$/,
|
|
485
|
+
},
|
|
486
|
+
"yii-debug-mode-prod": {
|
|
487
|
+
// Hard-coded `define('YII_DEBUG', true)` in web/index.php is a deploy
|
|
488
|
+
// disaster — full stack traces leak into production HTTP responses.
|
|
489
|
+
// The pattern intentionally matches both `define()` and `defined() and`
|
|
490
|
+
// forms, since both are legal Yii2 entry-point styles.
|
|
491
|
+
regex: /\b(?:define|defined)\s*\(\s*['"]YII_DEBUG['"][^)]*(?:,\s*true|\)\s*(?:and|&&)\s*YII_DEBUG\s*===?\s*true)/,
|
|
492
|
+
description: "YII_DEBUG enabled — leaks full stack traces, file paths, and variable contents in HTTP responses (Yii2)",
|
|
493
|
+
},
|
|
494
|
+
"yii-cookie-no-validation": {
|
|
495
|
+
// Empty / placeholder cookie validation key disables HMAC integrity on
|
|
496
|
+
// signed cookies. Matches blank string or obvious placeholder values.
|
|
497
|
+
regex: /['"]cookieValidationKey['"]\s*=>\s*['"](?:|change[-_]?me|TODO|xxx+|FIXME|placeholder|insert[-_]?key)['"]/i,
|
|
498
|
+
description: "cookieValidationKey is empty or placeholder — signed cookies have no HMAC integrity check (Yii2)",
|
|
499
|
+
},
|
|
500
|
+
"yii-mass-assignment-unsafe": {
|
|
501
|
+
// ->setAttributes($_POST) / ->setAttributes($request->post()) — usually
|
|
502
|
+
// unsafe unless paired with safeAttributes()/scenarios(). We can't tell
|
|
503
|
+
// statically that the class has scenarios(); flag as MEDIUM and let the
|
|
504
|
+
// reviewer make the call.
|
|
505
|
+
regex: /->setAttributes\s*\(\s*(?:\$_(?:POST|GET|REQUEST)\b|Yii::\$app->request->(?:post|get)\(\s*\))/,
|
|
506
|
+
description: "setAttributes() called with raw user input — bypasses scenarios() guards if not paired with safeAttributes (Yii2)",
|
|
507
|
+
},
|
|
508
|
+
"yii-raw-sql-where": {
|
|
509
|
+
// ActiveQuery->where("col = $var") — string interpolation in WHERE.
|
|
510
|
+
// Matches both single and double quotes. Yii2 supports param-binding
|
|
511
|
+
// via array form `['=', 'col', $var]` which is the safe alternative.
|
|
512
|
+
regex: /->where\s*\(\s*["'][^"']*\$\{?[a-zA-Z_]/,
|
|
513
|
+
description: "ActiveQuery->where() with string concatenation — bypasses Yii2 parameter binding (Yii2 SQL injection risk)",
|
|
514
|
+
},
|
|
515
|
+
"php-md5-password": {
|
|
516
|
+
// md5/sha1 applied to anything that smells like a password/secret.
|
|
517
|
+
// High false-positive risk on legitimate hash use; severity HIGH because
|
|
518
|
+
// when it IS a password hash it's a CVE-class bug.
|
|
519
|
+
regex: /\b(?:md5|sha1)\s*\(\s*\$(?:password|hasl|haslo|pwd|pass|secret|token|hash)\b/i,
|
|
520
|
+
description: "md5() or sha1() used on password/secret — both are broken for password hashing. Use password_hash() / Yii::\\$app->security->generatePasswordHash() (PHP)",
|
|
521
|
+
},
|
|
522
|
+
"php-rand-token": {
|
|
523
|
+
// rand() / mt_rand() / uniqid() on a variable named like a token/secret.
|
|
524
|
+
regex: /\$(?:token|nonce|csrf|secret|api[_-]?key|reset[_-]?key)\s*=\s*(?:rand|mt_rand|uniqid)\s*\(/i,
|
|
525
|
+
description: "rand()/mt_rand()/uniqid() used to generate token/secret — not cryptographically secure. Use random_bytes() / Yii::\\$app->security->generateRandomString() (PHP)",
|
|
526
|
+
},
|
|
527
|
+
"php-loose-comparison-secret": {
|
|
528
|
+
// == on hash/token comparison — timing attack. Very narrow regex;
|
|
529
|
+
// requires explicit variable naming.
|
|
530
|
+
regex: /\b(?:==|!=)\s*\$(?:hash|token|signature|hmac|expected[_-]?hash|secret)\b|\$(?:hash|token|signature|hmac)\s*(?:==|!=)\s*[\$"']/i,
|
|
531
|
+
description: "Loose comparison on secret/hash/token — timing-attack vulnerable. Use hash_equals() (PHP)",
|
|
532
|
+
},
|
|
533
|
+
"yii-rbac-cached-permission": {
|
|
534
|
+
// ->can() inside a foreach loop — DbManager hits the DB per call site,
|
|
535
|
+
// O(n) DB roundtrips on a list view. Match foreach + ->can within a
|
|
536
|
+
// bounded window so we don't false-flag unrelated calls in long files.
|
|
537
|
+
regex: /\bforeach\s*\([^{]*\{[\s\S]{0,800}?->can\s*\(/,
|
|
538
|
+
description: "->can() called inside foreach — Yii2 DbManager hits the DB per call. Cache permissions or use checkAccess() once outside the loop (Yii2)",
|
|
539
|
+
},
|
|
540
|
+
"yii-no-row-level-locking": {
|
|
541
|
+
// beginTransaction in the same function as findOne/find()->one()
|
|
542
|
+
// without ->forUpdate() — concurrency bug in incentive/payment flows.
|
|
543
|
+
// Bounded window prevents false positives on long methods that legitimately
|
|
544
|
+
// separate the transaction from the read.
|
|
545
|
+
regex: /->beginTransaction\s*\(\s*\)[\s\S]{0,1500}?(?:::findOne\s*\(|->one\s*\(\s*\))(?![\s\S]{0,200}->forUpdate\b)/,
|
|
546
|
+
description: "Transaction reads a row without SELECT FOR UPDATE — concurrent writers can race and produce duplicate state mutations (Yii2)",
|
|
547
|
+
},
|
|
548
|
+
"yii-config-hardcoded-secret": {
|
|
549
|
+
// Hardcoded literal in 'cookieValidationKey' / 'apiKey' / 'jwtSecret'.
|
|
550
|
+
// Hex/base64 strings of >=20 chars are strong signal. We allow common
|
|
551
|
+
// env() / getenv() lookups as escape hatch.
|
|
552
|
+
regex: /['"](?:cookieValidationKey|apiKey|jwtSecret|secretKey|app[_-]?secret|stripe[_-]?secret)['"]\s*=>\s*['"][A-Za-z0-9+\/_=-]{20,}['"]/,
|
|
553
|
+
description: "Hardcoded secret in config array — should come from env var or runtime/config-local.php that is gitignored (Yii2)",
|
|
554
|
+
},
|
|
555
|
+
"yii-unbounded-all": {
|
|
556
|
+
// Find()-builder ending in ->all() inside a console controller. We can't
|
|
557
|
+
// easily restrict via path in regex, so use file include pattern. The
|
|
558
|
+
// pattern matches any `find()...all()` chain that doesn't use ->limit().
|
|
559
|
+
regex: /::find\s*\([^)]*\)[\s\S]{0,400}?->all\s*\(\s*\)(?![\s\S]{0,100}->limit\b)/,
|
|
560
|
+
description: "ActiveQuery->all() without ->limit() — loads the entire result set into memory. Use ->batch()/->each() for cron/console flows (Yii2 perf)",
|
|
561
|
+
fileIncludePattern: /(?:commands|console)\/[^/]+Controller\.php$/,
|
|
562
|
+
},
|
|
563
|
+
// --- Sprint 7 perf patterns (sourced from tgm-panel performance-audit findings) ---
|
|
564
|
+
"yii-translate-in-loop": {
|
|
565
|
+
// Yii::t() inside a foreach. Costly when paired with DbMessageSource and
|
|
566
|
+
// no message cache (which IS the tgm-panel perf-audit P1 finding). 800-char
|
|
567
|
+
// window after the foreach captures typical loop bodies; nested loops
|
|
568
|
+
// matched separately by global /g.
|
|
569
|
+
regex: /\bforeach\s*\([^{]*\{[\s\S]{0,800}?\\?\bYii::t\s*\(/,
|
|
570
|
+
description: "Yii::t() inside foreach — expensive when DbMessageSource caching is off. Move translation outside the loop OR enable enableCaching on the message source (Yii2 perf)",
|
|
571
|
+
},
|
|
572
|
+
"yii-dbtarget-info-level": {
|
|
573
|
+
// DbTarget log target with 'levels' including info/trace/profile.
|
|
574
|
+
// Writes setting often left from local dev; writes to DB on every
|
|
575
|
+
// request hits hard at scale. Bounded window captures the array.
|
|
576
|
+
regex: /['"]class['"]\s*=>\s*['"][^'"]*DbTarget['"][\s\S]{0,400}?['"]levels['"]\s*=>\s*\[[^\]]*\b(?:info|trace|profile)\b/,
|
|
577
|
+
description: "DbTarget logging info/trace/profile to DB on every request — moves the logger off the hot path (Yii2 perf)",
|
|
578
|
+
},
|
|
579
|
+
"yii-find-with-large-then-filter": {
|
|
580
|
+
// ->find()->all() followed by `array_filter` / `array_map` on the result —
|
|
581
|
+
// pull-then-filter pattern that should be ->where()->all() instead.
|
|
582
|
+
regex: /->find\s*\([^)]*\)[\s\S]{0,200}?->all\s*\(\s*\)\s*;\s*[^\n]{0,200}?\barray_(?:filter|map)\s*\(/,
|
|
583
|
+
description: "ActiveQuery->all() into array_filter/array_map — push the filter into the WHERE clause to reduce I/O (Yii2 perf)",
|
|
584
|
+
},
|
|
585
|
+
"yii-cache-no-ttl": {
|
|
586
|
+
// Yii::$app->cache->set('key', $value) — no TTL argument means cache
|
|
587
|
+
// entry persists indefinitely. Often the deliberate choice, but on
|
|
588
|
+
// user-keyed caches it's a memory bomb.
|
|
589
|
+
regex: /\\?\bYii::\$app->cache->set\s*\(\s*[^,]+,\s*[^,)]+\)/,
|
|
590
|
+
description: "cache->set without TTL — entry persists indefinitely. Add a third TTL argument unless caching a global config value (Yii2 perf)",
|
|
591
|
+
},
|
|
592
|
+
"yii-no-batch-on-large": {
|
|
593
|
+
// Same as yii-unbounded-all but applies to non-controller files (services,
|
|
594
|
+
// jobs/, components/). Together they cover 95% of unbounded reads.
|
|
595
|
+
regex: /::find\s*\([^)]*\)[\s\S]{0,400}?->all\s*\(\s*\)(?![\s\S]{0,100}->(?:limit|batch|each)\b)/,
|
|
596
|
+
description: "find()->all() in service/job code without ->limit() / ->batch() / ->each() — risk of OOM on growing tables (Yii2 perf)",
|
|
597
|
+
fileIncludePattern: /(?:components|services|jobs|workers|tasks)\/[^/]+\.php$/,
|
|
598
|
+
},
|
|
343
599
|
// NestJS anti-patterns
|
|
344
600
|
"nest-circular-inject": {
|
|
345
601
|
regex: /@Inject\s*\(\s*forwardRef\s*\(/,
|
|
@@ -717,6 +973,7 @@ export async function searchPatterns(repo, pattern, options) {
|
|
|
717
973
|
let fileExcludePattern;
|
|
718
974
|
let fileIncludePattern;
|
|
719
975
|
let postFilter;
|
|
976
|
+
let preprocess;
|
|
720
977
|
const builtin = BUILTIN_PATTERNS[pattern];
|
|
721
978
|
if (builtin) {
|
|
722
979
|
regex = builtin.regex;
|
|
@@ -724,6 +981,7 @@ export async function searchPatterns(repo, pattern, options) {
|
|
|
724
981
|
fileExcludePattern = builtin.fileExcludePattern;
|
|
725
982
|
fileIncludePattern = builtin.fileIncludePattern;
|
|
726
983
|
postFilter = builtin.postFilter;
|
|
984
|
+
preprocess = builtin.preprocess;
|
|
727
985
|
}
|
|
728
986
|
else {
|
|
729
987
|
try {
|
|
@@ -751,14 +1009,21 @@ export async function searchPatterns(repo, pattern, options) {
|
|
|
751
1009
|
if (fileIncludePattern && !fileIncludePattern.test(sym.file))
|
|
752
1010
|
continue;
|
|
753
1011
|
scanned++;
|
|
754
|
-
|
|
1012
|
+
// Tier 8 — preprocess source if pattern opts in. Strip preserves positions
|
|
1013
|
+
// so line/column math below remains accurate.
|
|
1014
|
+
const scanSource = preprocess === "strip-comments-strings"
|
|
1015
|
+
? stripCommentsAndStrings(sym.source)
|
|
1016
|
+
: sym.source;
|
|
1017
|
+
const match = regex.exec(scanSource);
|
|
755
1018
|
if (match) {
|
|
756
1019
|
if (!shouldKeepPostFilterMatch(pattern, match[0], postFilter))
|
|
757
1020
|
continue;
|
|
758
1021
|
// Extract context: the matching line(s)
|
|
759
1022
|
const matchStart = match.index;
|
|
760
1023
|
const linesBefore = sym.source.slice(0, matchStart).split("\n").length;
|
|
761
|
-
|
|
1024
|
+
// Use ORIGINAL source for the displayed context line, not stripped.
|
|
1025
|
+
const origLine = sym.source.slice(matchStart, sym.source.indexOf("\n", matchStart) === -1 ? sym.source.length : sym.source.indexOf("\n", matchStart));
|
|
1026
|
+
const matchedText = origLine.length > 0 ? origLine : match[0].split("\n")[0];
|
|
762
1027
|
matches.push({
|
|
763
1028
|
name: sym.name,
|
|
764
1029
|
kind: sym.kind,
|
|
@@ -795,12 +1060,19 @@ export async function searchPatterns(repo, pattern, options) {
|
|
|
795
1060
|
continue;
|
|
796
1061
|
}
|
|
797
1062
|
scanned++;
|
|
798
|
-
|
|
1063
|
+
// Tier 8 — preprocess source if pattern opts in.
|
|
1064
|
+
const scanContent = preprocess === "strip-comments-strings"
|
|
1065
|
+
? stripCommentsAndStrings(content)
|
|
1066
|
+
: content;
|
|
1067
|
+
const match = regex.exec(scanContent);
|
|
799
1068
|
if (match) {
|
|
800
1069
|
if (!shouldKeepPostFilterMatch(pattern, match[0], postFilter))
|
|
801
1070
|
continue;
|
|
802
1071
|
const linesBefore = content.slice(0, match.index).split("\n").length;
|
|
803
|
-
|
|
1072
|
+
// Display original line, not stripped
|
|
1073
|
+
const lineEnd = content.indexOf("\n", match.index);
|
|
1074
|
+
const origLine = content.slice(match.index, lineEnd === -1 ? content.length : lineEnd);
|
|
1075
|
+
const matchedText = origLine.length > 0 ? origLine : match[0].split("\n")[0];
|
|
804
1076
|
matches.push({
|
|
805
1077
|
name: fileEntry.path.split("/").pop() ?? fileEntry.path,
|
|
806
1078
|
kind: "function", // file-level match has no symbol kind
|