prodlint 0.7.0 → 0.7.1
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/LICENSE +21 -21
- package/README.md +253 -252
- package/action.yml +152 -152
- package/dist/cli.js +136 -15
- package/dist/index.js +136 -15
- package/dist/mcp.js +136 -15
- package/package.json +83 -83
package/action.yml
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
name: 'Prodlint'
|
|
2
|
-
description: 'The linter for vibe-coded apps — catch what AI coding tools miss'
|
|
3
|
-
branding:
|
|
4
|
-
icon: 'shield'
|
|
5
|
-
color: 'green'
|
|
6
|
-
|
|
7
|
-
inputs:
|
|
8
|
-
path:
|
|
9
|
-
description: 'Path to scan (default: current directory)'
|
|
10
|
-
required: false
|
|
11
|
-
default: '.'
|
|
12
|
-
threshold:
|
|
13
|
-
description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
|
|
14
|
-
required: false
|
|
15
|
-
default: '0'
|
|
16
|
-
ignore:
|
|
17
|
-
description: 'Glob patterns to ignore (comma-separated)'
|
|
18
|
-
required: false
|
|
19
|
-
default: ''
|
|
20
|
-
comment:
|
|
21
|
-
description: 'Post a PR comment with results (true/false)'
|
|
22
|
-
required: false
|
|
23
|
-
default: 'true'
|
|
24
|
-
|
|
25
|
-
outputs:
|
|
26
|
-
score:
|
|
27
|
-
description: 'Overall prodlint score (0-100)'
|
|
28
|
-
value: ${{ steps.scan.outputs.score }}
|
|
29
|
-
critical:
|
|
30
|
-
description: 'Number of critical findings'
|
|
31
|
-
value: ${{ steps.scan.outputs.critical }}
|
|
32
|
-
|
|
33
|
-
runs:
|
|
34
|
-
using: 'composite'
|
|
35
|
-
steps:
|
|
36
|
-
- name: Setup Node.js
|
|
37
|
-
uses: actions/setup-node@v4
|
|
38
|
-
with:
|
|
39
|
-
node-version: '20'
|
|
40
|
-
|
|
41
|
-
- name: Run prodlint
|
|
42
|
-
id: scan
|
|
43
|
-
shell: bash
|
|
44
|
-
env:
|
|
45
|
-
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
-
INPUT_IGNORE: ${{ inputs.ignore }}
|
|
47
|
-
run: |
|
|
48
|
-
CMD_ARGS=("$INPUT_PATH" "--json")
|
|
49
|
-
if [ -n "$INPUT_IGNORE" ]; then
|
|
50
|
-
IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
|
|
51
|
-
for p in "${PATTERNS[@]}"; do
|
|
52
|
-
trimmed=$(echo "$p" | xargs)
|
|
53
|
-
CMD_ARGS+=("--ignore" "$trimmed")
|
|
54
|
-
done
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
|
|
58
|
-
|
|
59
|
-
SCORE=$(echo "$OUTPUT" | node -e "
|
|
60
|
-
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
61
|
-
console.log(Number(d.overallScore) || 0);
|
|
62
|
-
" 2>/dev/null || echo "0")
|
|
63
|
-
SCORE=$(echo "$SCORE" | tr -cd '0-9')
|
|
64
|
-
|
|
65
|
-
CRITICAL=$(echo "$OUTPUT" | node -e "
|
|
66
|
-
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
67
|
-
console.log(Number(d.summary.critical) || 0);
|
|
68
|
-
" 2>/dev/null || echo "0")
|
|
69
|
-
CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
|
|
70
|
-
|
|
71
|
-
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
|
72
|
-
echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
|
|
73
|
-
echo "$OUTPUT" > /tmp/prodlint-result.json
|
|
74
|
-
|
|
75
|
-
- name: Generate comment body
|
|
76
|
-
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
77
|
-
id: comment
|
|
78
|
-
shell: bash
|
|
79
|
-
env:
|
|
80
|
-
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
81
|
-
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
82
|
-
run: |
|
|
83
|
-
SCORE="$SCAN_SCORE"
|
|
84
|
-
THRESHOLD="$INPUT_THRESHOLD"
|
|
85
|
-
|
|
86
|
-
if [ "$SCORE" -ge 80 ]; then
|
|
87
|
-
EMOJI="✅"
|
|
88
|
-
COLOR="brightgreen"
|
|
89
|
-
elif [ "$SCORE" -ge 60 ]; then
|
|
90
|
-
EMOJI="⚠️"
|
|
91
|
-
COLOR="yellow"
|
|
92
|
-
else
|
|
93
|
-
EMOJI="🚨"
|
|
94
|
-
COLOR="red"
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# Build comment from JSON with markdown sanitization
|
|
98
|
-
TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
|
|
99
|
-
node -e "
|
|
100
|
-
const fs = require('fs');
|
|
101
|
-
const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
|
|
102
|
-
const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
|
|
103
|
-
const lines = [];
|
|
104
|
-
lines.push('## ' + '$EMOJI' + ' Prodlint Score: **' + d.overallScore + '/100**');
|
|
105
|
-
lines.push('');
|
|
106
|
-
lines.push('| Category | Score | Issues |');
|
|
107
|
-
lines.push('|----------|-------|--------|');
|
|
108
|
-
for (const c of d.categoryScores) {
|
|
109
|
-
const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
|
|
110
|
-
lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
111
|
-
}
|
|
112
|
-
if (d.summary.critical > 0) {
|
|
113
|
-
lines.push('');
|
|
114
|
-
lines.push('### Critical Issues');
|
|
115
|
-
lines.push('');
|
|
116
|
-
const crits = d.findings.filter(f => f.severity === 'critical');
|
|
117
|
-
for (const f of crits.slice(0, 10)) {
|
|
118
|
-
lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
|
|
119
|
-
}
|
|
120
|
-
if (crits.length > 10) {
|
|
121
|
-
lines.push('- ...and ' + (crits.length - 10) + ' more');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
lines.push('');
|
|
125
|
-
lines.push('---');
|
|
126
|
-
lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
|
|
127
|
-
fs.writeFileSync('$TMPFILE', lines.join('\n'));
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
# Use the generated file for the comment step
|
|
131
|
-
cp "$TMPFILE" /tmp/prodlint-comment.md
|
|
132
|
-
|
|
133
|
-
- name: Post PR comment
|
|
134
|
-
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
135
|
-
uses: marocchino/sticky-pull-request-comment@v2
|
|
136
|
-
with:
|
|
137
|
-
header: prodlint
|
|
138
|
-
path: /tmp/prodlint-comment.md
|
|
139
|
-
|
|
140
|
-
- name: Check threshold
|
|
141
|
-
if: inputs.threshold != '0'
|
|
142
|
-
shell: bash
|
|
143
|
-
env:
|
|
144
|
-
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
145
|
-
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
146
|
-
run: |
|
|
147
|
-
SCORE="$SCAN_SCORE"
|
|
148
|
-
THRESHOLD="$INPUT_THRESHOLD"
|
|
149
|
-
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
150
|
-
echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
|
|
151
|
-
exit 1
|
|
152
|
-
fi
|
|
1
|
+
name: 'Prodlint'
|
|
2
|
+
description: 'The linter for vibe-coded apps — catch what AI coding tools miss'
|
|
3
|
+
branding:
|
|
4
|
+
icon: 'shield'
|
|
5
|
+
color: 'green'
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
path:
|
|
9
|
+
description: 'Path to scan (default: current directory)'
|
|
10
|
+
required: false
|
|
11
|
+
default: '.'
|
|
12
|
+
threshold:
|
|
13
|
+
description: 'Minimum score to pass (0-100). Fails the check if score is below this.'
|
|
14
|
+
required: false
|
|
15
|
+
default: '0'
|
|
16
|
+
ignore:
|
|
17
|
+
description: 'Glob patterns to ignore (comma-separated)'
|
|
18
|
+
required: false
|
|
19
|
+
default: ''
|
|
20
|
+
comment:
|
|
21
|
+
description: 'Post a PR comment with results (true/false)'
|
|
22
|
+
required: false
|
|
23
|
+
default: 'true'
|
|
24
|
+
|
|
25
|
+
outputs:
|
|
26
|
+
score:
|
|
27
|
+
description: 'Overall prodlint score (0-100)'
|
|
28
|
+
value: ${{ steps.scan.outputs.score }}
|
|
29
|
+
critical:
|
|
30
|
+
description: 'Number of critical findings'
|
|
31
|
+
value: ${{ steps.scan.outputs.critical }}
|
|
32
|
+
|
|
33
|
+
runs:
|
|
34
|
+
using: 'composite'
|
|
35
|
+
steps:
|
|
36
|
+
- name: Setup Node.js
|
|
37
|
+
uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: '20'
|
|
40
|
+
|
|
41
|
+
- name: Run prodlint
|
|
42
|
+
id: scan
|
|
43
|
+
shell: bash
|
|
44
|
+
env:
|
|
45
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
+
INPUT_IGNORE: ${{ inputs.ignore }}
|
|
47
|
+
run: |
|
|
48
|
+
CMD_ARGS=("$INPUT_PATH" "--json")
|
|
49
|
+
if [ -n "$INPUT_IGNORE" ]; then
|
|
50
|
+
IFS=',' read -ra PATTERNS <<< "$INPUT_IGNORE"
|
|
51
|
+
for p in "${PATTERNS[@]}"; do
|
|
52
|
+
trimmed=$(echo "$p" | xargs)
|
|
53
|
+
CMD_ARGS+=("--ignore" "$trimmed")
|
|
54
|
+
done
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
OUTPUT=$(npx -y prodlint@latest "${CMD_ARGS[@]}" 2>&1) || true
|
|
58
|
+
|
|
59
|
+
SCORE=$(echo "$OUTPUT" | node -e "
|
|
60
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
61
|
+
console.log(Number(d.overallScore) || 0);
|
|
62
|
+
" 2>/dev/null || echo "0")
|
|
63
|
+
SCORE=$(echo "$SCORE" | tr -cd '0-9')
|
|
64
|
+
|
|
65
|
+
CRITICAL=$(echo "$OUTPUT" | node -e "
|
|
66
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
67
|
+
console.log(Number(d.summary.critical) || 0);
|
|
68
|
+
" 2>/dev/null || echo "0")
|
|
69
|
+
CRITICAL=$(echo "$CRITICAL" | tr -cd '0-9')
|
|
70
|
+
|
|
71
|
+
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
|
72
|
+
echo "critical=${CRITICAL:-0}" >> "$GITHUB_OUTPUT"
|
|
73
|
+
echo "$OUTPUT" > /tmp/prodlint-result.json
|
|
74
|
+
|
|
75
|
+
- name: Generate comment body
|
|
76
|
+
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
77
|
+
id: comment
|
|
78
|
+
shell: bash
|
|
79
|
+
env:
|
|
80
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
81
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
82
|
+
run: |
|
|
83
|
+
SCORE="$SCAN_SCORE"
|
|
84
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
85
|
+
|
|
86
|
+
if [ "$SCORE" -ge 80 ]; then
|
|
87
|
+
EMOJI="✅"
|
|
88
|
+
COLOR="brightgreen"
|
|
89
|
+
elif [ "$SCORE" -ge 60 ]; then
|
|
90
|
+
EMOJI="⚠️"
|
|
91
|
+
COLOR="yellow"
|
|
92
|
+
else
|
|
93
|
+
EMOJI="🚨"
|
|
94
|
+
COLOR="red"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Build comment from JSON with markdown sanitization
|
|
98
|
+
TMPFILE=$(mktemp "${RUNNER_TEMP:-/tmp}/prodlint-comment-XXXXXX.md")
|
|
99
|
+
node -e "
|
|
100
|
+
const fs = require('fs');
|
|
101
|
+
const d = JSON.parse(fs.readFileSync('/tmp/prodlint-result.json', 'utf8'));
|
|
102
|
+
const esc = s => String(s).replace(/[[\]()\\*_\`<>]/g, c => '\\\\' + c);
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push('## ' + '$EMOJI' + ' Prodlint Score: **' + d.overallScore + '/100**');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('| Category | Score | Issues |');
|
|
107
|
+
lines.push('|----------|-------|--------|');
|
|
108
|
+
for (const c of d.categoryScores) {
|
|
109
|
+
const icon = c.score >= 80 ? '🟢' : c.score >= 60 ? '🟡' : '🔴';
|
|
110
|
+
lines.push('| ' + icon + ' ' + esc(c.category) + ' | ' + c.score + '/100 | ' + c.findingCount + ' |');
|
|
111
|
+
}
|
|
112
|
+
if (d.summary.critical > 0) {
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('### Critical Issues');
|
|
115
|
+
lines.push('');
|
|
116
|
+
const crits = d.findings.filter(f => f.severity === 'critical');
|
|
117
|
+
for (const f of crits.slice(0, 10)) {
|
|
118
|
+
lines.push('- \`' + esc(f.ruleId) + '\` \`' + esc(f.file) + ':' + f.line + '\` — ' + esc(f.message));
|
|
119
|
+
}
|
|
120
|
+
if (crits.length > 10) {
|
|
121
|
+
lines.push('- ...and ' + (crits.length - 10) + ' more');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('---');
|
|
126
|
+
lines.push('*Scanned ' + d.filesScanned + ' files in ' + d.scanDurationMs + 'ms · [prodlint](https://prodlint.com)*');
|
|
127
|
+
fs.writeFileSync('$TMPFILE', lines.join('\n'));
|
|
128
|
+
"
|
|
129
|
+
|
|
130
|
+
# Use the generated file for the comment step
|
|
131
|
+
cp "$TMPFILE" /tmp/prodlint-comment.md
|
|
132
|
+
|
|
133
|
+
- name: Post PR comment
|
|
134
|
+
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
135
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
136
|
+
with:
|
|
137
|
+
header: prodlint
|
|
138
|
+
path: /tmp/prodlint-comment.md
|
|
139
|
+
|
|
140
|
+
- name: Check threshold
|
|
141
|
+
if: inputs.threshold != '0'
|
|
142
|
+
shell: bash
|
|
143
|
+
env:
|
|
144
|
+
SCAN_SCORE: ${{ steps.scan.outputs.score }}
|
|
145
|
+
INPUT_THRESHOLD: ${{ inputs.threshold }}
|
|
146
|
+
run: |
|
|
147
|
+
SCORE="$SCAN_SCORE"
|
|
148
|
+
THRESHOLD="$INPUT_THRESHOLD"
|
|
149
|
+
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
150
|
+
echo "::error::Prodlint score ($SCORE) is below threshold ($THRESHOLD)"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
package/dist/cli.js
CHANGED
|
@@ -76,7 +76,10 @@ function isTestFile(relativePath) {
|
|
|
76
76
|
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || /(?:^|\/)__tests__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
|
|
77
77
|
}
|
|
78
78
|
function isScriptFile(relativePath) {
|
|
79
|
-
|
|
79
|
+
if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
|
|
80
|
+
const name = relativePath.split("/").pop() ?? "";
|
|
81
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
82
|
+
return /^(seed|migrate|setup|bootstrap|generate|codegen|sync|deploy|cleanup|reset)$/.test(base);
|
|
80
83
|
}
|
|
81
84
|
function isConfigFile(relativePath) {
|
|
82
85
|
const name = relativePath.split("/").pop() ?? "";
|
|
@@ -245,6 +248,25 @@ function findLoopsAST(ast) {
|
|
|
245
248
|
});
|
|
246
249
|
return loops;
|
|
247
250
|
}
|
|
251
|
+
function getImportSources(ast) {
|
|
252
|
+
const sources = [];
|
|
253
|
+
walkAST(ast.program, (node) => {
|
|
254
|
+
if (node.type === "ImportDeclaration") {
|
|
255
|
+
const decl = node;
|
|
256
|
+
sources.push({ source: decl.source.value, line: decl.loc?.start.line ?? 1 });
|
|
257
|
+
}
|
|
258
|
+
if (node.type === "CallExpression") {
|
|
259
|
+
const call = node;
|
|
260
|
+
if (call.callee.type === "Identifier" && call.callee.name === "require" && call.arguments.length === 1 && call.arguments[0].type === "StringLiteral") {
|
|
261
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
262
|
+
}
|
|
263
|
+
if (call.callee.type === "Import" && call.arguments.length >= 1 && call.arguments[0].type === "StringLiteral") {
|
|
264
|
+
sources.push({ source: call.arguments[0].value, line: node.loc?.start.line ?? 1 });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return sources;
|
|
269
|
+
}
|
|
248
270
|
function isUserInputNode(node) {
|
|
249
271
|
if (node.type === "MemberExpression") {
|
|
250
272
|
const mem = node;
|
|
@@ -439,6 +461,66 @@ async function readFileContext(root, relativePath) {
|
|
|
439
461
|
return null;
|
|
440
462
|
}
|
|
441
463
|
}
|
|
464
|
+
async function getWorkspacePatterns(root, packageJson) {
|
|
465
|
+
const patterns = [];
|
|
466
|
+
if (packageJson) {
|
|
467
|
+
const workspaces = packageJson.workspaces;
|
|
468
|
+
if (Array.isArray(workspaces)) {
|
|
469
|
+
patterns.push(...workspaces);
|
|
470
|
+
} else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
471
|
+
patterns.push(...workspaces.packages);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const raw = await readFile(resolve(root, "pnpm-workspace.yaml"), "utf-8");
|
|
476
|
+
let inPackages = false;
|
|
477
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
478
|
+
const trimmed = line.trim();
|
|
479
|
+
if (/^packages\s*:/.test(trimmed)) {
|
|
480
|
+
inPackages = true;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (inPackages) {
|
|
484
|
+
if (/^-\s+/.test(trimmed)) {
|
|
485
|
+
const glob = trimmed.replace(/^-\s+/, "").replace(/^['"]|['"]$/g, "");
|
|
486
|
+
if (glob) patterns.push(glob);
|
|
487
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
return patterns;
|
|
495
|
+
}
|
|
496
|
+
async function collectWorkspaceDependencies(root, patterns) {
|
|
497
|
+
const deps = /* @__PURE__ */ new Set();
|
|
498
|
+
const globPatterns = patterns.map((p) => `${p}/package.json`);
|
|
499
|
+
try {
|
|
500
|
+
const pkgFiles = await fg(globPatterns, {
|
|
501
|
+
cwd: root,
|
|
502
|
+
absolute: false,
|
|
503
|
+
ignore: ["**/node_modules/**"]
|
|
504
|
+
});
|
|
505
|
+
for (const pkgFile of pkgFiles) {
|
|
506
|
+
try {
|
|
507
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
508
|
+
const pkg = JSON.parse(raw);
|
|
509
|
+
if (pkg.name) deps.add(pkg.name);
|
|
510
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
511
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
512
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
513
|
+
deps.add(dep);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
return deps;
|
|
523
|
+
}
|
|
442
524
|
async function buildProjectContext(root, files) {
|
|
443
525
|
let packageJson = null;
|
|
444
526
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -457,19 +539,26 @@ async function buildProjectContext(root, files) {
|
|
|
457
539
|
...packageJson?.peerDependencies ?? {}
|
|
458
540
|
};
|
|
459
541
|
declaredDependencies = new Set(Object.keys(deps));
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
const workspacePatterns = await getWorkspacePatterns(root, packageJson);
|
|
545
|
+
if (workspacePatterns.length > 0) {
|
|
546
|
+
const workspaceDeps = await collectWorkspaceDependencies(root, workspacePatterns);
|
|
547
|
+
for (const dep of workspaceDeps) {
|
|
548
|
+
declaredDependencies.add(dep);
|
|
465
549
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
550
|
+
}
|
|
551
|
+
for (const dep of declaredDependencies) {
|
|
552
|
+
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
553
|
+
if (framework) {
|
|
554
|
+
detectedFrameworks.add(framework);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const framework of detectedFrameworks) {
|
|
558
|
+
if (RATE_LIMIT_FRAMEWORKS.has(framework)) {
|
|
559
|
+
hasRateLimiting = true;
|
|
560
|
+
break;
|
|
471
561
|
}
|
|
472
|
-
} catch {
|
|
473
562
|
}
|
|
474
563
|
try {
|
|
475
564
|
const raw = await readFile(resolve(root, "tsconfig.json"), "utf-8");
|
|
@@ -724,6 +813,32 @@ var hallucinatedImportsRule = {
|
|
|
724
813
|
const findings = [];
|
|
725
814
|
const seen = /* @__PURE__ */ new Set();
|
|
726
815
|
const isNonProd = isTestFile(file.relativePath) || isScriptFile(file.relativePath);
|
|
816
|
+
if (file.ast) {
|
|
817
|
+
try {
|
|
818
|
+
const imports = getImportSources(file.ast);
|
|
819
|
+
for (const { source: importPath, line } of imports) {
|
|
820
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
|
|
821
|
+
const pkgName = getPackageName(importPath);
|
|
822
|
+
if (seen.has(pkgName)) continue;
|
|
823
|
+
seen.add(pkgName);
|
|
824
|
+
if (isPathAlias(importPath, project.tsconfigPaths)) continue;
|
|
825
|
+
if (isNodeBuiltin(pkgName)) continue;
|
|
826
|
+
if (IMPLICIT_PACKAGES.has(importPath) || IMPLICIT_PACKAGES.has(pkgName)) continue;
|
|
827
|
+
if (project.declaredDependencies.has(pkgName)) continue;
|
|
828
|
+
findings.push({
|
|
829
|
+
ruleId: "hallucinated-imports",
|
|
830
|
+
file: file.relativePath,
|
|
831
|
+
line,
|
|
832
|
+
column: 1,
|
|
833
|
+
message: `Package "${pkgName}" is imported but not in package.json`,
|
|
834
|
+
severity: isNonProd ? "warning" : "critical",
|
|
835
|
+
category: "reliability"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return findings;
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
}
|
|
727
842
|
for (let i = 0; i < file.lines.length; i++) {
|
|
728
843
|
if (isCommentLine(file.lines, i, file.commentMap)) continue;
|
|
729
844
|
const line = file.lines[i];
|
|
@@ -2255,8 +2370,14 @@ function scoreCatchBody(bodyLines) {
|
|
|
2255
2370
|
const body = bodyLines.join("\n");
|
|
2256
2371
|
let score = 0;
|
|
2257
2372
|
if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
|
|
2258
|
-
if (/console\.(error|warn)\s*\(/.test(body) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2259
|
-
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body))
|
|
2373
|
+
if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
|
|
2374
|
+
if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
|
|
2375
|
+
/\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
|
|
2376
|
+
/\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
|
|
2377
|
+
/\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
|
|
2378
|
+
/\b(handleError|reportError|logError|notifyError|showError|onError|trackError)\s*\(/.test(body) || // Express middleware: next(err)
|
|
2379
|
+
/\bnext\s*\(\s*(err|error|e)\s*\)/.test(body) || // Next.js notFound()
|
|
2380
|
+
/\bnotFound\s*\(/.test(body)) {
|
|
2260
2381
|
score = 3;
|
|
2261
2382
|
}
|
|
2262
2383
|
const labels = {
|
|
@@ -2288,7 +2409,7 @@ var shallowCatchRule = {
|
|
|
2288
2409
|
if (!body || !body.loc) return;
|
|
2289
2410
|
const bodyStart = body.loc.start.line - 1;
|
|
2290
2411
|
const bodyEnd = body.loc.end.line - 1;
|
|
2291
|
-
const bodyLines = file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2412
|
+
const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
|
|
2292
2413
|
const { score, label } = scoreCatchBody(bodyLines);
|
|
2293
2414
|
if (score <= 1) {
|
|
2294
2415
|
findings.push({
|