deep-slop 1.4.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/.deep-slop/.deep-slop-ignore +13 -0
- package/LICENSE +21 -0
- package/README.md +1170 -0
- package/dist/arch-constraints-C7s1E_bc.js +450 -0
- package/dist/arch-rules-DI1SYPqu.js +358 -0
- package/dist/ast-slop-BGdr58wZ.js +1839 -0
- package/dist/config-lint-ph3vMUbg.js +371 -0
- package/dist/dead-flow-DHRkyxZT.js +1422 -0
- package/dist/deep-slop-bundled.js +33140 -0
- package/dist/discover-B_S_Fy2S.js +164 -0
- package/dist/dup-detect-DKRXM04q.js +709 -0
- package/dist/file-utils-B_HFXhCs.js +93 -0
- package/dist/format-lint-DeElllNm.js +445 -0
- package/dist/framework-lint-CqdlF9hX.js +782 -0
- package/dist/i18n-lint-CPzx7V8Q.js +605 -0
- package/dist/import-intelligence-SK4F7XpL.js +966 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +1030 -0
- package/dist/knip-CgxnnTBZ.js +93 -0
- package/dist/lint-external-ZbW3jGvB.js +326 -0
- package/dist/markup-lint-DKVEDz9M.js +805 -0
- package/dist/mcp.js +35939 -0
- package/dist/meta-quality-Dai1W5iC.js +224 -0
- package/dist/perf-hints-BnWFMFff.js +500 -0
- package/dist/security-deep-DJRINs10.js +1198 -0
- package/dist/syntax-deep-ZQYMutky.js +624 -0
- package/dist/tree-sitter-CM-cP0nl.js +661 -0
- package/dist/type-safety-Dboj2C1t.js +519 -0
- package/package.json +92 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
//#region src/engines/type-safety/index.ts
|
|
5
|
+
/** Check whether a file path is a TypeScript/JavaScript file we should scan */
|
|
6
|
+
function isTargetFile(filePath) {
|
|
7
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath);
|
|
8
|
+
}
|
|
9
|
+
/** Check whether a file path is TypeScript (not plain JS) */
|
|
10
|
+
function isTypeScriptFile(filePath) {
|
|
11
|
+
return /\.(ts|tsx)$/i.test(filePath);
|
|
12
|
+
}
|
|
13
|
+
/** Check whether a file is a JSX/TSX file */
|
|
14
|
+
function isJsxFile(filePath) {
|
|
15
|
+
return /\.(tsx|jsx)$/i.test(filePath);
|
|
16
|
+
}
|
|
17
|
+
/** Walk a directory recursively collecting target files (excludes configured patterns) */
|
|
18
|
+
async function collectFiles(root, exclude, filter) {
|
|
19
|
+
const { readdir } = await import("node:fs/promises");
|
|
20
|
+
const results = [];
|
|
21
|
+
async function walk(dir) {
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = join(dir, entry.name);
|
|
25
|
+
const rel = relative(root, fullPath);
|
|
26
|
+
if (exclude.some((pattern) => rel.includes(pattern) || entry.name === pattern || new RegExp(pattern.replace(/\*/g, ".*")).test(rel))) continue;
|
|
27
|
+
if (entry.isDirectory()) await walk(fullPath);
|
|
28
|
+
else if (isTargetFile(entry.name)) if (filter && filter.length > 0) {
|
|
29
|
+
const normalizedRel = rel.replace(/\\/g, "/");
|
|
30
|
+
if (filter.some((f) => normalizedRel === f.replace(/\\/g, "/"))) results.push(fullPath);
|
|
31
|
+
} else results.push(fullPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
await walk(root);
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
/** Count leading spaces/tabs for column calculation */
|
|
38
|
+
function columnForIndex(line, index) {
|
|
39
|
+
return index + 1;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Analyze the surrounding context of an `as any` cast and produce
|
|
43
|
+
* a CONTEXT-AWARE suggestion. This is the crown jewel of this engine.
|
|
44
|
+
*/
|
|
45
|
+
function analyzeAsAnyContext(lineText, matchIndex, _line, _filePath, surroundingLines) {
|
|
46
|
+
const before = surroundingLines.before.join("\n").toLowerCase();
|
|
47
|
+
const after = surroundingLines.after.join("\n").toLowerCase();
|
|
48
|
+
const full = `${before}\n${lineText.toLowerCase()}\n${after}`;
|
|
49
|
+
if ([
|
|
50
|
+
/\.\s*findFirst\s*\(/,
|
|
51
|
+
/\.\s*findMany\s*\(/,
|
|
52
|
+
/\.\s*findUnique\s*\(/,
|
|
53
|
+
/\.\s*create\s*\(/,
|
|
54
|
+
/\.\s*update\s*\(/,
|
|
55
|
+
/\.\s*delete\s*\(/,
|
|
56
|
+
/\.\s*upsert\s*\(/,
|
|
57
|
+
/\.\s*query\s*\(/,
|
|
58
|
+
/\.\s*execute\s*\(/,
|
|
59
|
+
/prisma\./,
|
|
60
|
+
/drizzle\./,
|
|
61
|
+
/\.\s*select\s*\(/,
|
|
62
|
+
/\.\s*from\s*\(/,
|
|
63
|
+
/\.\s*where\s*\(/,
|
|
64
|
+
/db\.\s*\(/
|
|
65
|
+
].some((p) => p.test(full))) return {
|
|
66
|
+
severity: "suggestion",
|
|
67
|
+
rule: "types/as-any-orm",
|
|
68
|
+
message: "`as any` cast after ORM query — common Drizzle/Prisma workaround",
|
|
69
|
+
help: "ORMs like Drizzle sometimes produce incomplete types. Consider defining the return type explicitly instead of casting to any.",
|
|
70
|
+
suggestion: {
|
|
71
|
+
type: "refactor",
|
|
72
|
+
text: "as Awaited<ReturnType<typeof db.query.table>>",
|
|
73
|
+
confidence: .6,
|
|
74
|
+
reason: "ORM query return types can be inferred. Use ReturnType or define a specific row interface instead of `as any`. This is a known Drizzle workaround but should be typed explicitly."
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
if ([
|
|
78
|
+
/\bwindow\b/,
|
|
79
|
+
/\bdocument\b/,
|
|
80
|
+
/\bnavigator\b/,
|
|
81
|
+
/\bglobalThis\b/
|
|
82
|
+
].some((p) => p.test(lineText))) {
|
|
83
|
+
const varName = lineText.match(/(\w+)\s*(?:\.\s*\w+\s*)*=\s*.*window.*as\s+any/)?.[1] ?? "CustomWindow";
|
|
84
|
+
return {
|
|
85
|
+
severity: "warning",
|
|
86
|
+
rule: "types/as-any-window",
|
|
87
|
+
message: `\`as any\` cast on window/document — use interface extension instead`,
|
|
88
|
+
help: `Declare an extended Window interface to add custom properties instead of casting to any.`,
|
|
89
|
+
suggestion: {
|
|
90
|
+
type: "insert",
|
|
91
|
+
text: `interface ${varName.replace(/window/i, "XWindow")} extends Window {\n // Add your custom properties here\n}\n\n// Then use: window as ${varName.replace(/window/i, "XWindow")}`,
|
|
92
|
+
confidence: .85,
|
|
93
|
+
reason: "Extending the Window interface is the TypeScript-idiomatic way to add custom globals. It preserves type safety for all standard properties while allowing your extensions."
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const jsonParsePattern = /json\s*\.\s*parse\s*\(/i;
|
|
98
|
+
if (jsonParsePattern.test(full) || jsonParsePattern.test(lineText)) return {
|
|
99
|
+
severity: "warning",
|
|
100
|
+
rule: "types/as-any-json-parse",
|
|
101
|
+
message: "`as any` cast on JSON.parse result — use runtime validation instead",
|
|
102
|
+
help: "JSON.parse returns `any` by default. Instead of casting, use a validation library to ensure the data matches your expected shape at runtime.",
|
|
103
|
+
suggestion: {
|
|
104
|
+
type: "refactor",
|
|
105
|
+
text: `import { z } from "zod";\nconst MySchema = z.object({ /* ... */ });\nconst data = MySchema.parse(JSON.parse(raw));`,
|
|
106
|
+
confidence: .8,
|
|
107
|
+
reason: "zod (or io-ts/valibot) validates at runtime AND gives you a typed result. This eliminates both the `any` and the risk of malformed data slipping through."
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (/(?:function\s*\w*\s*\(|(?:const|let|var)\s+\w+\s*=\s*(?:\([^)]*\)|[^=]+)=>)\s*[^)]*as\s+any/.test(lineText) || /(?:param|arg|option|config|ctx|event)\w*\s*:\s*any/i.test(lineText)) {
|
|
111
|
+
const paramName = lineText.match(/(\w+)\s*(?::\s*any|:\s*unknown|\.\.\.)[^=]*as\s+any/)?.[1] ?? "params";
|
|
112
|
+
return {
|
|
113
|
+
severity: "warning",
|
|
114
|
+
rule: "types/as-any-param",
|
|
115
|
+
message: `\`as any\` cast on function parameter — define an interface for \`${paramName}\``,
|
|
116
|
+
help: `Instead of casting parameters to any, define an interface that describes the expected shape.`,
|
|
117
|
+
suggestion: {
|
|
118
|
+
type: "refactor",
|
|
119
|
+
text: `interface ${capitalize(paramName)}Params {\n // Define expected properties\n}\n\n// Use: (${paramName}: ${capitalize(paramName)}Params) => ...`,
|
|
120
|
+
confidence: .7,
|
|
121
|
+
reason: "Explicit interfaces on function parameters provide documentation, IDE autocompletion, and compile-time safety. `as any` removes all of these benefits."
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
severity: "warning",
|
|
127
|
+
rule: "types/as-any",
|
|
128
|
+
message: "Unsafe `as any` cast — disables type checking",
|
|
129
|
+
help: "Replace `as any` with a concrete type. If the type is truly unknown, use `unknown` and narrow with type guards.",
|
|
130
|
+
suggestion: {
|
|
131
|
+
type: "replace",
|
|
132
|
+
text: "as unknown",
|
|
133
|
+
confidence: .5,
|
|
134
|
+
reason: "`unknown` is the type-safe alternative to `any` — it requires narrowing before use, preventing accidental property access on the wrong type. If you know the shape, declare an interface instead."
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function capitalize(s) {
|
|
139
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Detect `as any` casts with context-aware suggestions.
|
|
143
|
+
* Rule: types/as-any, types/as-any-orm, types/as-any-window,
|
|
144
|
+
* types/as-any-json-parse, types/as-any-param
|
|
145
|
+
*/
|
|
146
|
+
function detectAsAny(lines, filePath) {
|
|
147
|
+
const diagnostics = [];
|
|
148
|
+
const regex = /\bas\s+any\b/g;
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
let match;
|
|
152
|
+
while ((match = regex.exec(line)) !== null) {
|
|
153
|
+
const col = columnForIndex(line, match.index);
|
|
154
|
+
const before = lines.slice(Math.max(0, i - 5), i);
|
|
155
|
+
const after = lines.slice(i + 1, Math.min(lines.length, i + 6));
|
|
156
|
+
const ctx = analyzeAsAnyContext(line, match.index, i + 1, filePath, {
|
|
157
|
+
before,
|
|
158
|
+
after
|
|
159
|
+
});
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
filePath,
|
|
162
|
+
engine: "type-safety",
|
|
163
|
+
rule: ctx.rule,
|
|
164
|
+
severity: ctx.severity,
|
|
165
|
+
message: ctx.message,
|
|
166
|
+
help: ctx.help,
|
|
167
|
+
line: i + 1,
|
|
168
|
+
column: col,
|
|
169
|
+
category: "types",
|
|
170
|
+
fixable: false,
|
|
171
|
+
suggestion: ctx.suggestion
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return diagnostics;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Detect double type assertions: `as unknown as X`
|
|
179
|
+
* Rule: types/double-assertion
|
|
180
|
+
*/
|
|
181
|
+
function detectDoubleAssertions(lines, filePath) {
|
|
182
|
+
const diagnostics = [];
|
|
183
|
+
const regex = /\bas\s+unknown\s+as\s+(\w+)/g;
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
const line = lines[i];
|
|
186
|
+
let match;
|
|
187
|
+
while ((match = regex.exec(line)) !== null) {
|
|
188
|
+
const targetTypeName = match[1];
|
|
189
|
+
const col = columnForIndex(line, match.index);
|
|
190
|
+
diagnostics.push({
|
|
191
|
+
filePath,
|
|
192
|
+
engine: "type-safety",
|
|
193
|
+
rule: "types/double-assertion",
|
|
194
|
+
severity: "warning",
|
|
195
|
+
message: `Double type assertion \`as unknown as ${targetTypeName}\` — use a named interface instead`,
|
|
196
|
+
help: `Double assertions bypass TypeScript's safety checks. Define an interface (like the YaWindow pattern) and cast once to it.`,
|
|
197
|
+
line: i + 1,
|
|
198
|
+
column: col,
|
|
199
|
+
category: "types",
|
|
200
|
+
fixable: true,
|
|
201
|
+
suggestion: {
|
|
202
|
+
type: "refactor",
|
|
203
|
+
text: `interface ${targetTypeName.startsWith("I") ? targetTypeName : `I${targetTypeName}`} {\n // Define the expected shape\n}\n\n// Then use: ... as I${targetTypeName}`,
|
|
204
|
+
confidence: .75,
|
|
205
|
+
reason: `A named interface with a single cast is safer than \`as unknown as ${targetTypeName}\`. Double assertions hide real type mismatches — a named interface forces you to think about the actual shape.`
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return diagnostics;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Detect missing return type annotations on functions in TS files.
|
|
214
|
+
* Skip simple arrow functions in JSX/TSX files.
|
|
215
|
+
* Rule: types/missing-return-type
|
|
216
|
+
*/
|
|
217
|
+
function detectMissingReturnTypes(lines, filePath) {
|
|
218
|
+
if (!isTypeScriptFile(filePath)) return [];
|
|
219
|
+
const diagnostics = [];
|
|
220
|
+
const isJsx = isJsxFile(filePath);
|
|
221
|
+
const funcPatterns = [
|
|
222
|
+
/function\s+(\w+)\s*\([^)]*\)\s*\{/,
|
|
223
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>\s*\{/,
|
|
224
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*\w+\s*=>\s*\{/
|
|
225
|
+
];
|
|
226
|
+
if (!isJsx) funcPatterns.push(/export\s+(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{/, /export\s+default\s+(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{/);
|
|
227
|
+
for (let i = 0; i < lines.length; i++) {
|
|
228
|
+
const line = lines[i];
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
if (isJsx && /^\s*(?:const|let)\s+\w+\s*=\s*\([^)]*\)\s*=>\s*[^{]/.test(line)) continue;
|
|
231
|
+
for (const pattern of funcPatterns) {
|
|
232
|
+
const match = pattern.exec(trimmed);
|
|
233
|
+
if (!match) continue;
|
|
234
|
+
const funcName = match[1] || "anonymous";
|
|
235
|
+
if (/:\s*\w+(\[\])?\s*(=>|\{)/.test(trimmed) || /:\s*\w+<[^>]+>\s*(=>|\{)/.test(trimmed)) continue;
|
|
236
|
+
if (/^(constructor|render|componentDidMount|useEffect|useState)/.test(funcName)) continue;
|
|
237
|
+
diagnostics.push({
|
|
238
|
+
filePath,
|
|
239
|
+
engine: "type-safety",
|
|
240
|
+
rule: "types/missing-return-type",
|
|
241
|
+
severity: "info",
|
|
242
|
+
message: `Function \`${funcName}\` has no explicit return type annotation`,
|
|
243
|
+
help: "Add an explicit return type to document the function's contract and catch mismatches at compile time.",
|
|
244
|
+
line: i + 1,
|
|
245
|
+
column: trimmed.indexOf(funcName) + 1,
|
|
246
|
+
category: "types",
|
|
247
|
+
fixable: true,
|
|
248
|
+
suggestion: {
|
|
249
|
+
type: "refactor",
|
|
250
|
+
text: `: /* infer return type */ = `,
|
|
251
|
+
confidence: .4,
|
|
252
|
+
reason: "Explicit return types serve as documentation and catch accidental return-type changes. Hover over the function in your IDE to see the inferred type, then add it explicitly."
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return diagnostics;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Detect @ts-ignore and @ts-expect-error comments.
|
|
262
|
+
* Rule: types/ts-suppress
|
|
263
|
+
*/
|
|
264
|
+
function detectTsSuppress(lines, filePath) {
|
|
265
|
+
if (!isTypeScriptFile(filePath)) return [];
|
|
266
|
+
const diagnostics = [];
|
|
267
|
+
const patterns = [{
|
|
268
|
+
regex: /\/\/\s*@ts-ignore/,
|
|
269
|
+
label: "@ts-ignore"
|
|
270
|
+
}, {
|
|
271
|
+
regex: /\/\/\s*@ts-expect-error/,
|
|
272
|
+
label: "@ts-expect-error"
|
|
273
|
+
}];
|
|
274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
275
|
+
const line = lines[i];
|
|
276
|
+
for (const { regex, label } of patterns) {
|
|
277
|
+
const match = regex.exec(line);
|
|
278
|
+
if (!match) continue;
|
|
279
|
+
const col = columnForIndex(line, match.index);
|
|
280
|
+
const nextLine = i + 1 < lines.length ? lines[i + 1].trim() : "";
|
|
281
|
+
const suppressedHint = nextLine ? ` The suppressed line is: \`${nextLine}\`` : "";
|
|
282
|
+
diagnostics.push({
|
|
283
|
+
filePath,
|
|
284
|
+
engine: "type-safety",
|
|
285
|
+
rule: "types/ts-suppress",
|
|
286
|
+
severity: "warning",
|
|
287
|
+
message: `\`${label}\` suppresses a TypeScript error — fix the underlying type issue instead`,
|
|
288
|
+
help: `Type-suppression comments hide real problems.${suppressedHint} Fix the type error on the next line and remove this comment.`,
|
|
289
|
+
line: i + 1,
|
|
290
|
+
column: col,
|
|
291
|
+
category: "types",
|
|
292
|
+
fixable: true,
|
|
293
|
+
suggestion: {
|
|
294
|
+
type: "delete",
|
|
295
|
+
text: `// Remove ${label} and fix the type error on the next line`,
|
|
296
|
+
confidence: .7,
|
|
297
|
+
reason: `${label} hides type errors that could cause runtime failures. Fix the underlying type mismatch instead of suppressing it. If the error is in a dependency's type definitions, use \`declare module\` or a local override.`
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return diagnostics;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Detect non-null assertions: `x!` operator.
|
|
306
|
+
* Rule: types/non-null-assertion
|
|
307
|
+
*/
|
|
308
|
+
function detectNonNullAssertions(lines, filePath) {
|
|
309
|
+
if (!isTypeScriptFile(filePath)) return [];
|
|
310
|
+
const diagnostics = [];
|
|
311
|
+
const regex = /(\w+)!(?![=:(])/g;
|
|
312
|
+
for (let i = 0; i < lines.length; i++) {
|
|
313
|
+
const line = lines[i];
|
|
314
|
+
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) continue;
|
|
315
|
+
let match;
|
|
316
|
+
while ((match = regex.exec(line)) !== null) {
|
|
317
|
+
const identifier = match[1];
|
|
318
|
+
const col = columnForIndex(line, match.index + identifier.length);
|
|
319
|
+
const beforeMatch = line.substring(0, match.index);
|
|
320
|
+
const singleQuotes = (beforeMatch.match(/'/g) || []).length;
|
|
321
|
+
const doubleQuotes = (beforeMatch.match(/"/g) || []).length;
|
|
322
|
+
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) continue;
|
|
323
|
+
if (beforeMatch.includes("`") && !beforeMatch.includes("}")) continue;
|
|
324
|
+
diagnostics.push({
|
|
325
|
+
filePath,
|
|
326
|
+
engine: "type-safety",
|
|
327
|
+
rule: "types/non-null-assertion",
|
|
328
|
+
severity: "warning",
|
|
329
|
+
message: `Non-null assertion \`${identifier}!\` — this assertion is unchecked at runtime`,
|
|
330
|
+
help: `Non-null assertions (\`!\`) tell TypeScript to assume a value is non-null, but this isn't checked at runtime. Add an explicit null check instead.`,
|
|
331
|
+
line: i + 1,
|
|
332
|
+
column: col,
|
|
333
|
+
category: "types",
|
|
334
|
+
fixable: true,
|
|
335
|
+
suggestion: {
|
|
336
|
+
type: "replace",
|
|
337
|
+
text: `if (${identifier} != null) { /* use ${identifier} safely */ }`,
|
|
338
|
+
confidence: .65,
|
|
339
|
+
reason: "An explicit null check provides runtime safety AND narrows the type in TypeScript. The `!` operator only satisfies the compiler — it can cause runtime errors if the value is actually null/undefined."
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return diagnostics;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Detect `any` used as a generic type parameter.
|
|
348
|
+
* Rule: types/generic-any
|
|
349
|
+
*/
|
|
350
|
+
function detectGenericAny(lines, filePath) {
|
|
351
|
+
if (!isTypeScriptFile(filePath)) return [];
|
|
352
|
+
const diagnostics = [];
|
|
353
|
+
const genericPatterns = [
|
|
354
|
+
{
|
|
355
|
+
regex: /Array\s*<\s*any\s*>/g,
|
|
356
|
+
construct: "Array<any>",
|
|
357
|
+
suggestion: "unknown[]"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
regex: /ReadonlyArray\s*<\s*any\s*>/g,
|
|
361
|
+
construct: "ReadonlyArray<any>",
|
|
362
|
+
suggestion: "readonly unknown[]"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
regex: /Promise\s*<\s*any\s*>/g,
|
|
366
|
+
construct: "Promise<any>",
|
|
367
|
+
suggestion: "Promise<unknown>"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
regex: /Record\s*<\s*([^,]+)\s*,\s*any\s*>/g,
|
|
371
|
+
construct: "Record<K, any>",
|
|
372
|
+
suggestion: "Record<K, unknown>"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
regex: /Map\s*<\s*([^,]+)\s*,\s*any\s*>/g,
|
|
376
|
+
construct: "Map<K, any>",
|
|
377
|
+
suggestion: "Map<K, unknown>"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
regex: /Set\s*<\s*any\s*>/g,
|
|
381
|
+
construct: "Set<any>",
|
|
382
|
+
suggestion: "Set<unknown>"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
regex: /Partial\s*<\s*any\s*>/g,
|
|
386
|
+
construct: "Partial<any>",
|
|
387
|
+
suggestion: "Partial<Record<string, unknown>>"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
regex: /Omit\s*<\s*any\s*,/g,
|
|
391
|
+
construct: "Omit<any, K>",
|
|
392
|
+
suggestion: "Omit<Record<string, unknown>, K>"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
regex: /Pick\s*<\s*any\s*,/g,
|
|
396
|
+
construct: "Pick<any, K>",
|
|
397
|
+
suggestion: "Pick<Record<string, unknown>, K>"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
regex: /ReturnType\s*<\s*any\s*>/g,
|
|
401
|
+
construct: "ReturnType<any>",
|
|
402
|
+
suggestion: "ReturnType<typeof fn>"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
regex: /(\w+)\s*<\s*any\s*>/g,
|
|
406
|
+
construct: "T<any>",
|
|
407
|
+
suggestion: "T<unknown>"
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
for (let i = 0; i < lines.length; i++) {
|
|
411
|
+
const line = lines[i];
|
|
412
|
+
if (/^\s*\/\//.test(line) || /^\s*\*/.test(line)) continue;
|
|
413
|
+
for (const pattern of genericPatterns) {
|
|
414
|
+
let match;
|
|
415
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
416
|
+
const col = columnForIndex(line, match.index);
|
|
417
|
+
let suggestionText;
|
|
418
|
+
let reason;
|
|
419
|
+
if (pattern.construct === "Record<K, any>") {
|
|
420
|
+
suggestionText = `Record<${match[1]?.trim() ?? "string"}, unknown>`;
|
|
421
|
+
reason = `Using \`any\` as the value type in Record allows unrestricted access. Use \`unknown\` to require narrowing before use, or define a specific value interface.`;
|
|
422
|
+
} else if (pattern.construct === "Map<K, any>") {
|
|
423
|
+
suggestionText = `Map<${match[1]?.trim() ?? "string"}, unknown>`;
|
|
424
|
+
reason = `Using \`any\` as the value type in Map allows unrestricted access. Use \`unknown\` to require narrowing, or define a specific value interface.`;
|
|
425
|
+
} else if (pattern.construct === "Omit<any, K>" || pattern.construct === "Pick<any, K>") {
|
|
426
|
+
const constructName = pattern.construct.startsWith("Omit") ? "Omit" : "Pick";
|
|
427
|
+
suggestionText = `${constructName}<Record<string, unknown>, K>`;
|
|
428
|
+
reason = `Using \`any\` as the base type in ${constructName} makes the result essentially untyped. Define a proper interface as the base.`;
|
|
429
|
+
} else if (pattern.construct === "T<any>") {
|
|
430
|
+
suggestionText = `${match[1] ?? "T"}<unknown>`;
|
|
431
|
+
reason = `Using \`any\` as a generic parameter defeats the purpose of generics. Use \`unknown\` or a specific type.`;
|
|
432
|
+
} else {
|
|
433
|
+
suggestionText = pattern.suggestion;
|
|
434
|
+
reason = `Using \`any\` as a generic type parameter disables all type checking for that parameter. Use \`unknown\` to require narrowing, or define a specific type.`;
|
|
435
|
+
}
|
|
436
|
+
if (diagnostics.some((d) => d.line === i + 1 && d.column === col)) continue;
|
|
437
|
+
diagnostics.push({
|
|
438
|
+
filePath,
|
|
439
|
+
engine: "type-safety",
|
|
440
|
+
rule: "types/generic-any",
|
|
441
|
+
severity: "warning",
|
|
442
|
+
message: `\`${match[0]}\` uses \`any\` as a generic type parameter`,
|
|
443
|
+
help: `Replace \`any\` with a concrete type or \`unknown\` to preserve type safety.`,
|
|
444
|
+
line: i + 1,
|
|
445
|
+
column: col,
|
|
446
|
+
category: "types",
|
|
447
|
+
fixable: true,
|
|
448
|
+
suggestion: {
|
|
449
|
+
type: "replace",
|
|
450
|
+
text: suggestionText,
|
|
451
|
+
confidence: .7,
|
|
452
|
+
reason
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return diagnostics;
|
|
459
|
+
}
|
|
460
|
+
const typeSafetyEngine = {
|
|
461
|
+
name: "type-safety",
|
|
462
|
+
description: "Detects type safety issues with context-aware suggestions — as any casts, double assertions, missing return types, ts-suppress comments, non-null assertions, and generic any parameters.",
|
|
463
|
+
supportedLanguages: ["typescript", "javascript"],
|
|
464
|
+
async run(context) {
|
|
465
|
+
const startTime = performance.now();
|
|
466
|
+
const { flagAsAny, suggestTypes, flagDoubleAssertion } = context.config.types;
|
|
467
|
+
if (!flagAsAny && !suggestTypes && !flagDoubleAssertion) return {
|
|
468
|
+
engine: "type-safety",
|
|
469
|
+
diagnostics: [],
|
|
470
|
+
elapsed: performance.now() - startTime,
|
|
471
|
+
skipped: true,
|
|
472
|
+
skipReason: "All type-safety checks disabled in config"
|
|
473
|
+
};
|
|
474
|
+
const root = context.rootDirectory;
|
|
475
|
+
const exclude = context.config.exclude;
|
|
476
|
+
const files = context.files ? context.files.filter((f) => isTargetFile(f)).map((f) => join(root, f)) : await collectFiles(root, exclude);
|
|
477
|
+
if (files.length === 0) return {
|
|
478
|
+
engine: "type-safety",
|
|
479
|
+
diagnostics: [],
|
|
480
|
+
elapsed: performance.now() - startTime,
|
|
481
|
+
skipped: true,
|
|
482
|
+
skipReason: "No TypeScript/JavaScript files found to scan"
|
|
483
|
+
};
|
|
484
|
+
const allDiagnostics = [];
|
|
485
|
+
for (const filePath of files) {
|
|
486
|
+
let content;
|
|
487
|
+
try {
|
|
488
|
+
content = await readFile(filePath, "utf-8");
|
|
489
|
+
} catch {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const lines = content.split("\n");
|
|
493
|
+
const relPath = relative(root, filePath).replace(/\\/g, "/");
|
|
494
|
+
if (flagAsAny) allDiagnostics.push(...detectAsAny(lines, relPath));
|
|
495
|
+
if (flagDoubleAssertion) allDiagnostics.push(...detectDoubleAssertions(lines, relPath));
|
|
496
|
+
if (suggestTypes) allDiagnostics.push(...detectMissingReturnTypes(lines, relPath));
|
|
497
|
+
allDiagnostics.push(...detectTsSuppress(lines, relPath));
|
|
498
|
+
allDiagnostics.push(...detectNonNullAssertions(lines, relPath));
|
|
499
|
+
allDiagnostics.push(...detectGenericAny(lines, relPath));
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
engine: "type-safety",
|
|
503
|
+
diagnostics: allDiagnostics,
|
|
504
|
+
elapsed: performance.now() - startTime,
|
|
505
|
+
skipped: false
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
async fix(diagnostics, _context) {
|
|
509
|
+
diagnostics.filter((d) => d.fixable && d.suggestion && d.suggestion.type === "replace" && d.suggestion.confidence >= .8);
|
|
510
|
+
return {
|
|
511
|
+
fixed: 0,
|
|
512
|
+
remaining: diagnostics,
|
|
513
|
+
modifiedFiles: []
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
//#endregion
|
|
519
|
+
export { typeSafetyEngine };
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deep-slop",
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Deep AI slop detection with AST analysis, alternative import paths, dead code flow, type safety, and 12 engines. Far beyond aislop.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"deep-slop": "./dist/deep-slop-bundled.js",
|
|
8
|
+
"deep-slop-mcp": "./dist/mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/deep-slop-bundled.js",
|
|
12
|
+
"dist/mcp.js",
|
|
13
|
+
"dist/index.js",
|
|
14
|
+
"dist/**/*.js",
|
|
15
|
+
"dist/**/*.d.ts",
|
|
16
|
+
"scripts",
|
|
17
|
+
".deep-slop/.deep-slop-ignore",
|
|
18
|
+
"!dist/**/*.test.*",
|
|
19
|
+
"!dist/**/*.test.js.map"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsdown --watch",
|
|
29
|
+
"build": "rm -rf dist && NODE_ENV=production tsdown && npx esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/deep-slop-bundled.js --external:web-tree-sitter --external:tree-sitter-typescript --external:tree-sitter-python --external:tree-sitter-go --external:tree-sitter-rust --external:tree-sitter-php --external:tree-sitter-c-sharp --external:tree-sitter-swift --external:zod --external:glob --external:fast-glob --banner:js='import{createRequire}from\"node:module\";const require=createRequire(import.meta.url);' && npx esbuild src/mcp.ts --bundle --platform=node --format=esm --outfile=dist/mcp.js --external:web-tree-sitter --external:tree-sitter-typescript --external:tree-sitter-python --external:tree-sitter-go --external:tree-sitter-rust --external:tree-sitter-php --external:tree-sitter-c-sharp --external:tree-sitter-swift --external:zod --external:glob --external:fast-glob --banner:js='import{createRequire}from\"node:module\";const require=createRequire(import.meta.url);'",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"scan": "pnpm build && node dist/deep-slop-bundled.js scan .",
|
|
33
|
+
"scan:json": "pnpm build && node dist/deep-slop-bundled.js scan . --json"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"deep-slop",
|
|
37
|
+
"ai-slop",
|
|
38
|
+
"code-quality",
|
|
39
|
+
"ast",
|
|
40
|
+
"tree-sitter",
|
|
41
|
+
"import-intelligence",
|
|
42
|
+
"dead-code",
|
|
43
|
+
"type-safety",
|
|
44
|
+
"linter",
|
|
45
|
+
"static-analysis"
|
|
46
|
+
],
|
|
47
|
+
"author": "Romanchello",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/DemumuMind/deep-slopDM.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/DemumuMind/deep-slopDM#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/DemumuMind/deep-slopDM/issues"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"packageManager": "pnpm@10.28.0",
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@clack/prompts": "^1.2.0",
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
68
|
+
"commander": "^14.0.3",
|
|
69
|
+
"fast-glob": "^3.3.3",
|
|
70
|
+
"graphology": "^0.25.4",
|
|
71
|
+
"js-yaml": "^4.2.0",
|
|
72
|
+
"micromatch": "^4.0.8",
|
|
73
|
+
"minimatch": "^10.2.5",
|
|
74
|
+
"picocolors": "^1.1.1",
|
|
75
|
+
"tree-sitter-javascript": "^0.25.0",
|
|
76
|
+
"tree-sitter-python": "^0.25.0",
|
|
77
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
78
|
+
"web-tree-sitter": "^0.25.5",
|
|
79
|
+
"yaml": "^2.8.2",
|
|
80
|
+
"zod": "^4.3.6",
|
|
81
|
+
"zod-to-json-schema": "^3.25.2"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@types/js-yaml": "^4.0.9",
|
|
85
|
+
"@types/micromatch": "^4.0.10",
|
|
86
|
+
"@types/minimatch": "^6.0.0",
|
|
87
|
+
"@types/node": "^25.6.0",
|
|
88
|
+
"tsdown": "^0.20.3",
|
|
89
|
+
"typescript": "^5.9.3",
|
|
90
|
+
"vitest": "^4.1.8"
|
|
91
|
+
}
|
|
92
|
+
}
|