llm-slop-detector 0.5.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/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +429 -0
- package/THIRD_PARTY_NOTICES.md +310 -0
- package/builtin-packs/academic.json +105 -0
- package/builtin-packs/claudeisms.json +47 -0
- package/builtin-packs/cliches.json +57 -0
- package/builtin-packs/fiction.json +88 -0
- package/builtin-packs/security.json +359 -0
- package/builtin-packs/structural.json +31 -0
- package/builtin-rules.json +84 -0
- package/out/cli.js +421 -0
- package/out/core/comments.js +179 -0
- package/out/core/ignore.js +147 -0
- package/out/core/rules.js +301 -0
- package/out/core/scan.js +321 -0
- package/out/core/types.js +10 -0
- package/out/extension.js +607 -0
- package/out/mcp.js +265 -0
- package/out/rules.js +46 -0
- package/package.json +228 -0
package/out/core/scan.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.charDiagnosticMessage = charDiagnosticMessage;
|
|
4
|
+
exports.scanText = scanText;
|
|
5
|
+
exports.offsetToLineCol = offsetToLineCol;
|
|
6
|
+
const comments_1 = require("./comments");
|
|
7
|
+
function charDiagnosticMessage(def) {
|
|
8
|
+
const parts = [def.name];
|
|
9
|
+
if (def.replacement !== undefined) {
|
|
10
|
+
const shown = def.replacement === '' ? 'delete' :
|
|
11
|
+
def.replacement === '\n' ? 'newline' :
|
|
12
|
+
def.replacement === ' ' ? 'regular space' :
|
|
13
|
+
JSON.stringify(def.replacement);
|
|
14
|
+
parts.push(`fix: ${shown}`);
|
|
15
|
+
}
|
|
16
|
+
else if (def.suggestion) {
|
|
17
|
+
parts.push(def.suggestion);
|
|
18
|
+
}
|
|
19
|
+
return `${parts.join(' - ')} [${def.source}]`;
|
|
20
|
+
}
|
|
21
|
+
function scanText(text, rules, language) {
|
|
22
|
+
const findings = [];
|
|
23
|
+
const excluded = computeExcludedRanges(text, language);
|
|
24
|
+
if (excluded === null)
|
|
25
|
+
return findings;
|
|
26
|
+
const suppressions = computeSuppressions(text, excluded);
|
|
27
|
+
rules.charRegex.lastIndex = 0;
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = rules.charRegex.exec(text)) !== null) {
|
|
30
|
+
const def = rules.chars.get(m[0]);
|
|
31
|
+
if (!def)
|
|
32
|
+
continue;
|
|
33
|
+
if (offsetInRanges(m.index, excluded))
|
|
34
|
+
continue;
|
|
35
|
+
if (isSuppressed(m.index, suppressions, 'char', m[0]))
|
|
36
|
+
continue;
|
|
37
|
+
findings.push({
|
|
38
|
+
offset: m.index,
|
|
39
|
+
length: m[0].length,
|
|
40
|
+
matchText: m[0],
|
|
41
|
+
code: 'char',
|
|
42
|
+
severity: def.severity,
|
|
43
|
+
message: charDiagnosticMessage(def),
|
|
44
|
+
source: def.source,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const p of rules.phrases) {
|
|
48
|
+
p.regex.lastIndex = 0;
|
|
49
|
+
while ((m = p.regex.exec(text)) !== null) {
|
|
50
|
+
if (m[0].length === 0) {
|
|
51
|
+
p.regex.lastIndex++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (offsetInRanges(m.index, excluded))
|
|
55
|
+
continue;
|
|
56
|
+
if (isSuppressed(m.index, suppressions, 'phrase', m[0], p.pattern))
|
|
57
|
+
continue;
|
|
58
|
+
const reasonBit = p.reason ? ` - ${p.reason}` : '';
|
|
59
|
+
findings.push({
|
|
60
|
+
offset: m.index,
|
|
61
|
+
length: m[0].length,
|
|
62
|
+
matchText: m[0],
|
|
63
|
+
code: 'phrase',
|
|
64
|
+
severity: p.severity,
|
|
65
|
+
message: `LLM-style phrase: "${m[0]}"${reasonBit} [${p.source}]`,
|
|
66
|
+
source: p.source,
|
|
67
|
+
rulePattern: p.pattern,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Scope: markdown exclusions, code-comment inclusion, inline ignore directives
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Returns the ranges that should be SKIPPED during scanning. null means the
|
|
77
|
+
// language isn't supported and the file should not be scanned at all.
|
|
78
|
+
function computeExcludedRanges(text, language) {
|
|
79
|
+
if (language === 'markdown')
|
|
80
|
+
return computeMarkdownExclusions(text);
|
|
81
|
+
if (language === 'plaintext' || language === 'scminput')
|
|
82
|
+
return [];
|
|
83
|
+
if (language === 'git-commit')
|
|
84
|
+
return computeGitCommitExclusions(text);
|
|
85
|
+
const commentScanner = (0, comments_1.getCommentScanner)(language);
|
|
86
|
+
if (commentScanner === null)
|
|
87
|
+
return null;
|
|
88
|
+
const comments = mergeRanges(commentScanner(text));
|
|
89
|
+
return invertRanges(comments, text.length);
|
|
90
|
+
}
|
|
91
|
+
// Skip '#' comment lines (stripped by git before commit) and everything after
|
|
92
|
+
// the verbose-commit scissors marker ("# ------------------------ >8 ---...").
|
|
93
|
+
function computeGitCommitExclusions(text) {
|
|
94
|
+
const ranges = [];
|
|
95
|
+
const scissorsRe = /^#\s*-+\s*>8\s*-+/;
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i <= text.length) {
|
|
98
|
+
const nl = text.indexOf('\n', i);
|
|
99
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
100
|
+
const line = text.slice(i, lineEnd);
|
|
101
|
+
if (scissorsRe.test(line)) {
|
|
102
|
+
ranges.push([i, text.length]);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith('#')) {
|
|
106
|
+
ranges.push([i, nl === -1 ? text.length : nl + 1]);
|
|
107
|
+
}
|
|
108
|
+
if (nl === -1)
|
|
109
|
+
break;
|
|
110
|
+
i = nl + 1;
|
|
111
|
+
}
|
|
112
|
+
return mergeRanges(ranges);
|
|
113
|
+
}
|
|
114
|
+
function invertRanges(ranges, textLen) {
|
|
115
|
+
const inverted = [];
|
|
116
|
+
let cursor = 0;
|
|
117
|
+
for (const [s, e] of ranges) {
|
|
118
|
+
if (s > cursor)
|
|
119
|
+
inverted.push([cursor, s]);
|
|
120
|
+
cursor = Math.max(cursor, e);
|
|
121
|
+
}
|
|
122
|
+
if (cursor < textLen)
|
|
123
|
+
inverted.push([cursor, textLen]);
|
|
124
|
+
return inverted;
|
|
125
|
+
}
|
|
126
|
+
function mergeRanges(ranges) {
|
|
127
|
+
if (ranges.length === 0)
|
|
128
|
+
return ranges;
|
|
129
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
130
|
+
const merged = [[ranges[0][0], ranges[0][1]]];
|
|
131
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
132
|
+
const last = merged[merged.length - 1];
|
|
133
|
+
const curr = ranges[i];
|
|
134
|
+
if (curr[0] <= last[1]) {
|
|
135
|
+
last[1] = Math.max(last[1], curr[1]);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
merged.push([curr[0], curr[1]]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return merged;
|
|
142
|
+
}
|
|
143
|
+
function offsetInRanges(offset, ranges) {
|
|
144
|
+
let lo = 0, hi = ranges.length - 1;
|
|
145
|
+
while (lo <= hi) {
|
|
146
|
+
const mid = (lo + hi) >> 1;
|
|
147
|
+
const [s, e] = ranges[mid];
|
|
148
|
+
if (offset < s)
|
|
149
|
+
hi = mid - 1;
|
|
150
|
+
else if (offset >= e)
|
|
151
|
+
lo = mid + 1;
|
|
152
|
+
else
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
// Line-based scan for fenced code blocks and YAML frontmatter, plus regex for
|
|
158
|
+
// inline code spans and link URLs. Good enough for the 95% case without
|
|
159
|
+
// pulling in a CommonMark parser.
|
|
160
|
+
function computeMarkdownExclusions(text) {
|
|
161
|
+
const ranges = [];
|
|
162
|
+
let i = 0;
|
|
163
|
+
let lineIdx = 0;
|
|
164
|
+
let inFence = false;
|
|
165
|
+
let fenceChar = '';
|
|
166
|
+
let fenceLen = 0;
|
|
167
|
+
let fenceStart = 0;
|
|
168
|
+
let inFrontmatter = false;
|
|
169
|
+
let frontmatterStart = 0;
|
|
170
|
+
while (i <= text.length) {
|
|
171
|
+
const nl = text.indexOf('\n', i);
|
|
172
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
173
|
+
const line = text.slice(i, lineEnd);
|
|
174
|
+
const nextLineStart = nl === -1 ? text.length : nl + 1;
|
|
175
|
+
if (!inFence) {
|
|
176
|
+
if (lineIdx === 0 && line === '---') {
|
|
177
|
+
inFrontmatter = true;
|
|
178
|
+
frontmatterStart = i;
|
|
179
|
+
}
|
|
180
|
+
else if (inFrontmatter && (line === '---' || line === '...')) {
|
|
181
|
+
ranges.push([frontmatterStart, nextLineStart]);
|
|
182
|
+
inFrontmatter = false;
|
|
183
|
+
}
|
|
184
|
+
else if (!inFrontmatter) {
|
|
185
|
+
const m = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
186
|
+
if (m) {
|
|
187
|
+
inFence = true;
|
|
188
|
+
fenceChar = m[1][0];
|
|
189
|
+
fenceLen = m[1].length;
|
|
190
|
+
fenceStart = i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const closer = new RegExp('^ {0,3}' + (fenceChar === '`' ? '`' : '~') + '{' + fenceLen + ',}\\s*$');
|
|
196
|
+
if (closer.test(line)) {
|
|
197
|
+
ranges.push([fenceStart, nextLineStart]);
|
|
198
|
+
inFence = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (nl === -1)
|
|
202
|
+
break;
|
|
203
|
+
lineIdx++;
|
|
204
|
+
i = nextLineStart;
|
|
205
|
+
}
|
|
206
|
+
if (inFence)
|
|
207
|
+
ranges.push([fenceStart, text.length]);
|
|
208
|
+
let m;
|
|
209
|
+
const inlineCodeRe = /`[^`\n]+`/g;
|
|
210
|
+
while ((m = inlineCodeRe.exec(text)) !== null) {
|
|
211
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
212
|
+
}
|
|
213
|
+
const linkRe = /\[[^\]\n]*\]\(([^)\n]+)\)/g;
|
|
214
|
+
while ((m = linkRe.exec(text)) !== null) {
|
|
215
|
+
const parenOpen = m.index + m[0].lastIndexOf('(');
|
|
216
|
+
const parenClose = m.index + m[0].length - 1;
|
|
217
|
+
ranges.push([parenOpen + 1, parenClose]);
|
|
218
|
+
}
|
|
219
|
+
const autolinkRe = /<https?:\/\/[^>\s]+>/gi;
|
|
220
|
+
while ((m = autolinkRe.exec(text)) !== null) {
|
|
221
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
222
|
+
}
|
|
223
|
+
return mergeRanges(ranges);
|
|
224
|
+
}
|
|
225
|
+
function parseSuppressionSpecs(raw) {
|
|
226
|
+
const specs = raw.trim().split(/\s+/).filter(Boolean);
|
|
227
|
+
if (specs.length === 0)
|
|
228
|
+
return () => true;
|
|
229
|
+
const phraseSpecs = [];
|
|
230
|
+
const charSpecs = [];
|
|
231
|
+
for (const s of specs) {
|
|
232
|
+
if (s.startsWith('phrase:'))
|
|
233
|
+
phraseSpecs.push(s.slice('phrase:'.length));
|
|
234
|
+
else if (s.startsWith('char:'))
|
|
235
|
+
charSpecs.push(s.slice('char:'.length));
|
|
236
|
+
}
|
|
237
|
+
return (code, matchText, rulePattern) => {
|
|
238
|
+
if (code === 'phrase') {
|
|
239
|
+
return phraseSpecs.some(p => rulePattern === p);
|
|
240
|
+
}
|
|
241
|
+
return charSpecs.some(c => {
|
|
242
|
+
if (/^u\+/i.test(c)) {
|
|
243
|
+
const cp = parseInt(c.slice(2), 16);
|
|
244
|
+
return !Number.isNaN(cp) && matchText.codePointAt(0) === cp;
|
|
245
|
+
}
|
|
246
|
+
return matchText === c;
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function computeSuppressions(text, excluded) {
|
|
251
|
+
const directiveRe = /<!--\s*slop-(disable-next-line|disable-line|disable|enable)\b([^>]*?)-->/gi;
|
|
252
|
+
const directives = [];
|
|
253
|
+
let m;
|
|
254
|
+
while ((m = directiveRe.exec(text)) !== null) {
|
|
255
|
+
if (offsetInRanges(m.index, excluded))
|
|
256
|
+
continue;
|
|
257
|
+
directives.push({
|
|
258
|
+
kind: m[1].toLowerCase(),
|
|
259
|
+
applies: parseSuppressionSpecs(m[2] || ''),
|
|
260
|
+
start: m.index,
|
|
261
|
+
end: m.index + m[0].length,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const result = [];
|
|
265
|
+
let blockStart = null;
|
|
266
|
+
let blockApplies = null;
|
|
267
|
+
for (const d of directives) {
|
|
268
|
+
if (d.kind === 'disable') {
|
|
269
|
+
if (blockStart === null) {
|
|
270
|
+
blockStart = d.end;
|
|
271
|
+
blockApplies = d.applies;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (d.kind === 'enable') {
|
|
275
|
+
if (blockStart !== null && blockApplies !== null) {
|
|
276
|
+
result.push({ start: blockStart, end: d.start, applies: blockApplies });
|
|
277
|
+
blockStart = null;
|
|
278
|
+
blockApplies = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (d.kind === 'disable-line') {
|
|
282
|
+
const lineStart = text.lastIndexOf('\n', d.start - 1) + 1;
|
|
283
|
+
const nl = text.indexOf('\n', d.end);
|
|
284
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
285
|
+
result.push({ start: lineStart, end: lineEnd, applies: d.applies });
|
|
286
|
+
}
|
|
287
|
+
else if (d.kind === 'disable-next-line') {
|
|
288
|
+
const nl = text.indexOf('\n', d.end);
|
|
289
|
+
if (nl === -1)
|
|
290
|
+
continue;
|
|
291
|
+
const nextLineStart = nl + 1;
|
|
292
|
+
const nextNl = text.indexOf('\n', nextLineStart);
|
|
293
|
+
const nextLineEnd = nextNl === -1 ? text.length : nextNl;
|
|
294
|
+
result.push({ start: nextLineStart, end: nextLineEnd, applies: d.applies });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (blockStart !== null && blockApplies !== null) {
|
|
298
|
+
result.push({ start: blockStart, end: text.length, applies: blockApplies });
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
function isSuppressed(offset, suppressions, code, matchText, rulePattern) {
|
|
303
|
+
for (const s of suppressions) {
|
|
304
|
+
if (offset >= s.start && offset < s.end && s.applies(code, matchText, rulePattern)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
function offsetToLineCol(text, offset) {
|
|
311
|
+
let line = 1;
|
|
312
|
+
let lastNl = -1;
|
|
313
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
314
|
+
if (text.charCodeAt(i) === 10) {
|
|
315
|
+
line++;
|
|
316
|
+
lastNl = i;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { line, col: offset - lastNl };
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=scan.js.map
|