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,358 @@
|
|
|
1
|
+
import { t as collectFiles } from "./discover-B_S_Fy2S.js";
|
|
2
|
+
import { r as readFileContent } from "./file-utils-B_HFXhCs.js";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { minimatch } from "minimatch";
|
|
6
|
+
import yaml from "js-yaml";
|
|
7
|
+
|
|
8
|
+
//#region src/engines/arch-rules/loader.ts
|
|
9
|
+
const RULES_PATH = ".deep-slop/rules.yml";
|
|
10
|
+
/** Validate and normalize a single rule object */
|
|
11
|
+
function validateRule(raw, index) {
|
|
12
|
+
const name = typeof raw.name === "string" ? raw.name : `rule-${index + 1}`;
|
|
13
|
+
const type = raw.type;
|
|
14
|
+
if (!type || ![
|
|
15
|
+
"forbid_import",
|
|
16
|
+
"forbid_import_from_path",
|
|
17
|
+
"require_pattern"
|
|
18
|
+
].includes(type)) throw new Error(`Rule "${name}" has invalid type: "${type}". Expected: forbid_import, forbid_import_from_path, require_pattern`);
|
|
19
|
+
if (!raw.match || typeof raw.match !== "string") throw new Error(`Rule "${name}" is missing required "match" glob pattern`);
|
|
20
|
+
if (type === "forbid_import" && (!raw.forbid || typeof raw.forbid !== "string")) throw new Error(`Rule "${name}" (forbid_import) is missing required "forbid" field`);
|
|
21
|
+
if (type === "forbid_import_from_path" && (!raw.forbid || typeof raw.forbid !== "string")) throw new Error(`Rule "${name}" (forbid_import_from_path) is missing required "forbid" field`);
|
|
22
|
+
if (type === "forbid_import_from_path" && (!raw.from || typeof raw.from !== "string")) throw new Error(`Rule "${name}" (forbid_import_from_path) is missing required "from" field`);
|
|
23
|
+
if (type === "require_pattern" && (!raw.pattern || typeof raw.pattern !== "string")) throw new Error(`Rule "${name}" (require_pattern) is missing required "pattern" field`);
|
|
24
|
+
const severity = raw.severity;
|
|
25
|
+
if (severity && ![
|
|
26
|
+
"error",
|
|
27
|
+
"warning",
|
|
28
|
+
"info"
|
|
29
|
+
].includes(severity)) throw new Error(`Rule "${name}" has invalid severity: "${severity}". Expected: error, warning, info`);
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
type,
|
|
33
|
+
match: raw.match,
|
|
34
|
+
forbid: raw.forbid,
|
|
35
|
+
from: raw.from,
|
|
36
|
+
pattern: raw.pattern,
|
|
37
|
+
severity: severity ?? "warning",
|
|
38
|
+
where: raw.where
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Load architecture rules from .deep-slop/rules.yml.
|
|
43
|
+
* Returns an empty array if the file does not exist.
|
|
44
|
+
*/
|
|
45
|
+
async function loadRules(rootDir) {
|
|
46
|
+
const filePath = join(rootDir, RULES_PATH);
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
content = await readFile(filePath, "utf-8");
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const parsed = yaml.load(content);
|
|
54
|
+
if (!parsed || !Array.isArray(parsed.rules)) throw new Error(`Invalid rules file: ${RULES_PATH} — expected a "rules" array at top level`);
|
|
55
|
+
return parsed.rules.map((raw, i) => validateRule(raw, i));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/engines/arch-rules/matchers.ts
|
|
60
|
+
/** Extract imports from JS/TS content */
|
|
61
|
+
function extractJsImports(content) {
|
|
62
|
+
const imports = [];
|
|
63
|
+
const lines = content.split("\n");
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const trimmed = lines[i].trim();
|
|
66
|
+
const lineNum = i + 1;
|
|
67
|
+
const staticMatch = trimmed.match(/^import\s+(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
68
|
+
if (staticMatch) {
|
|
69
|
+
imports.push({
|
|
70
|
+
line: lineNum,
|
|
71
|
+
source: staticMatch[1],
|
|
72
|
+
raw: trimmed
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const sideEffectMatch = trimmed.match(/^import\s+['"]([^'"]+)['"]/);
|
|
77
|
+
if (sideEffectMatch) {
|
|
78
|
+
imports.push({
|
|
79
|
+
line: lineNum,
|
|
80
|
+
source: sideEffectMatch[1],
|
|
81
|
+
raw: trimmed
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const dynMatch = trimmed.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
86
|
+
if (dynMatch) {
|
|
87
|
+
imports.push({
|
|
88
|
+
line: lineNum,
|
|
89
|
+
source: dynMatch[1],
|
|
90
|
+
raw: trimmed
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const reqMatch = trimmed.match(/(?:const|let|var)\s+[^=]*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
95
|
+
if (reqMatch) imports.push({
|
|
96
|
+
line: lineNum,
|
|
97
|
+
source: reqMatch[1],
|
|
98
|
+
raw: trimmed
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return imports;
|
|
102
|
+
}
|
|
103
|
+
/** Extract imports from Python content */
|
|
104
|
+
function extractPythonImports(content) {
|
|
105
|
+
const imports = [];
|
|
106
|
+
const lines = content.split("\n");
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
const trimmed = lines[i].trim();
|
|
109
|
+
const lineNum = i + 1;
|
|
110
|
+
const fromMatch = trimmed.match(/^from\s+([^\s]+)\s+import/);
|
|
111
|
+
if (fromMatch) {
|
|
112
|
+
imports.push({
|
|
113
|
+
line: lineNum,
|
|
114
|
+
source: fromMatch[1],
|
|
115
|
+
raw: trimmed
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const importMatch = trimmed.match(/^import\s+([^\s,]+)/);
|
|
120
|
+
if (importMatch) imports.push({
|
|
121
|
+
line: lineNum,
|
|
122
|
+
source: importMatch[1],
|
|
123
|
+
raw: trimmed
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return imports;
|
|
127
|
+
}
|
|
128
|
+
/** Extract imports from Go content */
|
|
129
|
+
function extractGoImports(content) {
|
|
130
|
+
const imports = [];
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const trimmed = lines[i].trim();
|
|
134
|
+
const lineNum = i + 1;
|
|
135
|
+
const singleMatch = trimmed.match(/^import\s+"([^"]+)"/);
|
|
136
|
+
if (singleMatch) {
|
|
137
|
+
imports.push({
|
|
138
|
+
line: lineNum,
|
|
139
|
+
source: singleMatch[1],
|
|
140
|
+
raw: trimmed
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const multiMatch = trimmed.match(/^"([^"]+)"$/);
|
|
145
|
+
if (multiMatch) {
|
|
146
|
+
const nearby = content.substring(Math.max(0, content.indexOf(lines[i]) - 200), content.indexOf(lines[i]) + lines[i].length);
|
|
147
|
+
if (nearby.includes("import") && nearby.includes("(")) imports.push({
|
|
148
|
+
line: lineNum,
|
|
149
|
+
source: multiMatch[1],
|
|
150
|
+
raw: trimmed
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return imports;
|
|
155
|
+
}
|
|
156
|
+
/** Detect language from file extension and extract imports */
|
|
157
|
+
function extractImportsFromContent(content, filePath) {
|
|
158
|
+
const ext = filePath.split(".").pop() ?? "";
|
|
159
|
+
if ([
|
|
160
|
+
"ts",
|
|
161
|
+
"tsx",
|
|
162
|
+
"js",
|
|
163
|
+
"jsx",
|
|
164
|
+
"mjs",
|
|
165
|
+
"cjs"
|
|
166
|
+
].includes(ext)) return extractJsImports(content);
|
|
167
|
+
if (["py", "pyw"].includes(ext)) return extractPythonImports(content);
|
|
168
|
+
if (ext === "go") return extractGoImports(content);
|
|
169
|
+
return extractJsImports(content);
|
|
170
|
+
}
|
|
171
|
+
/** Check if a path matches a glob pattern */
|
|
172
|
+
function globMatch(pattern, path) {
|
|
173
|
+
return minimatch(path, pattern, { dot: true });
|
|
174
|
+
}
|
|
175
|
+
/** Build a diagnostic for an arch-rules violation */
|
|
176
|
+
function buildDiag(opts) {
|
|
177
|
+
return {
|
|
178
|
+
filePath: opts.filePath,
|
|
179
|
+
engine: "arch-rules",
|
|
180
|
+
rule: `arch-rules/${opts.rule.name}`,
|
|
181
|
+
severity: opts.rule.severity,
|
|
182
|
+
message: opts.message,
|
|
183
|
+
help: opts.help,
|
|
184
|
+
line: opts.line,
|
|
185
|
+
column: opts.column,
|
|
186
|
+
category: "architecture",
|
|
187
|
+
fixable: false,
|
|
188
|
+
detail: {
|
|
189
|
+
ruleName: opts.rule.name,
|
|
190
|
+
ruleType: opts.rule.type
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* forbid_import: Checks if a file contains a forbidden import.
|
|
196
|
+
* The `forbid` field is a glob that matches against import sources.
|
|
197
|
+
* The `match` field is a glob that selects which files to check.
|
|
198
|
+
*/
|
|
199
|
+
function applyForbidImport(rule, content, filePath) {
|
|
200
|
+
if (!rule.forbid) return [];
|
|
201
|
+
if (!globMatch(rule.match, filePath)) return [];
|
|
202
|
+
if (rule.where && !globMatch(rule.where, filePath)) return [];
|
|
203
|
+
const imports = extractImportsFromContent(content, filePath);
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const imp of imports) if (globMatch(rule.forbid, imp.source)) {
|
|
206
|
+
const col = imp.raw.indexOf(imp.source) + 1;
|
|
207
|
+
results.push(buildDiag({
|
|
208
|
+
filePath,
|
|
209
|
+
rule,
|
|
210
|
+
line: imp.line,
|
|
211
|
+
column: Math.max(col, 1),
|
|
212
|
+
message: `Forbidden import '${imp.source}' found (rule: ${rule.name})`,
|
|
213
|
+
help: `The rule "${rule.name}" forbids importing modules matching "${rule.forbid}". Remove this import or update the rule.`
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* forbid_import_from_path: Checks cross-layer imports.
|
|
220
|
+
* Files matching `match` must not import from paths matching `from`.
|
|
221
|
+
* The `forbid` field specifies what is being imported (glob on import source).
|
|
222
|
+
* The `from` field specifies the source path pattern that triggers the violation.
|
|
223
|
+
*/
|
|
224
|
+
function applyForbidImportFromPath(rule, content, filePath) {
|
|
225
|
+
if (!rule.forbid || !rule.from) return [];
|
|
226
|
+
if (!globMatch(rule.match, filePath)) return [];
|
|
227
|
+
if (rule.where && !globMatch(rule.where, filePath)) return [];
|
|
228
|
+
const imports = extractImportsFromContent(content, filePath);
|
|
229
|
+
const results = [];
|
|
230
|
+
for (const imp of imports) {
|
|
231
|
+
if (!globMatch(rule.forbid, imp.source)) continue;
|
|
232
|
+
if (!globMatch(rule.from, imp.source)) continue;
|
|
233
|
+
const col = imp.raw.indexOf(imp.source) + 1;
|
|
234
|
+
results.push(buildDiag({
|
|
235
|
+
filePath,
|
|
236
|
+
rule,
|
|
237
|
+
line: imp.line,
|
|
238
|
+
column: Math.max(col, 1),
|
|
239
|
+
message: `Cross-layer import: '${imp.source}' is forbidden from '${filePath}' (rule: ${rule.name})`,
|
|
240
|
+
help: `The rule "${rule.name}" forbids importing "${rule.forbid}" from "${rule.from}" in files matching "${rule.match}". Refactor to use a service layer or adapter instead.`
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
return results;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* require_pattern: Checks that a required regex pattern exists in the file.
|
|
247
|
+
* The `match` field is a glob that selects which files to check.
|
|
248
|
+
* The `pattern` field is a regex that must be present in the file content.
|
|
249
|
+
*/
|
|
250
|
+
function applyRequirePattern(rule, content, filePath) {
|
|
251
|
+
if (!rule.pattern) return [];
|
|
252
|
+
if (!globMatch(rule.match, filePath)) return [];
|
|
253
|
+
if (rule.where && !globMatch(rule.where, filePath)) return [];
|
|
254
|
+
const results = [];
|
|
255
|
+
try {
|
|
256
|
+
if (!new RegExp(rule.pattern, "m").test(content)) results.push(buildDiag({
|
|
257
|
+
filePath,
|
|
258
|
+
rule,
|
|
259
|
+
line: 1,
|
|
260
|
+
column: 1,
|
|
261
|
+
message: `Required pattern /${rule.pattern}/ not found (rule: ${rule.name})`,
|
|
262
|
+
help: `The rule "${rule.name}" requires files matching "${rule.match}" to contain the pattern /${rule.pattern}/. Add the required pattern to this file.`
|
|
263
|
+
}));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
results.push(buildDiag({
|
|
266
|
+
filePath,
|
|
267
|
+
rule,
|
|
268
|
+
line: 1,
|
|
269
|
+
column: 1,
|
|
270
|
+
message: `Rule "${rule.name}" has invalid regex pattern: ${rule.pattern}`,
|
|
271
|
+
help: `Fix the regex pattern in .deep-slop/rules.yml for rule "${rule.name}".`
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Apply a single rule to a file, dispatching to the correct matcher.
|
|
278
|
+
*/
|
|
279
|
+
function applyRule(rule, content, filePath) {
|
|
280
|
+
switch (rule.type) {
|
|
281
|
+
case "forbid_import": return applyForbidImport(rule, content, filePath);
|
|
282
|
+
case "forbid_import_from_path": return applyForbidImportFromPath(rule, content, filePath);
|
|
283
|
+
case "require_pattern": return applyRequirePattern(rule, content, filePath);
|
|
284
|
+
default: return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/engines/arch-rules/index.ts
|
|
290
|
+
const archRulesEngine = {
|
|
291
|
+
name: "arch-rules",
|
|
292
|
+
description: "User-defined architecture rules from .deep-slop/rules.yml: forbid imports, enforce cross-layer boundaries, and require patterns.",
|
|
293
|
+
supportedLanguages: [
|
|
294
|
+
"typescript",
|
|
295
|
+
"javascript",
|
|
296
|
+
"python",
|
|
297
|
+
"go"
|
|
298
|
+
],
|
|
299
|
+
async run(context) {
|
|
300
|
+
const start = performance.now();
|
|
301
|
+
const { rootDirectory, config } = context;
|
|
302
|
+
let rules;
|
|
303
|
+
try {
|
|
304
|
+
rules = await loadRules(rootDirectory);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
engine: this.name,
|
|
308
|
+
diagnostics: [],
|
|
309
|
+
elapsed: performance.now() - start,
|
|
310
|
+
skipped: true,
|
|
311
|
+
skipReason: `Failed to load rules: ${err instanceof Error ? err.message : String(err)}`
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (rules.length === 0) return {
|
|
315
|
+
engine: this.name,
|
|
316
|
+
diagnostics: [],
|
|
317
|
+
elapsed: performance.now() - start,
|
|
318
|
+
skipped: true,
|
|
319
|
+
skipReason: "No rules defined in .deep-slop/rules.yml"
|
|
320
|
+
};
|
|
321
|
+
const files = await collectFiles(rootDirectory, [
|
|
322
|
+
"typescript",
|
|
323
|
+
"javascript",
|
|
324
|
+
"python",
|
|
325
|
+
"go"
|
|
326
|
+
], config.exclude, context.files);
|
|
327
|
+
if (files.length === 0) return {
|
|
328
|
+
engine: this.name,
|
|
329
|
+
diagnostics: [],
|
|
330
|
+
elapsed: performance.now() - start,
|
|
331
|
+
skipped: true,
|
|
332
|
+
skipReason: "No files found for supported languages (ts, js, py, go)"
|
|
333
|
+
};
|
|
334
|
+
const diagnostics = [];
|
|
335
|
+
for (const absPath of files) {
|
|
336
|
+
const relPath = relative(rootDirectory, absPath);
|
|
337
|
+
let content;
|
|
338
|
+
try {
|
|
339
|
+
content = await readFileContent(absPath);
|
|
340
|
+
} catch {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
for (const rule of rules) {
|
|
344
|
+
const ruleDiagnostics = applyRule(rule, content, relPath);
|
|
345
|
+
diagnostics.push(...ruleDiagnostics);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
engine: this.name,
|
|
350
|
+
diagnostics,
|
|
351
|
+
elapsed: performance.now() - start,
|
|
352
|
+
skipped: false
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
export { archRulesEngine };
|