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 +37 -0
- package/dist/utils/codeReview.d.ts +14 -0
- package/dist/utils/codeReview.js +67 -9
- package/dist/utils/reviewConfig.d.ts +10 -0
- package/dist/utils/reviewConfig.js +129 -0
- package/package.json +1 -1
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
|
*/
|
package/dist/utils/codeReview.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
200
|
+
while ((match = regex.exec(scannable)) !== null) {
|
|
172
201
|
// Find line number
|
|
173
|
-
const beforeMatch =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|