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,624 @@
|
|
|
1
|
+
import { i as toLines, r as readFileContent, t as detectEncodingAnomalies } from "./file-utils-B_HFXhCs.js";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
//#region src/engines/syntax-deep/index.ts
|
|
6
|
+
/** Characters that don't need escaping inside a regex character class [...] */
|
|
7
|
+
const UNNECESSARY_CLASS_ESCAPES = new Set([
|
|
8
|
+
".",
|
|
9
|
+
"/",
|
|
10
|
+
"-",
|
|
11
|
+
" ",
|
|
12
|
+
":",
|
|
13
|
+
";",
|
|
14
|
+
",",
|
|
15
|
+
"!",
|
|
16
|
+
"#",
|
|
17
|
+
"%",
|
|
18
|
+
"&",
|
|
19
|
+
"(",
|
|
20
|
+
")",
|
|
21
|
+
"<",
|
|
22
|
+
"=",
|
|
23
|
+
">",
|
|
24
|
+
"@",
|
|
25
|
+
"]",
|
|
26
|
+
"^",
|
|
27
|
+
"_",
|
|
28
|
+
"`",
|
|
29
|
+
"{",
|
|
30
|
+
"}",
|
|
31
|
+
"|",
|
|
32
|
+
"~"
|
|
33
|
+
]);
|
|
34
|
+
/** Unicode control / invisible characters to flag as anomalies */
|
|
35
|
+
const UNICODE_ANOMALY_RANGES = [
|
|
36
|
+
{
|
|
37
|
+
lo: 0,
|
|
38
|
+
hi: 8,
|
|
39
|
+
name: "C0 control char"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
lo: 11,
|
|
43
|
+
hi: 12,
|
|
44
|
+
name: "C0 control char"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
lo: 14,
|
|
48
|
+
hi: 31,
|
|
49
|
+
name: "C0 control char"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
lo: 127,
|
|
53
|
+
hi: 127,
|
|
54
|
+
name: "DEL control char"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
lo: 8203,
|
|
58
|
+
hi: 8203,
|
|
59
|
+
name: "zero-width space"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
lo: 8204,
|
|
63
|
+
hi: 8205,
|
|
64
|
+
name: "zero-width joiner/non-joiner"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
lo: 8206,
|
|
68
|
+
hi: 8207,
|
|
69
|
+
name: "RTL/LTR mark"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
lo: 8234,
|
|
73
|
+
hi: 8238,
|
|
74
|
+
name: "RTL/LTR override/embedding"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
lo: 8294,
|
|
78
|
+
hi: 8297,
|
|
79
|
+
name: "isolate/override"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
lo: 65279,
|
|
83
|
+
hi: 65279,
|
|
84
|
+
name: "ZWNBSP/BOM"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
lo: 65529,
|
|
88
|
+
hi: 65531,
|
|
89
|
+
name: "interlinear annotation"
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
function makeDiagnostic(filePath, rule, severity, message, help, line, column, fixable, suggestion, detail) {
|
|
93
|
+
return {
|
|
94
|
+
filePath,
|
|
95
|
+
engine: "syntax-deep",
|
|
96
|
+
rule: `syntax-deep/${rule}`,
|
|
97
|
+
severity,
|
|
98
|
+
message,
|
|
99
|
+
help,
|
|
100
|
+
line,
|
|
101
|
+
column,
|
|
102
|
+
category: "syntax",
|
|
103
|
+
fixable,
|
|
104
|
+
suggestion,
|
|
105
|
+
detail
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Collect all files to scan from context */
|
|
109
|
+
async function collectFiles(context) {
|
|
110
|
+
if (context.files && context.files.length > 0) return context.files;
|
|
111
|
+
const root = context.rootDirectory;
|
|
112
|
+
const exclude = context.config.exclude ?? [];
|
|
113
|
+
const extensions = new Set([
|
|
114
|
+
".ts",
|
|
115
|
+
".tsx",
|
|
116
|
+
".js",
|
|
117
|
+
".jsx",
|
|
118
|
+
".mjs",
|
|
119
|
+
".cjs",
|
|
120
|
+
".py",
|
|
121
|
+
".go",
|
|
122
|
+
".rs",
|
|
123
|
+
".rb",
|
|
124
|
+
".php",
|
|
125
|
+
".java",
|
|
126
|
+
".json",
|
|
127
|
+
".yaml",
|
|
128
|
+
".yml",
|
|
129
|
+
".toml",
|
|
130
|
+
".md",
|
|
131
|
+
".css",
|
|
132
|
+
".scss",
|
|
133
|
+
".less",
|
|
134
|
+
".html",
|
|
135
|
+
".vue",
|
|
136
|
+
".svelte"
|
|
137
|
+
]);
|
|
138
|
+
const files = [];
|
|
139
|
+
async function walk(dir) {
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
const { readdir } = await import("node:fs/promises");
|
|
143
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const full = join(dir, entry.name);
|
|
149
|
+
const rel = relative(root, full);
|
|
150
|
+
if (exclude.some((pat) => rel.startsWith(pat) || entry.name === pat)) continue;
|
|
151
|
+
if (entry.isDirectory()) await walk(full);
|
|
152
|
+
else if (entry.isFile()) {
|
|
153
|
+
const ext = entry.name.substring(entry.name.lastIndexOf("."));
|
|
154
|
+
if (extensions.has(ext)) files.push(rel);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await walk(root);
|
|
159
|
+
return files;
|
|
160
|
+
}
|
|
161
|
+
/** Read raw buffer to detect BOM at byte level */
|
|
162
|
+
async function readRawBytes(filePath) {
|
|
163
|
+
const { readFile: readBuf } = await import("node:fs/promises");
|
|
164
|
+
return readBuf(filePath);
|
|
165
|
+
}
|
|
166
|
+
/** 1. BOM & ZWNBSP detection */
|
|
167
|
+
function checkBomAndZwnbsp(content, filePath, rawBuf) {
|
|
168
|
+
const diagnostics = [];
|
|
169
|
+
const anomalies = detectEncodingAnomalies(content);
|
|
170
|
+
if (rawBuf.length >= 3 && rawBuf[0] === 239 && rawBuf[1] === 187 && rawBuf[2] === 191) diagnostics.push(makeDiagnostic(filePath, "bom-present", "warning", "File starts with UTF-8 BOM (U+FEFF)", "BOM can cause issues with shebangs, concatenation, and some parsers. Remove it.", 1, 1, true, {
|
|
171
|
+
type: "replace",
|
|
172
|
+
text: content.startsWith("") ? content.slice(1) : content,
|
|
173
|
+
range: {
|
|
174
|
+
startLine: 1,
|
|
175
|
+
startCol: 1,
|
|
176
|
+
endLine: 1,
|
|
177
|
+
endCol: 4
|
|
178
|
+
},
|
|
179
|
+
confidence: 1,
|
|
180
|
+
reason: "Strip BOM to normalize file encoding"
|
|
181
|
+
}));
|
|
182
|
+
if (anomalies.hasZwnbsp) {
|
|
183
|
+
const lines = toLines(content);
|
|
184
|
+
for (const { num, text } of lines) {
|
|
185
|
+
const idx = text.indexOf("");
|
|
186
|
+
if (idx !== -1) diagnostics.push(makeDiagnostic(filePath, "zwnbsp-mid-file", "warning", `Zero-width no-break space (U+FEFF) found at column ${idx + 1}`, "ZWNBSP mid-file is invisible and can cause subtle bugs. Remove it.", num, idx + 1, true, {
|
|
187
|
+
type: "replace",
|
|
188
|
+
text: text.replace(/\uFEFF/g, ""),
|
|
189
|
+
range: {
|
|
190
|
+
startLine: num,
|
|
191
|
+
startCol: idx + 1,
|
|
192
|
+
endLine: num,
|
|
193
|
+
endCol: idx + 2
|
|
194
|
+
},
|
|
195
|
+
confidence: 1,
|
|
196
|
+
reason: "Remove invisible ZWNBSP character"
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return diagnostics;
|
|
201
|
+
}
|
|
202
|
+
/** 2 & 3. Line ending checks: CRLF and mixed */
|
|
203
|
+
function checkLineEndings(content, filePath) {
|
|
204
|
+
const diagnostics = [];
|
|
205
|
+
const anomalies = detectEncodingAnomalies(content);
|
|
206
|
+
if (anomalies.lineEnding === "crlf") {
|
|
207
|
+
const lines = content.split("\n");
|
|
208
|
+
let firstCrlfLine = 0;
|
|
209
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].endsWith("\r")) {
|
|
210
|
+
firstCrlfLine = i + 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
diagnostics.push(makeDiagnostic(filePath, "crlf-line-endings", "warning", "File uses CRLF (\\r\\n) line endings throughout", "Normalize to LF for cross-platform consistency. Git core.autocrlf can mask this.", firstCrlfLine || 1, 1, true, {
|
|
214
|
+
type: "replace",
|
|
215
|
+
text: content.replace(/\r\n/g, "\n"),
|
|
216
|
+
confidence: 1,
|
|
217
|
+
reason: "Normalize CRLF to LF"
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
if (anomalies.lineEnding === "mixed") {
|
|
221
|
+
const crlfCount = (content.match(/\r\n/g) ?? []).length;
|
|
222
|
+
const lfOnlyCount = (content.match(/(?<!\r)\n/g) ?? []).length;
|
|
223
|
+
const lines = content.split("\n");
|
|
224
|
+
let firstMixedLine = 0;
|
|
225
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].endsWith("\r")) {
|
|
226
|
+
firstMixedLine = i + 1;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
diagnostics.push(makeDiagnostic(filePath, "mixed-line-endings", "warning", `Mixed line endings: ${crlfCount} CRLF and ${lfOnlyCount} LF`, "Mixed line endings cause noisy git diffs and can break some tools. Normalize to LF.", firstMixedLine || 1, 1, true, {
|
|
230
|
+
type: "replace",
|
|
231
|
+
text: content.replace(/\r\n/g, "\n"),
|
|
232
|
+
confidence: 1,
|
|
233
|
+
reason: "Normalize all line endings to LF"
|
|
234
|
+
}, {
|
|
235
|
+
crlfCount,
|
|
236
|
+
lfOnlyCount
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
return diagnostics;
|
|
240
|
+
}
|
|
241
|
+
/** 4. Invalid escape sequences in JS/TS string literals */
|
|
242
|
+
function checkInvalidEscapes(content, filePath) {
|
|
243
|
+
const diagnostics = [];
|
|
244
|
+
const lines = toLines(content);
|
|
245
|
+
const escapeInStringRe = /(["'`])(?:[^\\]|\\.)*?\1/g;
|
|
246
|
+
for (const { num, text } of lines) {
|
|
247
|
+
const trimmed = text.trimStart();
|
|
248
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
249
|
+
let match;
|
|
250
|
+
escapeInStringRe.lastIndex = 0;
|
|
251
|
+
while ((match = escapeInStringRe.exec(text)) !== null) {
|
|
252
|
+
const fullMatch = match[0];
|
|
253
|
+
const quote = match[1];
|
|
254
|
+
const startPos = match.index;
|
|
255
|
+
for (let i = 1; i < fullMatch.length - 1; i++) if (fullMatch[i] === "\\") {
|
|
256
|
+
const nextChar = fullMatch[i + 1];
|
|
257
|
+
if (!nextChar) continue;
|
|
258
|
+
if ([
|
|
259
|
+
"n",
|
|
260
|
+
"r",
|
|
261
|
+
"t",
|
|
262
|
+
"b",
|
|
263
|
+
"f",
|
|
264
|
+
"v",
|
|
265
|
+
"\\",
|
|
266
|
+
"'",
|
|
267
|
+
"\"",
|
|
268
|
+
"`",
|
|
269
|
+
"0"
|
|
270
|
+
].includes(nextChar)) continue;
|
|
271
|
+
if (nextChar === "x") {
|
|
272
|
+
const hexPart = fullMatch.substring(i + 2, i + 4);
|
|
273
|
+
if (/^[0-9a-fA-F]{2}$/.test(hexPart)) {
|
|
274
|
+
i += 3;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
diagnostics.push(makeDiagnostic(filePath, "invalid-escape-sequence", "warning", `Invalid escape sequence \\x without 2 hex digits`, "Use \\x followed by exactly 2 hex digits, or escape the backslash (\\\\x).", num, startPos + i + 1, true, {
|
|
278
|
+
type: "replace",
|
|
279
|
+
text: `\\x`,
|
|
280
|
+
range: {
|
|
281
|
+
startLine: num,
|
|
282
|
+
startCol: startPos + i + 1,
|
|
283
|
+
endLine: num,
|
|
284
|
+
endCol: startPos + i + 3
|
|
285
|
+
},
|
|
286
|
+
confidence: .9,
|
|
287
|
+
reason: "Fix invalid hex escape"
|
|
288
|
+
}));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (nextChar === "u") {
|
|
292
|
+
const rest = fullMatch.substring(i + 2);
|
|
293
|
+
if (/^\{[0-9a-fA-F]+\}/.test(rest) || /^[0-9a-fA-F]{4}/.test(rest)) {
|
|
294
|
+
const unicodeMatch = rest.match(/^(\{[0-9a-fA-F]+\}|[0-9a-fA-F]{4})/);
|
|
295
|
+
if (unicodeMatch) i += 1 + unicodeMatch[0].length;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (nextChar === "\n" || nextChar === "\r") continue;
|
|
301
|
+
if (/[sdwDWbBgBpP]/.test(nextChar)) diagnostics.push(makeDiagnostic(filePath, "regex-escape-in-string", "warning", `Escape sequence \\${nextChar} is a regex token but appears in a ${quote}-quoted string`, `If this is meant as a regex pattern, use a regex literal /\\${nextChar}/ instead of a string. If the backslash is literal, escape it as \\\\${nextChar}.`, num, startPos + i + 1, true, {
|
|
302
|
+
type: "replace",
|
|
303
|
+
text: `\\\\${nextChar}`,
|
|
304
|
+
range: {
|
|
305
|
+
startLine: num,
|
|
306
|
+
startCol: startPos + i + 1,
|
|
307
|
+
endLine: num,
|
|
308
|
+
endCol: startPos + i + 3
|
|
309
|
+
},
|
|
310
|
+
confidence: .7,
|
|
311
|
+
reason: "Likely regex-as-string confusion; escape the backslash or use a regex literal"
|
|
312
|
+
}, {
|
|
313
|
+
escapeChar: nextChar,
|
|
314
|
+
quoteType: quote
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return diagnostics;
|
|
320
|
+
}
|
|
321
|
+
/** 5. Unnecessary regex escapes inside character classes */
|
|
322
|
+
function checkUnnecessaryRegexEscapes(content, filePath) {
|
|
323
|
+
const diagnostics = [];
|
|
324
|
+
const lines = toLines(content);
|
|
325
|
+
const charClassRe = /\[(?:[^\]\\]|\\.)*\]/g;
|
|
326
|
+
for (const { num, text } of lines) {
|
|
327
|
+
const regexLiteralRe = /\/(?:[^\\/]|\\.)+\/[gimsuy]*/g;
|
|
328
|
+
const regExpCtorRe = /new\s+RegExp\s*\(\s*(["'`])(?:[^\\]|\\.)*?\1/g;
|
|
329
|
+
const regexBodies = [];
|
|
330
|
+
let rmatch;
|
|
331
|
+
regexLiteralRe.lastIndex = 0;
|
|
332
|
+
while ((rmatch = regexLiteralRe.exec(text)) !== null) regexBodies.push({
|
|
333
|
+
start: rmatch.index,
|
|
334
|
+
body: rmatch[0],
|
|
335
|
+
kind: "literal"
|
|
336
|
+
});
|
|
337
|
+
regExpCtorRe.lastIndex = 0;
|
|
338
|
+
while ((rmatch = regExpCtorRe.exec(text)) !== null) regexBodies.push({
|
|
339
|
+
start: rmatch.index,
|
|
340
|
+
body: rmatch[0],
|
|
341
|
+
kind: "ctor"
|
|
342
|
+
});
|
|
343
|
+
for (const { start, body } of regexBodies) {
|
|
344
|
+
charClassRe.lastIndex = 0;
|
|
345
|
+
let cmatch;
|
|
346
|
+
while ((cmatch = charClassRe.exec(body)) !== null) {
|
|
347
|
+
const classContent = cmatch[0];
|
|
348
|
+
for (let i = 1; i < classContent.length - 1; i++) if (classContent[i] === "\\") {
|
|
349
|
+
const nextChar = classContent[i + 1];
|
|
350
|
+
if (!nextChar) continue;
|
|
351
|
+
if (UNNECESSARY_CLASS_ESCAPES.has(nextChar)) {
|
|
352
|
+
const absCol = start + cmatch.index + i + 1;
|
|
353
|
+
diagnostics.push(makeDiagnostic(filePath, "unnecessary-regex-class-escape", "info", `Unnecessary escape \\${nextChar} inside character class [${classContent}]`, `Inside [...], '${nextChar}' doesn't need escaping. Use ${nextChar} instead of \\${nextChar} for clarity.`, num, absCol + 1, true, {
|
|
354
|
+
type: "replace",
|
|
355
|
+
text: nextChar,
|
|
356
|
+
range: {
|
|
357
|
+
startLine: num,
|
|
358
|
+
startCol: absCol + 1,
|
|
359
|
+
endLine: num,
|
|
360
|
+
endCol: absCol + 3
|
|
361
|
+
},
|
|
362
|
+
confidence: .95,
|
|
363
|
+
reason: "Character doesn't need escaping inside a character class"
|
|
364
|
+
}, {
|
|
365
|
+
charClass: classContent,
|
|
366
|
+
escapedChar: nextChar
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return diagnostics;
|
|
374
|
+
}
|
|
375
|
+
/** 6. Number precision: floating point literals with >15 significant digits */
|
|
376
|
+
function checkNumberPrecision(content, filePath) {
|
|
377
|
+
const diagnostics = [];
|
|
378
|
+
const lines = toLines(content);
|
|
379
|
+
const numberRe = /\b(?:0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][01]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\b/g;
|
|
380
|
+
for (const { num, text } of lines) {
|
|
381
|
+
let match;
|
|
382
|
+
numberRe.lastIndex = 0;
|
|
383
|
+
while ((match = numberRe.exec(text)) !== null) {
|
|
384
|
+
const numStr = match[0];
|
|
385
|
+
if (/^0[xXoObB]/.test(numStr)) continue;
|
|
386
|
+
numStr.replace(/^0+/, "").replace(/[.eE].*$/, "").replace(/^0+/, "");
|
|
387
|
+
const significantDigits = numStr.split(/[eE]/)[0].replace(/^0+/, "").replace(".", "").replace(/^0+/, "").length;
|
|
388
|
+
if (significantDigits > 15) {
|
|
389
|
+
const value = parseFloat(numStr);
|
|
390
|
+
const backToString = value.toString();
|
|
391
|
+
if (backToString !== numStr && !backToString.startsWith(numStr)) diagnostics.push(makeDiagnostic(filePath, "precision-loss", "warning", `Numeric literal ${numStr} has ${significantDigits} significant digits — exceeds IEEE 754 double precision (15-17 digits)`, `Runtime value becomes ${value}. Use BigInt for integers or a decimal library for precise arithmetic.`, num, match.index + 1, false, void 0, {
|
|
392
|
+
original: numStr,
|
|
393
|
+
runtimeValue: value,
|
|
394
|
+
significantDigits
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return diagnostics;
|
|
400
|
+
}
|
|
401
|
+
/** 7. Unicode anomalies: control chars, zero-width, RTL overrides */
|
|
402
|
+
function checkUnicodeAnomalies(content, filePath) {
|
|
403
|
+
const diagnostics = [];
|
|
404
|
+
const lines = toLines(content);
|
|
405
|
+
for (const { num, text } of lines) for (let col = 0; col < text.length; col++) {
|
|
406
|
+
const cp = text.codePointAt(col);
|
|
407
|
+
for (const range of UNICODE_ANOMALY_RANGES) if (cp >= range.lo && cp <= range.hi) {
|
|
408
|
+
if (cp === 9 || cp === 10 || cp === 13) continue;
|
|
409
|
+
const hex = cp > 65535 ? `U+${cp.toString(16).toUpperCase().padStart(6, "0")}` : `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
410
|
+
diagnostics.push(makeDiagnostic(filePath, "unicode-anomaly", "warning", `Invisible/control character ${hex} (${range.name}) found at column ${col + 1}`, "This character is invisible in most editors and may indicate a homoglyph attack or copy-paste artifact. Remove it.", num, col + 1, true, {
|
|
411
|
+
type: "replace",
|
|
412
|
+
text: "",
|
|
413
|
+
range: {
|
|
414
|
+
startLine: num,
|
|
415
|
+
startCol: col + 1,
|
|
416
|
+
endLine: num,
|
|
417
|
+
endCol: col + 2
|
|
418
|
+
},
|
|
419
|
+
confidence: 1,
|
|
420
|
+
reason: "Remove invisible/control character"
|
|
421
|
+
}, {
|
|
422
|
+
codePoint: cp,
|
|
423
|
+
hex,
|
|
424
|
+
anomalyType: range.name
|
|
425
|
+
}));
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
if (cp > 65535) col++;
|
|
429
|
+
}
|
|
430
|
+
return diagnostics;
|
|
431
|
+
}
|
|
432
|
+
/** 8. Trailing whitespace */
|
|
433
|
+
function checkTrailingWhitespace(content, filePath) {
|
|
434
|
+
const diagnostics = [];
|
|
435
|
+
const lines = toLines(content);
|
|
436
|
+
for (const { num, text } of lines) {
|
|
437
|
+
const trailingMatch = text.match(/([ \t]+)$/);
|
|
438
|
+
if (trailingMatch) {
|
|
439
|
+
const trailingLen = trailingMatch[1].length;
|
|
440
|
+
const col = text.length - trailingLen + 1;
|
|
441
|
+
diagnostics.push(makeDiagnostic(filePath, "trailing-whitespace", "info", `Line has ${trailingLen} trailing whitespace character${trailingLen > 1 ? "s" : ""}`, "Remove trailing whitespace. It creates noisy diffs and wastes space.", num, col, true, {
|
|
442
|
+
type: "replace",
|
|
443
|
+
text: text.replace(/[ \t]+$/, ""),
|
|
444
|
+
range: {
|
|
445
|
+
startLine: num,
|
|
446
|
+
startCol: col,
|
|
447
|
+
endLine: num,
|
|
448
|
+
endCol: text.length + 1
|
|
449
|
+
},
|
|
450
|
+
confidence: 1,
|
|
451
|
+
reason: "Remove trailing whitespace"
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return diagnostics;
|
|
456
|
+
}
|
|
457
|
+
/** 9. Missing final newline */
|
|
458
|
+
function checkMissingFinalNewline(content, filePath) {
|
|
459
|
+
if (content.length === 0) return [];
|
|
460
|
+
if (!content.endsWith("\n")) {
|
|
461
|
+
const lineCount = content.split("\n").length;
|
|
462
|
+
return [makeDiagnostic(filePath, "missing-final-newline", "info", "File does not end with a newline (\\n)", "POSIX requires files to end with a newline. Some tools may misbehave without it.", lineCount, content.split("\n").pop().length + 1, true, {
|
|
463
|
+
type: "insert",
|
|
464
|
+
text: "\n",
|
|
465
|
+
range: {
|
|
466
|
+
startLine: lineCount,
|
|
467
|
+
startCol: content.split("\n").pop().length + 1,
|
|
468
|
+
endLine: lineCount,
|
|
469
|
+
endCol: content.split("\n").pop().length + 1
|
|
470
|
+
},
|
|
471
|
+
confidence: 1,
|
|
472
|
+
reason: "Add final newline"
|
|
473
|
+
})];
|
|
474
|
+
}
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
/** 10. Inconsistent indentation (mixed tabs and spaces) */
|
|
478
|
+
function checkInconsistentIndentation(content, filePath) {
|
|
479
|
+
const diagnostics = [];
|
|
480
|
+
const lines = toLines(content);
|
|
481
|
+
let tabIndented = 0;
|
|
482
|
+
let spaceIndented = 0;
|
|
483
|
+
const mixedLines = [];
|
|
484
|
+
for (const { num, text } of lines) {
|
|
485
|
+
if (text.length === 0) continue;
|
|
486
|
+
const indentMatch = text.match(/^([\t ]+)/);
|
|
487
|
+
if (!indentMatch) continue;
|
|
488
|
+
const indent = indentMatch[1];
|
|
489
|
+
const hasTab = indent.includes(" ");
|
|
490
|
+
const hasSpace = indent.includes(" ");
|
|
491
|
+
if (hasTab && hasSpace) mixedLines.push({
|
|
492
|
+
num,
|
|
493
|
+
indent
|
|
494
|
+
});
|
|
495
|
+
else if (hasTab) tabIndented++;
|
|
496
|
+
else if (hasSpace) spaceIndented++;
|
|
497
|
+
}
|
|
498
|
+
for (const { num, indent } of mixedLines) diagnostics.push(makeDiagnostic(filePath, "mixed-indent-line", "warning", `Line mixes tabs and spaces in indentation: ${JSON.stringify(indent)}`, "Use consistent indentation (either all tabs or all spaces) within the same file.", num, 1, true, {
|
|
499
|
+
type: "replace",
|
|
500
|
+
text: indent.replace(/\t/g, " "),
|
|
501
|
+
range: {
|
|
502
|
+
startLine: num,
|
|
503
|
+
startCol: 1,
|
|
504
|
+
endLine: num,
|
|
505
|
+
endCol: indent.length + 1
|
|
506
|
+
},
|
|
507
|
+
confidence: .8,
|
|
508
|
+
reason: "Normalize to consistent indentation"
|
|
509
|
+
}, { indent }));
|
|
510
|
+
if (mixedLines.length === 0 && tabIndented > 0 && spaceIndented > 0) {
|
|
511
|
+
const firstTabLine = lines.find((l) => l.text.match(/^\t/));
|
|
512
|
+
const firstSpaceLine = lines.find((l) => l.text.match(/^ /));
|
|
513
|
+
diagnostics.push(makeDiagnostic(filePath, "inconsistent-indent-style", "info", `File uses both tabs (${tabIndented} lines) and spaces (${spaceIndented} lines) for indentation`, "Pick one style and apply it consistently. Consider using an .editorconfig file.", firstTabLine?.num ?? firstSpaceLine?.num ?? 1, 1, true, void 0, {
|
|
514
|
+
tabLines: tabIndented,
|
|
515
|
+
spaceLines: spaceIndented
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
return diagnostics;
|
|
519
|
+
}
|
|
520
|
+
const syntaxDeepEngine = {
|
|
521
|
+
name: "syntax-deep",
|
|
522
|
+
description: "Detects syntax-level anomalies that cause subtle bugs: BOM, mixed line endings, invalid escapes, precision loss, unicode anomalies, and formatting inconsistencies.",
|
|
523
|
+
supportedLanguages: [
|
|
524
|
+
"typescript",
|
|
525
|
+
"javascript",
|
|
526
|
+
"python",
|
|
527
|
+
"go",
|
|
528
|
+
"rust",
|
|
529
|
+
"ruby",
|
|
530
|
+
"php",
|
|
531
|
+
"java"
|
|
532
|
+
],
|
|
533
|
+
async run(context) {
|
|
534
|
+
const start = performance.now();
|
|
535
|
+
const diagnostics = [];
|
|
536
|
+
const files = await collectFiles(context);
|
|
537
|
+
for (const relPath of files) {
|
|
538
|
+
const absPath = join(context.rootDirectory, relPath);
|
|
539
|
+
let content;
|
|
540
|
+
let rawBuf;
|
|
541
|
+
try {
|
|
542
|
+
rawBuf = await readRawBytes(absPath);
|
|
543
|
+
content = rawBuf.toString("utf-8");
|
|
544
|
+
} catch {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
diagnostics.push(...checkBomAndZwnbsp(content, relPath, rawBuf));
|
|
548
|
+
diagnostics.push(...checkLineEndings(content, relPath));
|
|
549
|
+
diagnostics.push(...checkInvalidEscapes(content, relPath));
|
|
550
|
+
diagnostics.push(...checkUnnecessaryRegexEscapes(content, relPath));
|
|
551
|
+
diagnostics.push(...checkNumberPrecision(content, relPath));
|
|
552
|
+
diagnostics.push(...checkUnicodeAnomalies(content, relPath));
|
|
553
|
+
diagnostics.push(...checkTrailingWhitespace(content, relPath));
|
|
554
|
+
diagnostics.push(...checkMissingFinalNewline(content, relPath));
|
|
555
|
+
diagnostics.push(...checkInconsistentIndentation(content, relPath));
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
engine: "syntax-deep",
|
|
559
|
+
diagnostics,
|
|
560
|
+
elapsed: performance.now() - start,
|
|
561
|
+
skipped: false
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
async fix(diagnostics, context) {
|
|
565
|
+
const fixed = [];
|
|
566
|
+
const remaining = [];
|
|
567
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const d of diagnostics) {
|
|
569
|
+
if (!byFile.has(d.filePath)) byFile.set(d.filePath, []);
|
|
570
|
+
byFile.get(d.filePath).push(d);
|
|
571
|
+
}
|
|
572
|
+
const modifiedFiles = [];
|
|
573
|
+
const fixableRules = new Set([
|
|
574
|
+
"syntax-deep/bom-present",
|
|
575
|
+
"syntax-deep/zwnbsp-mid-file",
|
|
576
|
+
"syntax-deep/crlf-line-endings",
|
|
577
|
+
"syntax-deep/mixed-line-endings",
|
|
578
|
+
"syntax-deep/trailing-whitespace",
|
|
579
|
+
"syntax-deep/missing-final-newline"
|
|
580
|
+
]);
|
|
581
|
+
for (const [filePath, fileDiagnostics] of byFile) {
|
|
582
|
+
if (!fileDiagnostics.some((d) => fixableRules.has(d.rule))) {
|
|
583
|
+
remaining.push(...fileDiagnostics);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const absPath = join(context.rootDirectory, filePath);
|
|
587
|
+
let content;
|
|
588
|
+
try {
|
|
589
|
+
content = await readFileContent(absPath);
|
|
590
|
+
} catch {
|
|
591
|
+
remaining.push(...fileDiagnostics);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
let modified = content;
|
|
595
|
+
if (fileDiagnostics.some((d) => d.rule === "syntax-deep/bom-present")) {
|
|
596
|
+
if (modified.charCodeAt(0) === 65279) modified = modified.slice(1);
|
|
597
|
+
}
|
|
598
|
+
if (fileDiagnostics.some((d) => d.rule === "syntax-deep/zwnbsp-mid-file")) modified = modified.replace(/\uFEFF/g, "");
|
|
599
|
+
if (fileDiagnostics.some((d) => d.rule === "syntax-deep/crlf-line-endings" || d.rule === "syntax-deep/mixed-line-endings")) modified = modified.replace(/\r\n/g, "\n");
|
|
600
|
+
if (fileDiagnostics.some((d) => d.rule === "syntax-deep/trailing-whitespace")) modified = modified.replace(/[ \t]+$/gm, "");
|
|
601
|
+
if (fileDiagnostics.some((d) => d.rule === "syntax-deep/missing-final-newline")) {
|
|
602
|
+
if (modified.length > 0 && !modified.endsWith("\n")) modified += "\n";
|
|
603
|
+
}
|
|
604
|
+
if (modified !== content) try {
|
|
605
|
+
await writeFile(absPath, modified, "utf-8");
|
|
606
|
+
modifiedFiles.push(filePath);
|
|
607
|
+
for (const d of fileDiagnostics) if (fixableRules.has(d.rule)) fixed.push(d);
|
|
608
|
+
else remaining.push(d);
|
|
609
|
+
} catch {
|
|
610
|
+
remaining.push(...fileDiagnostics);
|
|
611
|
+
}
|
|
612
|
+
else for (const d of fileDiagnostics) if (fixableRules.has(d.rule)) fixed.push(d);
|
|
613
|
+
else remaining.push(d);
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
fixed: fixed.length,
|
|
617
|
+
remaining,
|
|
618
|
+
modifiedFiles
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
//#endregion
|
|
624
|
+
export { syntaxDeepEngine };
|