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/extension.js
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.activate = activate;
|
|
4
|
+
exports.deactivate = deactivate;
|
|
5
|
+
const vscode = require("vscode");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const fsp = require("fs/promises");
|
|
9
|
+
const rules_1 = require("./rules");
|
|
10
|
+
const scan_1 = require("./core/scan");
|
|
11
|
+
const comments_1 = require("./core/comments");
|
|
12
|
+
const ignore_1 = require("./core/ignore");
|
|
13
|
+
const SOURCE = 'LLM Slop';
|
|
14
|
+
const DOCS_URI = vscode.Uri.parse('https://github.com/mandakan/llm-slop-detector#what-it-flags');
|
|
15
|
+
const BASE_LANGS = ['markdown', 'plaintext'];
|
|
16
|
+
let SUPPORTED_LANGS = new Set(BASE_LANGS);
|
|
17
|
+
const CODE_ACTION_SELECTORS = [{ scheme: 'file' }, { scheme: 'untitled' }];
|
|
18
|
+
// File-extension to language mapping for closed files. Mirrors the CLI; the
|
|
19
|
+
// extension uses VS Code's language IDs for open documents but needs an
|
|
20
|
+
// extension-based fallback for the workspace scan, which reads files off disk.
|
|
21
|
+
const PROSE_EXTENSIONS = new Map([
|
|
22
|
+
['.md', 'markdown'],
|
|
23
|
+
['.markdown', 'markdown'],
|
|
24
|
+
['.mdown', 'markdown'],
|
|
25
|
+
['.txt', 'plaintext'],
|
|
26
|
+
['.text', 'plaintext'],
|
|
27
|
+
]);
|
|
28
|
+
const CODE_EXTENSIONS = new Map([
|
|
29
|
+
['.ts', 'typescript'], ['.mts', 'typescript'], ['.cts', 'typescript'],
|
|
30
|
+
['.tsx', 'typescriptreact'],
|
|
31
|
+
['.js', 'javascript'], ['.mjs', 'javascript'], ['.cjs', 'javascript'],
|
|
32
|
+
['.jsx', 'javascriptreact'],
|
|
33
|
+
['.py', 'python'],
|
|
34
|
+
['.rs', 'rust'],
|
|
35
|
+
['.go', 'go'],
|
|
36
|
+
['.java', 'java'],
|
|
37
|
+
['.cs', 'csharp'],
|
|
38
|
+
['.cpp', 'cpp'], ['.cxx', 'cpp'], ['.cc', 'cpp'], ['.hpp', 'cpp'], ['.hxx', 'cpp'],
|
|
39
|
+
['.c', 'c'], ['.h', 'c'],
|
|
40
|
+
['.rb', 'ruby'],
|
|
41
|
+
['.php', 'php'],
|
|
42
|
+
['.sh', 'shellscript'], ['.bash', 'shellscript'], ['.zsh', 'shellscript'],
|
|
43
|
+
['.swift', 'swift'],
|
|
44
|
+
['.kt', 'kotlin'], ['.kts', 'kotlin'],
|
|
45
|
+
['.scala', 'scala'], ['.sc', 'scala'],
|
|
46
|
+
['.dart', 'dart'],
|
|
47
|
+
['.pl', 'perl'], ['.pm', 'perl'],
|
|
48
|
+
['.r', 'r'],
|
|
49
|
+
['.yaml', 'yaml'], ['.yml', 'yaml'],
|
|
50
|
+
]);
|
|
51
|
+
function rebuildSupportedLangs() {
|
|
52
|
+
const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
|
|
53
|
+
const scanComments = cfg.get('scanCodeComments', false);
|
|
54
|
+
const codeLangs = cfg.get('codeCommentLanguages', []);
|
|
55
|
+
const scanCommitMessages = cfg.get('scanCommitMessages', true);
|
|
56
|
+
const allowed = new Set(comments_1.SUPPORTED_CODE_LANGUAGES);
|
|
57
|
+
SUPPORTED_LANGS = new Set([
|
|
58
|
+
...BASE_LANGS,
|
|
59
|
+
...(scanComments ? codeLangs.filter(l => allowed.has(l)) : []),
|
|
60
|
+
...(scanCommitMessages ? ['git-commit', 'scminput'] : []),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
// Module-level mutable rule state. Rebuilt on config change / rule-file change
|
|
64
|
+
// and scans read through it.
|
|
65
|
+
let RULES = { chars: new Map(), phrases: [], sources: [], charRegex: /(?!)/g, overridesApplied: 0 };
|
|
66
|
+
// One ignore matcher per workspace folder. Rebuilt on config change or when a
|
|
67
|
+
// .slopignore file is created/changed/deleted. Untitled and out-of-workspace
|
|
68
|
+
// docs bypass ignore filtering.
|
|
69
|
+
let IGNORE_BY_FOLDER = new Map();
|
|
70
|
+
function isIgnoredDocument(doc) {
|
|
71
|
+
if (doc.uri.scheme !== 'file')
|
|
72
|
+
return false;
|
|
73
|
+
const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
|
|
74
|
+
if (!folder)
|
|
75
|
+
return false;
|
|
76
|
+
const matcher = IGNORE_BY_FOLDER.get(folder.uri.fsPath);
|
|
77
|
+
if (!matcher || matcher.patterns.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
const rel = path.relative(folder.uri.fsPath, doc.uri.fsPath);
|
|
80
|
+
if (rel.startsWith('..'))
|
|
81
|
+
return false;
|
|
82
|
+
return matcher.ignores(rel);
|
|
83
|
+
}
|
|
84
|
+
// Findings keyed by document URI, stashed during scan so the hover provider
|
|
85
|
+
// can recover rule metadata (pattern, matched char) without rescanning.
|
|
86
|
+
const FINDINGS_BY_URI = new Map();
|
|
87
|
+
const PENDING_REFRESH = new Map();
|
|
88
|
+
function cancelPendingRefresh(uriKey) {
|
|
89
|
+
const p = PENDING_REFRESH.get(uriKey);
|
|
90
|
+
if (p !== undefined) {
|
|
91
|
+
clearTimeout(p.timer);
|
|
92
|
+
PENDING_REFRESH.delete(uriKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function getReplacement(char) {
|
|
96
|
+
return RULES.chars.get(char)?.replacement;
|
|
97
|
+
}
|
|
98
|
+
function diagnosticCode(d) {
|
|
99
|
+
const c = d.code;
|
|
100
|
+
if (typeof c === 'object' && c !== null)
|
|
101
|
+
return c.value;
|
|
102
|
+
return c;
|
|
103
|
+
}
|
|
104
|
+
function scanDocument(doc) {
|
|
105
|
+
const lang = doc.languageId;
|
|
106
|
+
if (!SUPPORTED_LANGS.has(lang)) {
|
|
107
|
+
FINDINGS_BY_URI.delete(doc.uri.toString());
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const findings = (0, scan_1.scanText)(doc.getText(), RULES, lang);
|
|
111
|
+
FINDINGS_BY_URI.set(doc.uri.toString(), findings);
|
|
112
|
+
return findings.map(f => {
|
|
113
|
+
const start = doc.positionAt(f.offset);
|
|
114
|
+
const end = doc.positionAt(f.offset + f.length);
|
|
115
|
+
const d = new vscode.Diagnostic(new vscode.Range(start, end), f.message, (0, rules_1.severityToVscode)(f.severity));
|
|
116
|
+
d.source = SOURCE;
|
|
117
|
+
d.code = { value: f.code, target: DOCS_URI };
|
|
118
|
+
return d;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Code actions (quick fixes)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
class SlopCodeActionProvider {
|
|
125
|
+
static providedCodeActionKinds = [vscode.CodeActionKind.QuickFix];
|
|
126
|
+
provideCodeActions(document, _range, context) {
|
|
127
|
+
const actions = [];
|
|
128
|
+
for (const diag of context.diagnostics) {
|
|
129
|
+
if (diag.source !== SOURCE || diagnosticCode(diag) !== 'char')
|
|
130
|
+
continue;
|
|
131
|
+
const char = document.getText(diag.range);
|
|
132
|
+
const replacement = getReplacement(char);
|
|
133
|
+
if (replacement === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
const def = RULES.chars.get(char);
|
|
136
|
+
const title = replacement === ''
|
|
137
|
+
? `Delete ${def?.name ?? 'character'}`
|
|
138
|
+
: replacement === '\n'
|
|
139
|
+
? `Replace ${def?.name ?? 'character'} with newline`
|
|
140
|
+
: `Replace ${def?.name ?? 'character'} with ${JSON.stringify(replacement)}`;
|
|
141
|
+
const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix);
|
|
142
|
+
action.edit = new vscode.WorkspaceEdit();
|
|
143
|
+
action.edit.replace(document.uri, diag.range, replacement);
|
|
144
|
+
action.diagnostics = [diag];
|
|
145
|
+
action.isPreferred = true;
|
|
146
|
+
actions.push(action);
|
|
147
|
+
}
|
|
148
|
+
const contextHasFixableChar = context.diagnostics.some(d => d.source === SOURCE && diagnosticCode(d) === 'char' &&
|
|
149
|
+
getReplacement(document.getText(d.range)) !== undefined);
|
|
150
|
+
const fixable = vscode.languages.getDiagnostics(document.uri)
|
|
151
|
+
.filter(d => d.source === SOURCE && diagnosticCode(d) === 'char')
|
|
152
|
+
.filter(d => getReplacement(document.getText(d.range)) !== undefined);
|
|
153
|
+
if (contextHasFixableChar && fixable.length > 0) {
|
|
154
|
+
const fixAll = new vscode.CodeAction(`Fix all LLM slop characters in file (${fixable.length})`, vscode.CodeActionKind.QuickFix);
|
|
155
|
+
fixAll.edit = new vscode.WorkspaceEdit();
|
|
156
|
+
for (const d of fixable) {
|
|
157
|
+
const c = document.getText(d.range);
|
|
158
|
+
const r = getReplacement(c);
|
|
159
|
+
if (r !== undefined)
|
|
160
|
+
fixAll.edit.replace(document.uri, d.range, r);
|
|
161
|
+
}
|
|
162
|
+
fixAll.diagnostics = fixable;
|
|
163
|
+
actions.push(fixAll);
|
|
164
|
+
}
|
|
165
|
+
return actions;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Hover provider: show rule metadata + ready-to-copy ignore snippet
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
function charCodepointSpec(char) {
|
|
172
|
+
return 'U+' + char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0');
|
|
173
|
+
}
|
|
174
|
+
function ignoreSpecFor(f) {
|
|
175
|
+
return f.code === 'phrase' && f.rulePattern !== undefined
|
|
176
|
+
? `phrase:${f.rulePattern}`
|
|
177
|
+
: `char:${charCodepointSpec(f.matchText)}`;
|
|
178
|
+
}
|
|
179
|
+
class SlopHoverProvider {
|
|
180
|
+
provideHover(document, position) {
|
|
181
|
+
const findings = FINDINGS_BY_URI.get(document.uri.toString());
|
|
182
|
+
if (!findings || findings.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
const offset = document.offsetAt(position);
|
|
185
|
+
const matched = findings.filter(f => offset >= f.offset && offset < f.offset + f.length);
|
|
186
|
+
if (matched.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
const blocks = matched.map(f => {
|
|
189
|
+
const spec = ignoreSpecFor(f);
|
|
190
|
+
const heading = f.code === 'phrase' ? 'LLM-style phrase' : 'Flagged character';
|
|
191
|
+
const lines = [
|
|
192
|
+
`**${heading}** -- \`${f.source}\``,
|
|
193
|
+
'',
|
|
194
|
+
`Rule selector: \`${spec}\``,
|
|
195
|
+
'',
|
|
196
|
+
'Suppress the next line:',
|
|
197
|
+
'```markdown',
|
|
198
|
+
`<!-- slop-disable-next-line ${spec} -->`,
|
|
199
|
+
'```',
|
|
200
|
+
];
|
|
201
|
+
return lines.join('\n');
|
|
202
|
+
});
|
|
203
|
+
const md = new vscode.MarkdownString(blocks.join('\n\n---\n\n'));
|
|
204
|
+
md.isTrusted = false;
|
|
205
|
+
md.supportHtml = false;
|
|
206
|
+
return new vscode.Hover(md);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Activation
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
function activate(context) {
|
|
213
|
+
const collection = vscode.languages.createDiagnosticCollection('llmSlopDetector');
|
|
214
|
+
context.subscriptions.push(collection);
|
|
215
|
+
const refresh = (doc) => {
|
|
216
|
+
const enabled = vscode.workspace.getConfiguration('llmSlopDetector').get('enabled', true);
|
|
217
|
+
if (!enabled || !SUPPORTED_LANGS.has(doc.languageId) || isIgnoredDocument(doc)) {
|
|
218
|
+
collection.delete(doc.uri);
|
|
219
|
+
FINDINGS_BY_URI.delete(doc.uri.toString());
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
collection.set(doc.uri, scanDocument(doc));
|
|
223
|
+
};
|
|
224
|
+
// Leading+trailing debounce: first change after idle triggers an immediate
|
|
225
|
+
// scan so feedback stays snappy; subsequent changes within the window
|
|
226
|
+
// collapse into one trailing scan when the timer fires.
|
|
227
|
+
const scheduleRefresh = (doc) => {
|
|
228
|
+
const key = doc.uri.toString();
|
|
229
|
+
const raw = vscode.workspace.getConfiguration('llmSlopDetector').get('debounceMs', 150);
|
|
230
|
+
const ms = Number.isFinite(raw) ? Math.max(0, Math.min(2000, raw)) : 150;
|
|
231
|
+
if (ms === 0) {
|
|
232
|
+
cancelPendingRefresh(key);
|
|
233
|
+
refresh(doc);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const existing = PENDING_REFRESH.get(key);
|
|
237
|
+
if (existing !== undefined) {
|
|
238
|
+
existing.trailing = true;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
refresh(doc);
|
|
242
|
+
const entry = { timer: undefined, trailing: false };
|
|
243
|
+
entry.timer = setTimeout(() => {
|
|
244
|
+
const current = PENDING_REFRESH.get(key);
|
|
245
|
+
PENDING_REFRESH.delete(key);
|
|
246
|
+
if (current?.trailing)
|
|
247
|
+
refresh(doc);
|
|
248
|
+
}, ms);
|
|
249
|
+
PENDING_REFRESH.set(key, entry);
|
|
250
|
+
};
|
|
251
|
+
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
|
252
|
+
status.command = 'llmSlopDetector.toggle';
|
|
253
|
+
context.subscriptions.push(status);
|
|
254
|
+
const updateStatus = () => {
|
|
255
|
+
const editor = vscode.window.activeTextEditor;
|
|
256
|
+
if (!editor || !SUPPORTED_LANGS.has(editor.document.languageId) || isIgnoredDocument(editor.document)) {
|
|
257
|
+
status.hide();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const enabled = vscode.workspace.getConfiguration('llmSlopDetector').get('enabled', true);
|
|
261
|
+
if (!enabled) {
|
|
262
|
+
status.text = '$(circle-slash) Slop off';
|
|
263
|
+
status.tooltip = 'LLM Slop Detector is disabled. Click to enable.';
|
|
264
|
+
status.backgroundColor = undefined;
|
|
265
|
+
status.show();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const diags = vscode.languages.getDiagnostics(editor.document.uri)
|
|
269
|
+
.filter(d => d.source === SOURCE);
|
|
270
|
+
const chars = diags.filter(d => diagnosticCode(d) === 'char').length;
|
|
271
|
+
const phrases = diags.filter(d => diagnosticCode(d) === 'phrase').length;
|
|
272
|
+
const total = chars + phrases;
|
|
273
|
+
if (total === 0) {
|
|
274
|
+
status.text = '$(check) No slop';
|
|
275
|
+
status.tooltip = 'LLM Slop Detector: no issues in this file. Click to disable.';
|
|
276
|
+
status.backgroundColor = undefined;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
status.text = `$(warning) ${total} slop`;
|
|
280
|
+
const charPart = `${chars} character${chars === 1 ? '' : 's'}`;
|
|
281
|
+
const phrasePart = `${phrases} phrase${phrases === 1 ? '' : 's'}`;
|
|
282
|
+
status.tooltip = `LLM Slop Detector: ${charPart}, ${phrasePart}. Click to disable.`;
|
|
283
|
+
status.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
|
284
|
+
}
|
|
285
|
+
status.show();
|
|
286
|
+
};
|
|
287
|
+
const reloadIgnore = () => {
|
|
288
|
+
const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
|
|
289
|
+
const extra = cfg.get('exclude', []);
|
|
290
|
+
const next = new Map();
|
|
291
|
+
for (const folder of vscode.workspace.workspaceFolders ?? []) {
|
|
292
|
+
next.set(folder.uri.fsPath, (0, ignore_1.loadIgnoreMatcher)(folder.uri.fsPath, extra));
|
|
293
|
+
}
|
|
294
|
+
IGNORE_BY_FOLDER = next;
|
|
295
|
+
};
|
|
296
|
+
const reloadRules = () => {
|
|
297
|
+
RULES = (0, rules_1.loadRules)(context.extensionUri);
|
|
298
|
+
reloadIgnore();
|
|
299
|
+
rebuildSupportedLangs();
|
|
300
|
+
vscode.workspace.textDocuments.forEach(refresh);
|
|
301
|
+
updateStatus();
|
|
302
|
+
};
|
|
303
|
+
reloadRules();
|
|
304
|
+
// Live-reload when a local .llmsloprc.json is created/changed/deleted
|
|
305
|
+
// anywhere in the workspace. The loader itself only reads the files at
|
|
306
|
+
// workspace roots; nested matches just trigger a harmless reload.
|
|
307
|
+
const watcher = vscode.workspace.createFileSystemWatcher(`**/${rules_1.LOCAL_RULES_FILENAME}`);
|
|
308
|
+
const ignoreWatcher = vscode.workspace.createFileSystemWatcher(`**/${ignore_1.SLOPIGNORE_FILENAME}`);
|
|
309
|
+
context.subscriptions.push(watcher, watcher.onDidChange(reloadRules), watcher.onDidCreate(reloadRules), watcher.onDidDelete(reloadRules), ignoreWatcher, ignoreWatcher.onDidChange(reloadRules), ignoreWatcher.onDidCreate(reloadRules), ignoreWatcher.onDidDelete(reloadRules));
|
|
310
|
+
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(doc => { refresh(doc); updateStatus(); }), vscode.workspace.onDidChangeTextDocument(e => { scheduleRefresh(e.document); }), vscode.workspace.onDidCloseTextDocument(doc => {
|
|
311
|
+
const key = doc.uri.toString();
|
|
312
|
+
cancelPendingRefresh(key);
|
|
313
|
+
collection.delete(doc.uri);
|
|
314
|
+
FINDINGS_BY_URI.delete(key);
|
|
315
|
+
updateStatus();
|
|
316
|
+
}), vscode.workspace.onDidChangeWorkspaceFolders(reloadRules), vscode.workspace.onDidGrantWorkspaceTrust(reloadRules), vscode.window.onDidChangeActiveTextEditor(() => updateStatus()), vscode.languages.onDidChangeDiagnostics(() => updateStatus()), vscode.workspace.onDidChangeConfiguration(e => {
|
|
317
|
+
if (e.affectsConfiguration('llmSlopDetector'))
|
|
318
|
+
reloadRules();
|
|
319
|
+
}), vscode.languages.registerCodeActionsProvider(CODE_ACTION_SELECTORS, new SlopCodeActionProvider(), { providedCodeActionKinds: SlopCodeActionProvider.providedCodeActionKinds }), vscode.languages.registerHoverProvider(CODE_ACTION_SELECTORS, new SlopHoverProvider()), vscode.commands.registerCommand('llmSlopDetector.toggle', async () => {
|
|
320
|
+
const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
|
|
321
|
+
const current = cfg.get('enabled', true);
|
|
322
|
+
await cfg.update('enabled', !current, vscode.ConfigurationTarget.Global);
|
|
323
|
+
vscode.window.showInformationMessage(`LLM Slop Detector ${!current ? 'enabled' : 'disabled'}`);
|
|
324
|
+
}), vscode.commands.registerCommand('llmSlopDetector.openSettings', async () => {
|
|
325
|
+
await vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${context.extension.id}`);
|
|
326
|
+
}), vscode.commands.registerCommand('llmSlopDetector.showOnboarding', () => showOnboarding(context)), vscode.commands.registerCommand('llmSlopDetector.scanSelection', () => scanSelection()), vscode.commands.registerCommand('llmSlopDetector.scanWorkspace', () => scanWorkspace()), vscode.commands.registerCommand('llmSlopDetector.showRuleSources', async () => {
|
|
327
|
+
if (RULES.sources.length === 0) {
|
|
328
|
+
vscode.window.showInformationMessage('LLM Slop Detector: no rule sources loaded.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const items = RULES.sources.map(s => ({
|
|
332
|
+
label: `$(list-unordered) ${s.name}${s.version ? ` v${s.version}` : ''}`,
|
|
333
|
+
description: `${s.charCount} char${s.charCount === 1 ? '' : 's'}, ${s.phraseCount} phrase${s.phraseCount === 1 ? '' : 's'}`,
|
|
334
|
+
detail: s.description ? `${s.description} (${s.origin})` : s.origin,
|
|
335
|
+
}));
|
|
336
|
+
if (RULES.overridesApplied > 0) {
|
|
337
|
+
items.push({
|
|
338
|
+
label: `$(settings-gear) ${RULES.overridesApplied} severity override${RULES.overridesApplied === 1 ? '' : 's'} applied`,
|
|
339
|
+
description: 'via llmSlopDetector.severityOverrides',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
await vscode.window.showQuickPick(items, { title: 'LLM Slop Detector: loaded rule sources' });
|
|
343
|
+
}));
|
|
344
|
+
void maybeShowOnboarding(context);
|
|
345
|
+
}
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Scan selection
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
function severityCodicon(s) {
|
|
350
|
+
switch (s) {
|
|
351
|
+
case vscode.DiagnosticSeverity.Error: return 'error';
|
|
352
|
+
case vscode.DiagnosticSeverity.Warning: return 'warning';
|
|
353
|
+
case vscode.DiagnosticSeverity.Information: return 'info';
|
|
354
|
+
case vscode.DiagnosticSeverity.Hint: return 'lightbulb';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function scanSelection() {
|
|
358
|
+
const editor = vscode.window.activeTextEditor;
|
|
359
|
+
if (!editor) {
|
|
360
|
+
vscode.window.showInformationMessage('LLM Slop Detector: no active editor.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const doc = editor.document;
|
|
364
|
+
if (!SUPPORTED_LANGS.has(doc.languageId)) {
|
|
365
|
+
vscode.window.showInformationMessage(`LLM Slop Detector: ${doc.languageId} is not a scanned language.`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const sel = editor.selection;
|
|
369
|
+
const scope = sel.isEmpty ? doc.lineAt(sel.start).range : new vscode.Range(sel.start, sel.end);
|
|
370
|
+
const diags = vscode.languages.getDiagnostics(doc.uri)
|
|
371
|
+
.filter(d => d.source === SOURCE && scope.intersection(d.range))
|
|
372
|
+
.sort((a, b) => a.range.start.compareTo(b.range.start));
|
|
373
|
+
if (diags.length === 0) {
|
|
374
|
+
vscode.window.showInformationMessage(sel.isEmpty
|
|
375
|
+
? 'LLM Slop Detector: no findings on this line.'
|
|
376
|
+
: 'LLM Slop Detector: no findings in selection.');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const items = diags.map(d => ({
|
|
380
|
+
label: `$(${severityCodicon(d.severity)}) ${doc.getText(d.range).trim() || String(d.code)}`,
|
|
381
|
+
description: `Line ${d.range.start.line + 1}, col ${d.range.start.character + 1}`,
|
|
382
|
+
detail: d.message,
|
|
383
|
+
diagnostic: d,
|
|
384
|
+
}));
|
|
385
|
+
const pick = await vscode.window.showQuickPick(items, {
|
|
386
|
+
title: `LLM Slop in ${sel.isEmpty ? 'line' : 'selection'} (${diags.length} finding${diags.length === 1 ? '' : 's'})`,
|
|
387
|
+
matchOnDescription: true,
|
|
388
|
+
matchOnDetail: true,
|
|
389
|
+
});
|
|
390
|
+
if (pick) {
|
|
391
|
+
editor.revealRange(pick.diagnostic.range, vscode.TextEditorRevealType.InCenter);
|
|
392
|
+
editor.selection = new vscode.Selection(pick.diagnostic.range.start, pick.diagnostic.range.end);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function buildScanExtensionMap() {
|
|
396
|
+
const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
|
|
397
|
+
const out = new Map(PROSE_EXTENSIONS);
|
|
398
|
+
if (cfg.get('scanCodeComments', false)) {
|
|
399
|
+
const allowed = new Set(comments_1.SUPPORTED_CODE_LANGUAGES);
|
|
400
|
+
const enabled = new Set(cfg.get('codeCommentLanguages', []).filter(l => allowed.has(l)));
|
|
401
|
+
for (const [ext, lang] of CODE_EXTENSIONS) {
|
|
402
|
+
if (enabled.has(lang))
|
|
403
|
+
out.set(ext, lang);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
function walkWorkspaceFolder(rootDir, currentDir, extensions, ignore, push, token) {
|
|
409
|
+
if (token.isCancellationRequested)
|
|
410
|
+
return;
|
|
411
|
+
let entries;
|
|
412
|
+
try {
|
|
413
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const e of entries) {
|
|
419
|
+
if (token.isCancellationRequested)
|
|
420
|
+
return;
|
|
421
|
+
// Same hard-coded skips as the CLI walker: dotfiles, node_modules, build out.
|
|
422
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'out')
|
|
423
|
+
continue;
|
|
424
|
+
const full = path.join(currentDir, e.name);
|
|
425
|
+
const rel = path.relative(rootDir, full);
|
|
426
|
+
if (e.isDirectory()) {
|
|
427
|
+
if (ignore && ignore.patterns.length > 0 && ignore.ignores(rel, true))
|
|
428
|
+
continue;
|
|
429
|
+
walkWorkspaceFolder(rootDir, full, extensions, ignore, push, token);
|
|
430
|
+
}
|
|
431
|
+
else if (e.isFile()) {
|
|
432
|
+
const lang = extensions.get(path.extname(e.name).toLowerCase());
|
|
433
|
+
if (lang === undefined)
|
|
434
|
+
continue;
|
|
435
|
+
if (ignore && ignore.patterns.length > 0 && ignore.ignores(rel, false))
|
|
436
|
+
continue;
|
|
437
|
+
push(full, lang);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function scanWorkspace() {
|
|
442
|
+
const folders = vscode.workspace.workspaceFolders ?? [];
|
|
443
|
+
if (folders.length === 0) {
|
|
444
|
+
vscode.window.showInformationMessage('LLM Slop Detector: open a folder to scan the workspace.');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
|
|
448
|
+
if (!cfg.get('enabled', true)) {
|
|
449
|
+
vscode.window.showInformationMessage('LLM Slop Detector is disabled. Enable it before scanning.');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const extensions = buildScanExtensionMap();
|
|
453
|
+
const hits = await vscode.window.withProgress({
|
|
454
|
+
location: vscode.ProgressLocation.Notification,
|
|
455
|
+
title: 'LLM Slop Detector: scanning workspace',
|
|
456
|
+
cancellable: true,
|
|
457
|
+
}, async (progress, token) => {
|
|
458
|
+
const targets = [];
|
|
459
|
+
for (const folder of folders) {
|
|
460
|
+
if (token.isCancellationRequested)
|
|
461
|
+
return undefined;
|
|
462
|
+
const matcher = IGNORE_BY_FOLDER.get(folder.uri.fsPath);
|
|
463
|
+
walkWorkspaceFolder(folder.uri.fsPath, folder.uri.fsPath, extensions, matcher, (absPath, lang) => {
|
|
464
|
+
targets.push({
|
|
465
|
+
absPath,
|
|
466
|
+
uri: vscode.Uri.file(absPath),
|
|
467
|
+
lang,
|
|
468
|
+
relPath: path.relative(folder.uri.fsPath, absPath),
|
|
469
|
+
});
|
|
470
|
+
}, token);
|
|
471
|
+
}
|
|
472
|
+
if (token.isCancellationRequested)
|
|
473
|
+
return undefined;
|
|
474
|
+
if (targets.length === 0)
|
|
475
|
+
return [];
|
|
476
|
+
// Use the in-memory text of any open document so unsaved changes are
|
|
477
|
+
// reflected in the scan, falling back to the on-disk version otherwise.
|
|
478
|
+
const openText = new Map();
|
|
479
|
+
for (const d of vscode.workspace.textDocuments) {
|
|
480
|
+
if (d.uri.scheme === 'file')
|
|
481
|
+
openText.set(d.uri.fsPath, d.getText());
|
|
482
|
+
}
|
|
483
|
+
progress.report({ message: `${targets.length} file${targets.length === 1 ? '' : 's'}` });
|
|
484
|
+
const out = [];
|
|
485
|
+
let next = 0;
|
|
486
|
+
let done = 0;
|
|
487
|
+
const total = targets.length;
|
|
488
|
+
const concurrency = Math.min(8, total);
|
|
489
|
+
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
490
|
+
while (next < total) {
|
|
491
|
+
if (token.isCancellationRequested)
|
|
492
|
+
return;
|
|
493
|
+
const t = targets[next++];
|
|
494
|
+
let text;
|
|
495
|
+
const open = openText.get(t.absPath);
|
|
496
|
+
if (open !== undefined) {
|
|
497
|
+
text = open;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
try {
|
|
501
|
+
text = await fsp.readFile(t.absPath, 'utf8');
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
done++;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const findings = (0, scan_1.scanText)(text, RULES, t.lang);
|
|
509
|
+
for (const f of findings) {
|
|
510
|
+
const lc = (0, scan_1.offsetToLineCol)(text, f.offset);
|
|
511
|
+
out.push({ uri: t.uri, relPath: t.relPath, finding: f, displayLine: lc.line, displayCol: lc.col });
|
|
512
|
+
}
|
|
513
|
+
done++;
|
|
514
|
+
if (done === total || done % 25 === 0) {
|
|
515
|
+
progress.report({ message: `${done}/${total} scanned, ${out.length} finding${out.length === 1 ? '' : 's'}` });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}));
|
|
519
|
+
if (token.isCancellationRequested)
|
|
520
|
+
return undefined;
|
|
521
|
+
return out;
|
|
522
|
+
});
|
|
523
|
+
if (hits === undefined)
|
|
524
|
+
return;
|
|
525
|
+
if (hits.length === 0) {
|
|
526
|
+
vscode.window.showInformationMessage('LLM Slop Detector: no findings in the workspace.');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
hits.sort((a, b) => {
|
|
530
|
+
const cmp = a.relPath.localeCompare(b.relPath);
|
|
531
|
+
if (cmp !== 0)
|
|
532
|
+
return cmp;
|
|
533
|
+
return a.finding.offset - b.finding.offset;
|
|
534
|
+
});
|
|
535
|
+
const counts = { error: 0, warning: 0, information: 0, hint: 0 };
|
|
536
|
+
const fileSet = new Set();
|
|
537
|
+
for (const h of hits) {
|
|
538
|
+
counts[h.finding.severity]++;
|
|
539
|
+
fileSet.add(h.relPath);
|
|
540
|
+
}
|
|
541
|
+
const summaryParts = [];
|
|
542
|
+
if (counts.error)
|
|
543
|
+
summaryParts.push(`${counts.error} error${counts.error === 1 ? '' : 's'}`);
|
|
544
|
+
if (counts.warning)
|
|
545
|
+
summaryParts.push(`${counts.warning} warning${counts.warning === 1 ? '' : 's'}`);
|
|
546
|
+
if (counts.information)
|
|
547
|
+
summaryParts.push(`${counts.information} info`);
|
|
548
|
+
if (counts.hint)
|
|
549
|
+
summaryParts.push(`${counts.hint} hint${counts.hint === 1 ? '' : 's'}`);
|
|
550
|
+
const summary = `${hits.length} finding${hits.length === 1 ? '' : 's'} in ${fileSet.size} file${fileSet.size === 1 ? '' : 's'} (${summaryParts.join(', ')})`;
|
|
551
|
+
const items = hits.map(h => ({
|
|
552
|
+
label: `$(${severityCodicon((0, rules_1.severityToVscode)(h.finding.severity))}) ${h.finding.matchText.trim() || h.finding.code}`,
|
|
553
|
+
description: `${h.relPath}:${h.displayLine}:${h.displayCol}`,
|
|
554
|
+
detail: h.finding.message,
|
|
555
|
+
hit: h,
|
|
556
|
+
}));
|
|
557
|
+
const pick = await vscode.window.showQuickPick(items, {
|
|
558
|
+
title: summary,
|
|
559
|
+
matchOnDescription: true,
|
|
560
|
+
matchOnDetail: true,
|
|
561
|
+
});
|
|
562
|
+
if (!pick)
|
|
563
|
+
return;
|
|
564
|
+
const doc = await vscode.workspace.openTextDocument(pick.hit.uri);
|
|
565
|
+
const editor = await vscode.window.showTextDocument(doc);
|
|
566
|
+
const start = doc.positionAt(pick.hit.finding.offset);
|
|
567
|
+
const end = doc.positionAt(pick.hit.finding.offset + pick.hit.finding.length);
|
|
568
|
+
const range = new vscode.Range(start, end);
|
|
569
|
+
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
|
|
570
|
+
editor.selection = new vscode.Selection(range.start, range.end);
|
|
571
|
+
}
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// Onboarding
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
// Versioned so we can re-trigger onboarding for material UX changes without
|
|
576
|
+
// spamming users who have already seen the current version. Bump the suffix
|
|
577
|
+
// when you want everyone to see the toast again.
|
|
578
|
+
const ONBOARDING_KEY = 'llmSlopDetector.onboarding.v2';
|
|
579
|
+
async function maybeShowOnboarding(context) {
|
|
580
|
+
if (context.globalState.get(ONBOARDING_KEY, false))
|
|
581
|
+
return;
|
|
582
|
+
await showOnboarding(context);
|
|
583
|
+
}
|
|
584
|
+
async function showOnboarding(context) {
|
|
585
|
+
const openPacks = 'Browse rule packs';
|
|
586
|
+
const learnMore = 'Learn more';
|
|
587
|
+
const dismiss = 'Dismiss';
|
|
588
|
+
const choice = await vscode.window.showInformationMessage('LLM Slop Detector is watching Markdown and plain-text files. Six optional rule packs (academic, cliches, fiction, claudeisms, structural, security) add broader coverage -- opt into them in settings.', openPacks, learnMore, dismiss);
|
|
589
|
+
// Record as shown regardless of choice. Any interaction -- including
|
|
590
|
+
// dismissal via the X button -- suppresses the toast on future activations.
|
|
591
|
+
await context.globalState.update(ONBOARDING_KEY, true);
|
|
592
|
+
if (choice === openPacks) {
|
|
593
|
+
// Focus the specific setting the onboarding is selling. The general
|
|
594
|
+
// "open all settings for this extension" entry point is the
|
|
595
|
+
// llmSlopDetector.openSettings command.
|
|
596
|
+
await vscode.commands.executeCommand('workbench.action.openSettings', 'llmSlopDetector.enabledPacks');
|
|
597
|
+
}
|
|
598
|
+
else if (choice === learnMore) {
|
|
599
|
+
await vscode.commands.executeCommand('extension.open', context.extension.id);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function deactivate() {
|
|
603
|
+
for (const { timer } of PENDING_REFRESH.values())
|
|
604
|
+
clearTimeout(timer);
|
|
605
|
+
PENDING_REFRESH.clear();
|
|
606
|
+
}
|
|
607
|
+
//# sourceMappingURL=extension.js.map
|