unguard 0.8.0 → 0.9.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/README.md +73 -19
- package/dist/{chunk-YYX4R25B.js → chunk-I4RZLVE2.js} +262 -63
- package/dist/chunk-I4RZLVE2.js.map +1 -0
- package/dist/cli.js +198 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -3
- package/dist/index.js +5 -1
- package/package.json +7 -3
- package/dist/chunk-YYX4R25B.js.map +0 -1
package/README.md
CHANGED
|
@@ -25,31 +25,71 @@ npx unguard
|
|
|
25
25
|
## Usage
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
unguard src
|
|
29
|
-
unguard src --
|
|
30
|
-
unguard src --
|
|
31
|
-
unguard src --
|
|
32
|
-
unguard src --
|
|
33
|
-
unguard src --
|
|
28
|
+
unguard src # scan files/directories
|
|
29
|
+
unguard src --config ./unguard.config.json # load config
|
|
30
|
+
unguard src --ignore '**/*.gen.ts' # add ignore globs
|
|
31
|
+
unguard src --filter no-any-cast # run a single rule
|
|
32
|
+
unguard src --rule duplicate-*=warning # override rule severity/policy
|
|
33
|
+
unguard src --rule category:cross-file=warning
|
|
34
|
+
unguard src --rule tag:safety=error
|
|
35
|
+
unguard src --severity=error,warning # show errors+warnings
|
|
36
|
+
unguard src --fail-on=error # fail only on errors
|
|
37
|
+
unguard src --format=flat # one-line-per-diagnostic, grepable
|
|
34
38
|
unguard src --format=flat | grep error
|
|
35
39
|
```
|
|
36
40
|
|
|
37
41
|
Add `unguard` to your lint check, especially if code is written by AI.
|
|
38
42
|
|
|
43
|
+
### Config
|
|
44
|
+
|
|
45
|
+
`unguard` automatically loads `./unguard.config.json` (or `./.unguardrc.json`). Use `--config <path>` to specify another file.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"paths": ["src", "apps/web/src"],
|
|
50
|
+
"ignore": ["**/*.gen.ts", "**/routeTree.gen.ts"],
|
|
51
|
+
"rules": {
|
|
52
|
+
"duplicate-*": "warning",
|
|
53
|
+
"category:cross-file": "warning",
|
|
54
|
+
"tag:safety": "error",
|
|
55
|
+
"no-ts-ignore": "error",
|
|
56
|
+
"prefer-*": "off"
|
|
57
|
+
},
|
|
58
|
+
"failOn": "error"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`rules` values can be `off`, `info`, `warning`, or `error`.
|
|
63
|
+
Selectors support:
|
|
64
|
+
- exact rule id: `no-ts-ignore`
|
|
65
|
+
- wildcard: `duplicate-*`
|
|
66
|
+
- category: `category:cross-file`
|
|
67
|
+
- tag: `tag:safety`
|
|
68
|
+
|
|
69
|
+
### Ignore behavior
|
|
70
|
+
|
|
71
|
+
`unguard` ignores:
|
|
72
|
+
- built-in: `node_modules`, `dist`, `.git`, declaration files (`*.d.ts`, `*.d.cts`, `*.d.mts`)
|
|
73
|
+
- generated files: `*.gen.*`, `*.generated.*`
|
|
74
|
+
- project `.gitignore`
|
|
75
|
+
- anything passed via `--ignore` or config `ignore`
|
|
76
|
+
|
|
39
77
|
### Exit codes
|
|
40
78
|
|
|
41
79
|
| Code | Meaning |
|
|
42
80
|
| ---- | ------- |
|
|
43
|
-
| 0 | No issues |
|
|
44
|
-
| 1 |
|
|
45
|
-
| 2 |
|
|
81
|
+
| 0 | No issues, or issues below `--fail-on` threshold |
|
|
82
|
+
| 1 | Failing diagnostics without errors |
|
|
83
|
+
| 2 | Failing diagnostics with at least one error |
|
|
46
84
|
|
|
47
|
-
Use `--
|
|
85
|
+
Use `--fail-on=error` in CI to fail only on errors while still showing all diagnostics:
|
|
48
86
|
|
|
49
87
|
```bash
|
|
50
|
-
unguard src --
|
|
88
|
+
unguard src --fail-on=error
|
|
51
89
|
```
|
|
52
90
|
|
|
91
|
+
`--severity` filters display only. `--fail-on` evaluates all diagnostics after rule policy.
|
|
92
|
+
|
|
53
93
|
### Output formats
|
|
54
94
|
|
|
55
95
|
**Grouped** (default) -- diagnostics grouped by file:
|
|
@@ -112,10 +152,10 @@ These rules use the TypeScript type checker. Non-nullable types suppress the dia
|
|
|
112
152
|
|
|
113
153
|
| Rule | Severity | What it catches |
|
|
114
154
|
| ---- | -------- | --------------- |
|
|
115
|
-
| `duplicate-type-declaration` |
|
|
116
|
-
| `duplicate-type-name` |
|
|
117
|
-
| `duplicate-function-declaration` |
|
|
118
|
-
| `duplicate-function-name` |
|
|
155
|
+
| `duplicate-type-declaration` | warning | Same type shape in multiple files |
|
|
156
|
+
| `duplicate-type-name` | warning | Same exported type name, different shapes |
|
|
157
|
+
| `duplicate-function-declaration` | warning | Same function body in multiple files |
|
|
158
|
+
| `duplicate-function-name` | warning | Same exported function name, different bodies |
|
|
119
159
|
| `optional-arg-always-used` | warning | Optional param provided at every call site |
|
|
120
160
|
| `explicit-null-arg` | warning | `fn(null)` / `fn(undefined)` to project functions |
|
|
121
161
|
|
|
@@ -141,10 +181,24 @@ file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for u
|
|
|
141
181
|
## API
|
|
142
182
|
|
|
143
183
|
```typescript
|
|
144
|
-
import { scan } from "unguard";
|
|
145
|
-
|
|
146
|
-
const result = await scan({ paths: ["src/"] });
|
|
147
|
-
|
|
184
|
+
import { executeScan, scan } from "unguard";
|
|
185
|
+
|
|
186
|
+
const result = await scan({ paths: ["src/"] }); // raw diagnostics
|
|
187
|
+
const execution = await executeScan({
|
|
188
|
+
paths: ["src"],
|
|
189
|
+
ignore: ["**/*.gen.ts"],
|
|
190
|
+
rulePolicy: {
|
|
191
|
+
"duplicate-*": "warning",
|
|
192
|
+
"category:cross-file": "warning",
|
|
193
|
+
"tag:safety": "error",
|
|
194
|
+
"prefer-*": "off",
|
|
195
|
+
},
|
|
196
|
+
showSeverities: ["error", "warning"],
|
|
197
|
+
failOn: "error",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log(execution.exitCode);
|
|
201
|
+
for (const d of execution.visibleDiagnostics) {
|
|
148
202
|
console.log(`${d.file}:${d.line} [${d.ruleId}] ${d.message}`);
|
|
149
203
|
}
|
|
150
204
|
```
|
|
@@ -630,7 +630,7 @@ var preferRequiredParamWithGuard = {
|
|
|
630
630
|
// src/rules/cross-file/duplicate-type-declaration.ts
|
|
631
631
|
var duplicateTypeDeclaration = {
|
|
632
632
|
id: "duplicate-type-declaration",
|
|
633
|
-
severity: "
|
|
633
|
+
severity: "warning",
|
|
634
634
|
message: "Identical type shape declared in multiple files; consolidate to a single definition",
|
|
635
635
|
analyze(project) {
|
|
636
636
|
const diagnostics = [];
|
|
@@ -658,7 +658,7 @@ var duplicateTypeDeclaration = {
|
|
|
658
658
|
import * as ts19 from "typescript";
|
|
659
659
|
var duplicateFunctionDeclaration = {
|
|
660
660
|
id: "duplicate-function-declaration",
|
|
661
|
-
severity: "
|
|
661
|
+
severity: "warning",
|
|
662
662
|
message: "Identical function body declared in multiple files; consolidate to a single definition",
|
|
663
663
|
analyze(project) {
|
|
664
664
|
const diagnostics = [];
|
|
@@ -862,7 +862,7 @@ import { dirname, resolve } from "path";
|
|
|
862
862
|
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
863
863
|
var duplicateFunctionName = {
|
|
864
864
|
id: "duplicate-function-name",
|
|
865
|
-
severity: "
|
|
865
|
+
severity: "warning",
|
|
866
866
|
message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
867
867
|
analyze(project) {
|
|
868
868
|
const diagnostics = [];
|
|
@@ -911,7 +911,7 @@ function resolveCandidates(fromFile, specifier) {
|
|
|
911
911
|
const candidates = [base];
|
|
912
912
|
for (const ext of EXTENSIONS) {
|
|
913
913
|
candidates.push(base + ext);
|
|
914
|
-
candidates.push(resolve(base,
|
|
914
|
+
candidates.push(resolve(base, `index${ext}`));
|
|
915
915
|
}
|
|
916
916
|
return candidates;
|
|
917
917
|
}
|
|
@@ -920,7 +920,7 @@ function resolveCandidates(fromFile, specifier) {
|
|
|
920
920
|
import * as ts23 from "typescript";
|
|
921
921
|
var duplicateTypeName = {
|
|
922
922
|
id: "duplicate-type-name",
|
|
923
|
-
severity: "
|
|
923
|
+
severity: "warning",
|
|
924
924
|
message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
925
925
|
analyze(project) {
|
|
926
926
|
const diagnostics = [];
|
|
@@ -991,15 +991,93 @@ var allRules = [
|
|
|
991
991
|
duplicateTypeName,
|
|
992
992
|
noDynamicImport
|
|
993
993
|
];
|
|
994
|
+
var ruleMetadata = {
|
|
995
|
+
"no-any-cast": { category: "type-evasion", tags: ["safety"] },
|
|
996
|
+
"no-explicit-any-annotation": { category: "type-evasion", tags: ["safety"] },
|
|
997
|
+
"no-type-assertion": { category: "type-evasion", tags: ["safety"] },
|
|
998
|
+
"no-ts-ignore": { category: "type-evasion", tags: ["safety"] },
|
|
999
|
+
"no-optional-property-access": { category: "defensive-code", tags: ["type-aware"] },
|
|
1000
|
+
"no-optional-element-access": { category: "defensive-code", tags: ["type-aware"] },
|
|
1001
|
+
"no-optional-call": { category: "defensive-code", tags: ["type-aware"] },
|
|
1002
|
+
"no-nullish-coalescing": { category: "defensive-code", tags: ["type-aware"] },
|
|
1003
|
+
"no-logical-or-fallback": { category: "defensive-code", tags: ["type-aware"] },
|
|
1004
|
+
"no-null-ternary-normalization": { category: "defensive-code", tags: ["type-aware"] },
|
|
1005
|
+
"no-non-null-assertion": { category: "defensive-code", tags: ["type-aware"] },
|
|
1006
|
+
"no-double-negation-coercion": { category: "defensive-code", tags: ["readability"] },
|
|
1007
|
+
"no-redundant-existence-guard": { category: "defensive-code", tags: ["type-aware"] },
|
|
1008
|
+
"no-empty-catch": { category: "error-handling", tags: ["safety"] },
|
|
1009
|
+
"no-catch-return": { category: "error-handling", tags: ["safety"] },
|
|
1010
|
+
"no-error-rewrap": { category: "error-handling", tags: ["safety"] },
|
|
1011
|
+
"no-inline-type-in-params": { category: "interface-design", tags: ["api"] },
|
|
1012
|
+
"prefer-default-param-value": { category: "interface-design", tags: ["api"] },
|
|
1013
|
+
"prefer-required-param-with-guard": { category: "interface-design", tags: ["api"] },
|
|
1014
|
+
"duplicate-type-declaration": { category: "cross-file", tags: ["duplicate"] },
|
|
1015
|
+
"duplicate-type-name": { category: "cross-file", tags: ["duplicate"] },
|
|
1016
|
+
"duplicate-function-declaration": { category: "cross-file", tags: ["duplicate"] },
|
|
1017
|
+
"duplicate-function-name": { category: "cross-file", tags: ["duplicate"] },
|
|
1018
|
+
"optional-arg-always-used": { category: "cross-file", tags: ["api"] },
|
|
1019
|
+
"explicit-null-arg": { category: "cross-file", tags: ["api"] },
|
|
1020
|
+
"no-dynamic-import": { category: "imports", tags: ["safety"] }
|
|
1021
|
+
};
|
|
1022
|
+
function getRuleMetadata(ruleId) {
|
|
1023
|
+
const metadata = ruleMetadata[ruleId];
|
|
1024
|
+
if (metadata !== void 0) return metadata;
|
|
1025
|
+
return { category: "cross-file", tags: [] };
|
|
1026
|
+
}
|
|
994
1027
|
|
|
995
|
-
// src/
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1028
|
+
// src/scan/config.ts
|
|
1029
|
+
var BUILTIN_IGNORE = [
|
|
1030
|
+
"**/node_modules/**",
|
|
1031
|
+
"**/dist/**",
|
|
1032
|
+
"**/.git/**",
|
|
1033
|
+
"**/*.d.ts",
|
|
1034
|
+
"**/*.d.cts",
|
|
1035
|
+
"**/*.d.mts"
|
|
1036
|
+
];
|
|
1037
|
+
var GENERATED_IGNORE = [
|
|
1038
|
+
"**/*.gen.*",
|
|
1039
|
+
"**/*.generated.*"
|
|
1040
|
+
];
|
|
1041
|
+
var DEFAULT_FAIL_ON = "info";
|
|
1042
|
+
function resolveScanConfig(options) {
|
|
1043
|
+
const paths = options.paths.length > 0 ? options.paths : ["."];
|
|
1044
|
+
const ignore2 = [...BUILTIN_IGNORE, ...GENERATED_IGNORE, ...options.ignore ?? []];
|
|
1045
|
+
return {
|
|
1046
|
+
paths,
|
|
1047
|
+
strict: options.strict ?? false,
|
|
1048
|
+
rules: options.rules ? [...options.rules] : null,
|
|
1049
|
+
ignore: ignore2,
|
|
1050
|
+
rulePolicy: toRulePolicyEntries(options.rulePolicy),
|
|
1051
|
+
showSeverities: normalizeSeveritySet(options.showSeverities),
|
|
1052
|
+
failOn: options.failOn ?? DEFAULT_FAIL_ON,
|
|
1053
|
+
useGitIgnore: options.useGitIgnore ?? true
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function toRulePolicyEntries(policy) {
|
|
1057
|
+
if (policy === void 0) return [];
|
|
1058
|
+
if (Array.isArray(policy)) return [...policy];
|
|
1059
|
+
const entries = [];
|
|
1060
|
+
for (const [selector, severity] of Object.entries(policy)) {
|
|
1061
|
+
entries.push({ selector, severity });
|
|
1062
|
+
}
|
|
1063
|
+
return entries;
|
|
1002
1064
|
}
|
|
1065
|
+
function normalizeSeveritySet(levels) {
|
|
1066
|
+
if (levels === void 0 || levels.length === 0) return null;
|
|
1067
|
+
return new Set(levels);
|
|
1068
|
+
}
|
|
1069
|
+
function isRulePolicySeverity(value) {
|
|
1070
|
+
return value === "off" || value === "info" || value === "warning" || value === "error";
|
|
1071
|
+
}
|
|
1072
|
+
function isSeverity(value) {
|
|
1073
|
+
return value === "info" || value === "warning" || value === "error";
|
|
1074
|
+
}
|
|
1075
|
+
function isFailOn(value) {
|
|
1076
|
+
return value === "none" || isSeverity(value);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/scan/analyze.ts
|
|
1080
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1003
1081
|
|
|
1004
1082
|
// src/collect/index.ts
|
|
1005
1083
|
import * as ts26 from "typescript";
|
|
@@ -1299,6 +1377,11 @@ function collectAllComments(sourceFile) {
|
|
|
1299
1377
|
return comments;
|
|
1300
1378
|
}
|
|
1301
1379
|
|
|
1380
|
+
// src/rules/types.ts
|
|
1381
|
+
function isTSRule(r) {
|
|
1382
|
+
return "kind" in r && r.kind === "ts";
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1302
1385
|
// src/typecheck/program.ts
|
|
1303
1386
|
import * as ts27 from "typescript";
|
|
1304
1387
|
import { dirname as dirname2 } from "path";
|
|
@@ -1388,86 +1471,67 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
|
1388
1471
|
};
|
|
1389
1472
|
}
|
|
1390
1473
|
|
|
1391
|
-
// src/
|
|
1392
|
-
|
|
1393
|
-
const
|
|
1394
|
-
const
|
|
1395
|
-
if (p === ".") return `./**/*.{ts,cts,mts,tsx}`;
|
|
1396
|
-
if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
|
|
1397
|
-
if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
|
|
1398
|
-
return `${p}/**/*.{ts,cts,mts,tsx}`;
|
|
1399
|
-
}
|
|
1400
|
-
return p;
|
|
1401
|
-
});
|
|
1402
|
-
const files = await fg(globs, {
|
|
1403
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.d.ts", "**/*.d.cts", "**/*.d.mts"],
|
|
1404
|
-
absolute: true
|
|
1405
|
-
});
|
|
1406
|
-
const activeRules = allRules.filter((r) => {
|
|
1407
|
-
if (options.rules && !options.rules.includes(r.id)) return false;
|
|
1408
|
-
return true;
|
|
1409
|
-
});
|
|
1410
|
-
const tsRules = activeRules.filter(isTSRule);
|
|
1411
|
-
const crossFileRules = activeRules.filter((r) => !isTSRule(r));
|
|
1474
|
+
// src/scan/analyze.ts
|
|
1475
|
+
function analyzeFiles(files, rules) {
|
|
1476
|
+
const tsRules = rules.filter(isTSRule);
|
|
1477
|
+
const crossFileRules = rules.filter((r) => !isTSRule(r));
|
|
1412
1478
|
const diagnostics = [];
|
|
1413
1479
|
const program = files.length > 0 ? createProgramFromFiles(files) : null;
|
|
1414
|
-
if (
|
|
1480
|
+
if (!program) return diagnostics;
|
|
1481
|
+
if (tsRules.length > 0) {
|
|
1415
1482
|
const checker = program.getTypeChecker();
|
|
1416
1483
|
for (const file of files) {
|
|
1417
1484
|
const source = readFileSync2(file, "utf8");
|
|
1418
1485
|
const sourceFile = program.getSourceFile(file);
|
|
1419
1486
|
if (sourceFile) {
|
|
1420
|
-
|
|
1421
|
-
diagnostics.push(...tsDiags);
|
|
1487
|
+
diagnostics.push(...runTSRules(tsRules, sourceFile, checker, source, file));
|
|
1422
1488
|
}
|
|
1423
1489
|
}
|
|
1424
1490
|
}
|
|
1425
|
-
if (crossFileRules.length > 0
|
|
1491
|
+
if (crossFileRules.length > 0) {
|
|
1426
1492
|
const projectIndex = collectProject(program);
|
|
1427
1493
|
for (const rule of crossFileRules) {
|
|
1428
|
-
const
|
|
1429
|
-
for (const
|
|
1430
|
-
const fileData = projectIndex.files.get(
|
|
1431
|
-
if (fileData !== void 0) annotate([
|
|
1494
|
+
const crossDiagnostics = rule.analyze(projectIndex);
|
|
1495
|
+
for (const diagnostic of crossDiagnostics) {
|
|
1496
|
+
const fileData = projectIndex.files.get(diagnostic.file);
|
|
1497
|
+
if (fileData !== void 0) annotate([diagnostic], fileData.comments, fileData.source);
|
|
1432
1498
|
}
|
|
1433
|
-
diagnostics.push(...
|
|
1499
|
+
diagnostics.push(...crossDiagnostics);
|
|
1434
1500
|
}
|
|
1435
1501
|
}
|
|
1502
|
+
return dedupeDiagnostics(diagnostics);
|
|
1503
|
+
}
|
|
1504
|
+
function dedupeDiagnostics(diagnostics) {
|
|
1436
1505
|
const seen = /* @__PURE__ */ new Set();
|
|
1437
1506
|
const deduped = [];
|
|
1438
|
-
for (const
|
|
1439
|
-
const key = `${
|
|
1507
|
+
for (const diagnostic of diagnostics) {
|
|
1508
|
+
const key = `${diagnostic.file}:${diagnostic.line}:${diagnostic.ruleId}`;
|
|
1440
1509
|
if (seen.has(key)) continue;
|
|
1441
1510
|
seen.add(key);
|
|
1442
|
-
deduped.push(
|
|
1443
|
-
}
|
|
1444
|
-
if (options.strict) {
|
|
1445
|
-
for (const d of deduped) {
|
|
1446
|
-
d.severity = "error";
|
|
1447
|
-
}
|
|
1511
|
+
deduped.push(diagnostic);
|
|
1448
1512
|
}
|
|
1449
|
-
return
|
|
1513
|
+
return deduped;
|
|
1450
1514
|
}
|
|
1451
1515
|
function annotate(diagnostics, comments, source) {
|
|
1452
1516
|
if (comments.length === 0 || diagnostics.length === 0) return;
|
|
1453
1517
|
const byEndLine = /* @__PURE__ */ new Map();
|
|
1454
|
-
for (const
|
|
1455
|
-
const endLine = lineAt(source,
|
|
1518
|
+
for (const comment of comments) {
|
|
1519
|
+
const endLine = lineAt(source, comment.end);
|
|
1456
1520
|
let list = byEndLine.get(endLine);
|
|
1457
1521
|
if (list === void 0) {
|
|
1458
1522
|
list = [];
|
|
1459
1523
|
byEndLine.set(endLine, list);
|
|
1460
1524
|
}
|
|
1461
|
-
list.push(
|
|
1525
|
+
list.push(comment);
|
|
1462
1526
|
}
|
|
1463
|
-
for (const
|
|
1464
|
-
const inline = findInlineComment(
|
|
1527
|
+
for (const diagnostic of diagnostics) {
|
|
1528
|
+
const inline = findInlineComment(diagnostic.line, byEndLine);
|
|
1465
1529
|
if (inline !== null) {
|
|
1466
|
-
|
|
1530
|
+
diagnostic.annotation = inline;
|
|
1467
1531
|
continue;
|
|
1468
1532
|
}
|
|
1469
|
-
const above = collectAnnotation(
|
|
1470
|
-
if (above !== null)
|
|
1533
|
+
const above = collectAnnotation(diagnostic.line - 1, byEndLine, source);
|
|
1534
|
+
if (above !== null) diagnostic.annotation = above;
|
|
1471
1535
|
}
|
|
1472
1536
|
}
|
|
1473
1537
|
function findInlineComment(diagLine, byEndLine) {
|
|
@@ -1485,9 +1549,7 @@ function collectAnnotation(commentEndLine, byEndLine, source) {
|
|
|
1485
1549
|
if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
|
|
1486
1550
|
const comment = commentsOnLine.at(-1);
|
|
1487
1551
|
if (comment === void 0) return null;
|
|
1488
|
-
if (comment.type === "Block")
|
|
1489
|
-
return cleanBlockComment(comment.value);
|
|
1490
|
-
}
|
|
1552
|
+
if (comment.type === "Block") return cleanBlockComment(comment.value);
|
|
1491
1553
|
const lines = [comment.value.trim()];
|
|
1492
1554
|
let prevLine = commentEndLine - 1;
|
|
1493
1555
|
for (; ; ) {
|
|
@@ -1512,8 +1574,145 @@ function lineAt(source, offset) {
|
|
|
1512
1574
|
return line;
|
|
1513
1575
|
}
|
|
1514
1576
|
|
|
1577
|
+
// src/scan/discover.ts
|
|
1578
|
+
import fg from "fast-glob";
|
|
1579
|
+
import ignore from "ignore";
|
|
1580
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
1581
|
+
import { relative, resolve as resolve2, sep } from "path";
|
|
1582
|
+
async function discoverFiles(config) {
|
|
1583
|
+
const globs = expandGlobs(config.paths);
|
|
1584
|
+
const discoveredFiles = await fg(globs, {
|
|
1585
|
+
ignore: config.ignore,
|
|
1586
|
+
absolute: true
|
|
1587
|
+
});
|
|
1588
|
+
if (!config.useGitIgnore) return discoveredFiles;
|
|
1589
|
+
return applyGitIgnore(discoveredFiles);
|
|
1590
|
+
}
|
|
1591
|
+
function expandGlobs(paths) {
|
|
1592
|
+
return paths.map((p) => {
|
|
1593
|
+
if (p === ".") return "./**/*.{ts,cts,mts,tsx}";
|
|
1594
|
+
if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
|
|
1595
|
+
if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
|
|
1596
|
+
return `${p}/**/*.{ts,cts,mts,tsx}`;
|
|
1597
|
+
}
|
|
1598
|
+
return p;
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
function applyGitIgnore(files) {
|
|
1602
|
+
const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
|
|
1603
|
+
if (!existsSync(gitIgnorePath)) return files;
|
|
1604
|
+
const matcher = ignore().add(readFileSync3(gitIgnorePath, "utf8"));
|
|
1605
|
+
return files.filter((file) => {
|
|
1606
|
+
const rel = relative(process.cwd(), file);
|
|
1607
|
+
if (rel.startsWith("..")) return true;
|
|
1608
|
+
const normalized = rel.split(sep).join("/");
|
|
1609
|
+
return !matcher.ignores(normalized);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/scan/policy.ts
|
|
1614
|
+
function buildRuleDescriptors(rules) {
|
|
1615
|
+
return rules.map((rule) => {
|
|
1616
|
+
const metadata = getRuleMetadata(rule.id);
|
|
1617
|
+
return {
|
|
1618
|
+
rule,
|
|
1619
|
+
category: metadata.category,
|
|
1620
|
+
tags: metadata.tags
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
function resolveActiveRules(descriptors, config) {
|
|
1625
|
+
const selectedRules = config.rules ? new Set(config.rules) : null;
|
|
1626
|
+
const active = [];
|
|
1627
|
+
for (const descriptor of descriptors) {
|
|
1628
|
+
if (selectedRules && !selectedRules.has(descriptor.rule.id)) continue;
|
|
1629
|
+
const resolvedSeverity = resolveRuleSeverity(descriptor, config);
|
|
1630
|
+
if (resolvedSeverity === "off") continue;
|
|
1631
|
+
if (descriptor.rule.severity === resolvedSeverity) {
|
|
1632
|
+
active.push(descriptor.rule);
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
active.push({ ...descriptor.rule, severity: resolvedSeverity });
|
|
1636
|
+
}
|
|
1637
|
+
return active;
|
|
1638
|
+
}
|
|
1639
|
+
function resolveRuleSeverity(descriptor, config) {
|
|
1640
|
+
let severity = descriptor.rule.severity;
|
|
1641
|
+
for (const entry of config.rulePolicy) {
|
|
1642
|
+
if (!matchesSelector(descriptor, entry.selector)) continue;
|
|
1643
|
+
severity = entry.severity;
|
|
1644
|
+
}
|
|
1645
|
+
if (severity === "off") return "off";
|
|
1646
|
+
if (config.strict) return "error";
|
|
1647
|
+
return severity;
|
|
1648
|
+
}
|
|
1649
|
+
function matchesSelector(descriptor, selector) {
|
|
1650
|
+
if (selector.startsWith("category:")) {
|
|
1651
|
+
return descriptor.category === selector.slice("category:".length);
|
|
1652
|
+
}
|
|
1653
|
+
if (selector.startsWith("tag:")) {
|
|
1654
|
+
return descriptor.tags.includes(selector.slice("tag:".length));
|
|
1655
|
+
}
|
|
1656
|
+
if (selector === descriptor.rule.id) return true;
|
|
1657
|
+
if (!selector.includes("*")) return false;
|
|
1658
|
+
const regex = new RegExp(`^${escapeRegex(selector).replaceAll("\\*", ".*")}$`);
|
|
1659
|
+
return regex.test(descriptor.rule.id);
|
|
1660
|
+
}
|
|
1661
|
+
function escapeRegex(value) {
|
|
1662
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1663
|
+
}
|
|
1664
|
+
function finalizeScanResult(diagnostics, fileCount, config) {
|
|
1665
|
+
const visibleDiagnostics = config.showSeverities ? diagnostics.filter((d) => config.showSeverities?.has(d.severity)) : diagnostics;
|
|
1666
|
+
return {
|
|
1667
|
+
diagnostics,
|
|
1668
|
+
visibleDiagnostics,
|
|
1669
|
+
fileCount,
|
|
1670
|
+
exitCode: computeExitCode(diagnostics, config.failOn)
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
function toScanResult(execution) {
|
|
1674
|
+
return {
|
|
1675
|
+
diagnostics: execution.diagnostics,
|
|
1676
|
+
fileCount: execution.fileCount
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function computeExitCode(diagnostics, failOn) {
|
|
1680
|
+
if (failOn === "none") return 0;
|
|
1681
|
+
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
1682
|
+
const hasWarning = diagnostics.some((d) => d.severity === "warning");
|
|
1683
|
+
const hasInfo = diagnostics.some((d) => d.severity === "info");
|
|
1684
|
+
if (failOn === "error") return hasError ? 2 : 0;
|
|
1685
|
+
if (failOn === "warning") {
|
|
1686
|
+
if (hasError) return 2;
|
|
1687
|
+
return hasWarning ? 1 : 0;
|
|
1688
|
+
}
|
|
1689
|
+
if (hasError) return 2;
|
|
1690
|
+
if (hasWarning || hasInfo) return 1;
|
|
1691
|
+
return 0;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/engine.ts
|
|
1695
|
+
async function executeScan(options) {
|
|
1696
|
+
const config = resolveScanConfig(options);
|
|
1697
|
+
const files = await discoverFiles(config);
|
|
1698
|
+
const descriptors = buildRuleDescriptors(allRules);
|
|
1699
|
+
const activeRules = resolveActiveRules(descriptors, config);
|
|
1700
|
+
const diagnostics = analyzeFiles(files, activeRules);
|
|
1701
|
+
return finalizeScanResult(diagnostics, files.length, config);
|
|
1702
|
+
}
|
|
1703
|
+
async function scan(options) {
|
|
1704
|
+
const execution = await executeScan(options);
|
|
1705
|
+
return toScanResult(execution);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1515
1708
|
export {
|
|
1516
1709
|
allRules,
|
|
1710
|
+
getRuleMetadata,
|
|
1711
|
+
toRulePolicyEntries,
|
|
1712
|
+
isRulePolicySeverity,
|
|
1713
|
+
isSeverity,
|
|
1714
|
+
isFailOn,
|
|
1715
|
+
executeScan,
|
|
1517
1716
|
scan
|
|
1518
1717
|
};
|
|
1519
|
-
//# sourceMappingURL=chunk-
|
|
1718
|
+
//# sourceMappingURL=chunk-I4RZLVE2.js.map
|