codeep 2.5.2 → 2.6.0

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 CHANGED
@@ -217,6 +217,43 @@ AI-powered review of your git diff with `/review`:
217
217
 
218
218
  If there are no git changes, falls back to static analysis automatically.
219
219
 
220
+ #### Custom rules (`.codeep/review.json`)
221
+
222
+ The static reviewer (`codeep review` / `/review --static`) ships a set of
223
+ built-in rules, but a project can tailor them — check a `.codeep/review.json`
224
+ into the repo and the CLI **and** the [Codeep GitHub Action](https://github.com/VladoIvankovic/codeep-action)
225
+ both pick it up automatically (zero LLM cost):
226
+
227
+ ```json
228
+ {
229
+ "rules": [
230
+ {
231
+ "id": "no-internal-import",
232
+ "pattern": "from ['\"]@acme/internal",
233
+ "category": "best-practice",
234
+ "severity": "error",
235
+ "message": "Don't import from @acme/internal outside the platform team",
236
+ "suggestion": "Use the public @acme/sdk package",
237
+ "extensions": [".ts", ".tsx"]
238
+ }
239
+ ],
240
+ "disable": ["todo-comment", "anonymous-function"],
241
+ "include": ["src/**"],
242
+ "exclude": ["**/*.test.ts", "vendor/**"]
243
+ }
244
+ ```
245
+
246
+ - **`rules`** — your own checks. `id`, `pattern` (a regex string), and `message`
247
+ are required; `flags` (default `g`), `category`, `severity`
248
+ (`error|warning|info|suggestion`), `suggestion`, and `extensions` are optional.
249
+ - **`disable`** — turn off built-in rules by id (e.g. `eval-usage`,
250
+ `hardcoded-password`, `todo-comment`, `any-type`, `console-statement`,
251
+ `long-file`, `long-function`, …).
252
+ - **`include` / `exclude`** — glob scoping (`**`, `*`, `?`); `include` empty = all files.
253
+
254
+ A missing, malformed, or partially-invalid config never breaks a review — bad
255
+ entries are skipped and the run proceeds with whatever is valid.
256
+
220
257
  ### Interactive Mode
221
258
  Agent asks clarifying questions when tasks are ambiguous:
222
259
  ```
@@ -28,6 +28,20 @@ export interface ReviewSummary {
28
28
  byCategory: Record<ReviewCategory, number>;
29
29
  bySeverity: Record<string, number>;
30
30
  }
31
+ /**
32
+ * A single deterministic review rule. Built-in rules and user rules from
33
+ * `.codeep/review.json` share this shape. `id` is stable so a project can
34
+ * disable a built-in rule by id (see utils/reviewConfig.ts).
35
+ */
36
+ export interface RuleDef {
37
+ id: string;
38
+ pattern: RegExp;
39
+ category: ReviewCategory;
40
+ severity: ReviewIssue['severity'];
41
+ message: string;
42
+ suggestion?: string;
43
+ extensions?: string[];
44
+ }
31
45
  /**
32
46
  * Perform code review
33
47
  */
@@ -4,10 +4,13 @@
4
4
  import { existsSync, readFileSync, readdirSync } from 'fs';
5
5
  import { join, extname, relative } from 'path';
6
6
  import { getChangedFiles } from './git.js';
7
- // Common code patterns that indicate issues
7
+ import { loadReviewConfig, globToRegExp } from './reviewConfig.js';
8
+ // Built-in code patterns that indicate issues. Each has a stable `id` so it can
9
+ // be turned off per-project via `.codeep/review.json` { "disable": ["..."] }.
8
10
  const CODE_PATTERNS = [
9
11
  // Security issues
10
12
  {
13
+ id: 'eval-usage',
11
14
  pattern: /eval\s*\(/g,
12
15
  category: 'security',
13
16
  severity: 'error',
@@ -16,6 +19,7 @@ const CODE_PATTERNS = [
16
19
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
17
20
  },
18
21
  {
22
+ id: 'inner-html',
19
23
  pattern: /innerHTML\s*=/g,
20
24
  category: 'security',
21
25
  severity: 'warning',
@@ -24,6 +28,7 @@ const CODE_PATTERNS = [
24
28
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
25
29
  },
26
30
  {
31
+ id: 'dangerously-set-inner-html',
27
32
  pattern: /dangerouslySetInnerHTML/g,
28
33
  category: 'security',
29
34
  severity: 'warning',
@@ -32,6 +37,7 @@ const CODE_PATTERNS = [
32
37
  extensions: ['.jsx', '.tsx'],
33
38
  },
34
39
  {
40
+ id: 'hardcoded-password',
35
41
  pattern: /password\s*=\s*['"][^'"]+['"]/gi,
36
42
  category: 'security',
37
43
  severity: 'error',
@@ -39,6 +45,7 @@ const CODE_PATTERNS = [
39
45
  suggestion: 'Use environment variables for sensitive data',
40
46
  },
41
47
  {
48
+ id: 'hardcoded-api-key',
42
49
  pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi,
43
50
  category: 'security',
44
51
  severity: 'error',
@@ -47,6 +54,7 @@ const CODE_PATTERNS = [
47
54
  },
48
55
  // Performance issues
49
56
  {
57
+ id: 'foreach-await',
50
58
  pattern: /\.forEach\s*\([^)]*\)\s*{\s*await/g,
51
59
  category: 'performance',
52
60
  severity: 'warning',
@@ -55,6 +63,7 @@ const CODE_PATTERNS = [
55
63
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
56
64
  },
57
65
  {
66
+ id: 'await-in-loop',
58
67
  pattern: /for\s*\([^)]+\)\s*{\s*await/g,
59
68
  category: 'performance',
60
69
  severity: 'info',
@@ -63,6 +72,7 @@ const CODE_PATTERNS = [
63
72
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
64
73
  },
65
74
  {
75
+ id: 'select-star',
66
76
  pattern: /SELECT\s+\*/gi,
67
77
  category: 'performance',
68
78
  severity: 'warning',
@@ -71,6 +81,7 @@ const CODE_PATTERNS = [
71
81
  },
72
82
  // Bug-prone patterns
73
83
  {
84
+ id: 'loose-null-check',
74
85
  pattern: /==\s*null|null\s*==/g,
75
86
  category: 'bug',
76
87
  severity: 'info',
@@ -79,6 +90,7 @@ const CODE_PATTERNS = [
79
90
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
80
91
  },
81
92
  {
93
+ id: 'empty-catch',
82
94
  pattern: /catch\s*\(\s*\w*\s*\)\s*{\s*}/g,
83
95
  category: 'bug',
84
96
  severity: 'warning',
@@ -86,6 +98,7 @@ const CODE_PATTERNS = [
86
98
  suggestion: 'Log the error or handle it appropriately',
87
99
  },
88
100
  {
101
+ id: 'console-statement',
89
102
  pattern: /console\.(log|debug|info|warn|error)\s*\(/g,
90
103
  category: 'maintainability',
91
104
  severity: 'info',
@@ -94,6 +107,7 @@ const CODE_PATTERNS = [
94
107
  extensions: ['.js', '.ts', '.jsx', '.tsx'],
95
108
  },
96
109
  {
110
+ id: 'todo-comment',
97
111
  pattern: /TODO|FIXME|HACK|XXX/g,
98
112
  category: 'maintainability',
99
113
  severity: 'info',
@@ -102,6 +116,7 @@ const CODE_PATTERNS = [
102
116
  },
103
117
  // Type safety
104
118
  {
119
+ id: 'any-type',
105
120
  pattern: /:\s*any\b/g,
106
121
  category: 'types',
107
122
  severity: 'warning',
@@ -110,6 +125,7 @@ const CODE_PATTERNS = [
110
125
  extensions: ['.ts', '.tsx'],
111
126
  },
112
127
  {
128
+ id: 'ts-ignore',
113
129
  pattern: /@ts-ignore/g,
114
130
  category: 'types',
115
131
  severity: 'warning',
@@ -118,6 +134,7 @@ const CODE_PATTERNS = [
118
134
  extensions: ['.ts', '.tsx'],
119
135
  },
120
136
  {
137
+ id: 'as-any',
121
138
  pattern: /as\s+any\b/g,
122
139
  category: 'types',
123
140
  severity: 'warning',
@@ -127,6 +144,7 @@ const CODE_PATTERNS = [
127
144
  },
128
145
  // Best practices
129
146
  {
147
+ id: 'var-usage',
130
148
  pattern: /var\s+\w+/g,
131
149
  category: 'best-practice',
132
150
  severity: 'info',
@@ -135,6 +153,7 @@ const CODE_PATTERNS = [
135
153
  extensions: ['.js', '.jsx'],
136
154
  },
137
155
  {
156
+ id: 'anonymous-function',
138
157
  pattern: /function\s*\(/g,
139
158
  category: 'style',
140
159
  severity: 'info',
@@ -144,6 +163,7 @@ const CODE_PATTERNS = [
144
163
  },
145
164
  // Documentation
146
165
  {
166
+ id: 'missing-jsdoc',
147
167
  pattern: /export\s+(default\s+)?(?:function|class|const)\s+\w+/g,
148
168
  category: 'documentation',
149
169
  severity: 'suggestion',
@@ -155,22 +175,31 @@ const CODE_PATTERNS = [
155
175
  /**
156
176
  * Analyze a single file for issues
157
177
  */
158
- function analyzeFile(filePath, content, projectRoot) {
178
+ function analyzeFile(filePath, content, projectRoot, rules, disabled) {
159
179
  const issues = [];
160
180
  const ext = extname(filePath);
161
181
  const relativePath = relative(projectRoot, filePath);
162
182
  const lines = content.split('\n');
163
- for (const pattern of CODE_PATTERNS) {
183
+ // Skip the regex pass on very large files (the cheap line-count heuristics
184
+ // below still run) so an oversized file can't stall the reviewer. NOTE: this
185
+ // bounds input SIZE only, not regex run-time — catastrophic backtracking is a
186
+ // function of pattern shape. Untrusted custom rules from .codeep/review.json
187
+ // are additionally screened at load (utils/reviewConfig.ts) and the GitHub
188
+ // Action caps wall-clock, but a zero-width match is guarded right here.
189
+ const scannable = content.length <= 2_000_000 ? content : '';
190
+ const MAX_MATCHES_PER_RULE = 1000;
191
+ for (const pattern of rules) {
164
192
  // Skip if pattern doesn't apply to this file type
165
193
  if (pattern.extensions && !pattern.extensions.includes(ext)) {
166
194
  continue;
167
195
  }
168
196
  // Find all matches
169
197
  let match;
198
+ let count = 0;
170
199
  const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
171
- while ((match = regex.exec(content)) !== null) {
200
+ while ((match = regex.exec(scannable)) !== null) {
172
201
  // Find line number
173
- const beforeMatch = content.slice(0, match.index);
202
+ const beforeMatch = scannable.slice(0, match.index);
174
203
  const lineNumber = beforeMatch.split('\n').length;
175
204
  issues.push({
176
205
  file: relativePath,
@@ -180,10 +209,17 @@ function analyzeFile(filePath, content, projectRoot) {
180
209
  message: pattern.message,
181
210
  suggestion: pattern.suggestion,
182
211
  });
212
+ // A zero-width match (e.g. a custom rule like `a?` or `(?:)`) leaves
213
+ // lastIndex unchanged, so exec() would return it forever — advance past it.
214
+ if (match.index === regex.lastIndex)
215
+ regex.lastIndex++;
216
+ // Bound pathological match floods (also caps the per-match work above).
217
+ if (++count >= MAX_MATCHES_PER_RULE)
218
+ break;
183
219
  }
184
220
  }
185
221
  // Check for long files
186
- if (lines.length > 500) {
222
+ if (!disabled.has('long-file') && lines.length > 500) {
187
223
  issues.push({
188
224
  file: relativePath,
189
225
  severity: 'info',
@@ -194,7 +230,8 @@ function analyzeFile(filePath, content, projectRoot) {
194
230
  // Check for long functions (basic heuristic)
195
231
  let braceDepth = 0;
196
232
  let functionStart = -1;
197
- for (let i = 0; i < lines.length; i++) {
233
+ const checkLongFunctions = !disabled.has('long-function');
234
+ for (let i = 0; checkLongFunctions && i < lines.length; i++) {
198
235
  const line = lines[i];
199
236
  if (/function\s+\w+|=>\s*{|\)\s*{/.test(line)) {
200
237
  if (braceDepth === 0) {
@@ -278,7 +315,28 @@ function getAllSourceFiles(dir, maxFiles = 50) {
278
315
  */
279
316
  export function performCodeReview(projectContext, specificFiles) {
280
317
  const projectRoot = projectContext.root || process.cwd();
281
- const filesToReview = getFilesToReview(projectRoot, specificFiles);
318
+ // Project-level config (.codeep/review.json): custom rules, disabled built-in
319
+ // ids, and include/exclude globs. Absent/invalid → defaults (built-ins only).
320
+ const config = loadReviewConfig(projectRoot);
321
+ const disabled = config?.disabled ?? new Set();
322
+ const effectiveRules = [
323
+ ...CODE_PATTERNS.filter((p) => !disabled.has(p.id)),
324
+ ...(config?.rules ?? []),
325
+ ];
326
+ let filesToReview = getFilesToReview(projectRoot, specificFiles);
327
+ // Apply include/exclude globs (posix-relative paths). Empty include = all.
328
+ if (config && (config.include.length > 0 || config.exclude.length > 0)) {
329
+ const inc = config.include.map(globToRegExp);
330
+ const exc = config.exclude.map(globToRegExp);
331
+ filesToReview = filesToReview.filter((f) => {
332
+ const rel = relative(projectRoot, f).split('\\').join('/');
333
+ if (inc.length > 0 && !inc.some((re) => re.test(rel)))
334
+ return false;
335
+ if (exc.some((re) => re.test(rel)))
336
+ return false;
337
+ return true;
338
+ });
339
+ }
282
340
  const allIssues = [];
283
341
  // Determine scope — mirrors the branching in getFilesToReview so the user
284
342
  // sees exactly which branch ran.
@@ -295,7 +353,7 @@ export function performCodeReview(projectContext, specificFiles) {
295
353
  for (const filePath of filesToReview) {
296
354
  try {
297
355
  const content = readFileSync(filePath, 'utf-8');
298
- const issues = analyzeFile(filePath, content, projectRoot);
356
+ const issues = analyzeFile(filePath, content, projectRoot, effectiveRules, disabled);
299
357
  allIssues.push(...issues);
300
358
  }
301
359
  catch { }
@@ -0,0 +1,10 @@
1
+ import type { RuleDef } from './codeReview';
2
+ export interface ReviewConfig {
3
+ rules: RuleDef[];
4
+ disabled: Set<string>;
5
+ include: string[];
6
+ exclude: string[];
7
+ }
8
+ /** Convert a simple glob (`**`, `*`, `?`) into an anchored RegExp over posix paths. */
9
+ export declare function globToRegExp(glob: string): RegExp;
10
+ export declare function loadReviewConfig(projectRoot: string): ReviewConfig | null;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Project-level review configuration: `.codeep/review.json`.
3
+ *
4
+ * Lets a repo extend the deterministic reviewer with its own rules, disable
5
+ * built-in rules by id, and scope which files are reviewed — all checked into
6
+ * the repo so the CLI (`codeep review`) and the GitHub Action enforce the same
7
+ * conventions with zero LLM cost. Loading is fully defensive: a missing,
8
+ * malformed, or partially-invalid config never throws — bad entries are skipped
9
+ * with a warning and the review proceeds with whatever is valid.
10
+ *
11
+ * Shape:
12
+ * {
13
+ * "rules": [
14
+ * { "id": "no-foo", "pattern": "\\bfoo\\(", "flags": "gi",
15
+ * "category": "bug", "severity": "warning",
16
+ * "message": "Avoid foo()", "suggestion": "Use bar()",
17
+ * "extensions": [".ts", ".js"] }
18
+ * ],
19
+ * "disable": ["eval-usage", "todo-comment"],
20
+ * "include": ["src/**"],
21
+ * "exclude": ["vendor/**", "dist/**"]
22
+ * }
23
+ */
24
+ import { existsSync, readFileSync } from 'fs';
25
+ import { join } from 'path';
26
+ const CONFIG_PATH = '.codeep/review.json';
27
+ const MAX_RULES = 200;
28
+ const VALID_CATEGORIES = [
29
+ 'security', 'performance', 'maintainability', 'bug', 'style', 'types', 'best-practice', 'documentation',
30
+ ];
31
+ const VALID_SEVERITIES = ['error', 'warning', 'info', 'suggestion'];
32
+ /** Convert a simple glob (`**`, `*`, `?`) into an anchored RegExp over posix paths. */
33
+ export function globToRegExp(glob) {
34
+ // Escape regex metacharacters but keep the glob wildcards * ? for translation.
35
+ // Function replacer (not `$&`) so a literal `$` in a path can't be mangled.
36
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, (m) => '\\' + m);
37
+ // Plain ASCII sentinels that won't appear in a real glob; split/join avoids
38
+ // any `$`-replacement pitfalls when substituting the regex fragments.
39
+ const DSTAR_SLASH = '__CODEEP_DSTAR_SLASH__';
40
+ const DSTAR = '__CODEEP_DSTAR__';
41
+ const body = escaped
42
+ .replace(/\*\*\//g, DSTAR_SLASH) // **/ → zero or more directory segments
43
+ .replace(/\*\*/g, DSTAR) // ** → anything, including slashes
44
+ .replace(/\*/g, '[^/]*') // * → within a single path segment
45
+ .replace(/\?/g, '[^/]') // ? → a single non-slash char
46
+ .split(DSTAR_SLASH).join('(?:.*/)?')
47
+ .split(DSTAR).join('.*');
48
+ return new RegExp('^' + body + '$');
49
+ }
50
+ function asStringArray(v) {
51
+ return Array.isArray(v)
52
+ ? v.filter((x) => typeof x === 'string' && x.length > 0)
53
+ : [];
54
+ }
55
+ export function loadReviewConfig(projectRoot) {
56
+ const filePath = join(projectRoot, CONFIG_PATH);
57
+ if (!existsSync(filePath))
58
+ return null;
59
+ let data;
60
+ try {
61
+ data = JSON.parse(readFileSync(filePath, 'utf-8'));
62
+ }
63
+ catch {
64
+ console.warn(`[codeep] Ignoring ${CONFIG_PATH}: not valid JSON.`);
65
+ return null;
66
+ }
67
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
68
+ console.warn(`[codeep] Ignoring ${CONFIG_PATH}: expected a JSON object.`);
69
+ return null;
70
+ }
71
+ const cfg = data;
72
+ const disabled = new Set(asStringArray(cfg.disable));
73
+ const include = asStringArray(cfg.include);
74
+ const exclude = asStringArray(cfg.exclude);
75
+ const rules = [];
76
+ const rawRules = Array.isArray(cfg.rules) ? cfg.rules.slice(0, MAX_RULES) : [];
77
+ for (const raw of rawRules) {
78
+ if (!raw || typeof raw !== 'object')
79
+ continue;
80
+ const r = raw;
81
+ const id = typeof r.id === 'string' && r.id.trim() ? r.id.trim() : null;
82
+ const message = typeof r.message === 'string' && r.message.trim() ? r.message.trim() : null;
83
+ const patternSrc = typeof r.pattern === 'string' && r.pattern ? r.pattern : null;
84
+ if (!id || !message || !patternSrc) {
85
+ console.warn(`[codeep] Skipping a rule in ${CONFIG_PATH}: each rule needs id, pattern and message.`);
86
+ continue;
87
+ }
88
+ // Reject oversized patterns outright — keeps regex compilation/run bounded.
89
+ if (patternSrc.length > 1000) {
90
+ console.warn(`[codeep] Skipping rule "${id}" in ${CONFIG_PATH}: pattern is too long (>1000 chars).`);
91
+ continue;
92
+ }
93
+ // Conservative ReDoS screen: reject the classic catastrophic shape — a group
94
+ // ending in an unbounded quantifier that is itself quantified, e.g. (a+)+,
95
+ // (\d*)*, (.*)+, (x+){2,}. Not exhaustive (the GitHub Action also bounds
96
+ // wall-clock), but it blocks the common foot-guns in an untrusted review.json.
97
+ if (/\([^)]*[+*]\)\s*[+*{]/.test(patternSrc)) {
98
+ console.warn(`[codeep] Skipping rule "${id}" in ${CONFIG_PATH}: nested quantifiers risk catastrophic backtracking (ReDoS).`);
99
+ continue;
100
+ }
101
+ // Always include the global flag so every match in a file is found.
102
+ let flags = typeof r.flags === 'string' && /^[gimsuy]*$/.test(r.flags) ? r.flags : '';
103
+ if (!flags.includes('g'))
104
+ flags += 'g';
105
+ let pattern;
106
+ try {
107
+ pattern = new RegExp(patternSrc, flags);
108
+ }
109
+ catch {
110
+ console.warn(`[codeep] Skipping rule "${id}" in ${CONFIG_PATH}: invalid regex.`);
111
+ continue;
112
+ }
113
+ const category = VALID_CATEGORIES.includes(r.category)
114
+ ? r.category : 'best-practice';
115
+ const severity = VALID_SEVERITIES.includes(r.severity)
116
+ ? r.severity : 'warning';
117
+ const extensions = asStringArray(r.extensions);
118
+ rules.push({
119
+ id,
120
+ pattern,
121
+ category,
122
+ severity,
123
+ message,
124
+ suggestion: typeof r.suggestion === 'string' ? r.suggestion : undefined,
125
+ extensions: extensions.length ? extensions : undefined,
126
+ });
127
+ }
128
+ return { rules, disabled, include, exclude };
129
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",