debtlens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +153 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +18 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/parseList.d.ts +2 -0
- package/dist/cli/parseList.js +29 -0
- package/dist/cli/parseList.js.map +1 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +40 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loadConfig.d.ts +2 -0
- package/dist/config/loadConfig.js +23 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/mergeConfig.d.ts +2 -0
- package/dist/config/mergeConfig.js +26 -0
- package/dist/config/mergeConfig.js.map +1 -0
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +63 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/template.d.ts +10 -0
- package/dist/config/template.js +32 -0
- package/dist/config/template.js.map +1 -0
- package/dist/core/baseline.d.ts +23 -0
- package/dist/core/baseline.js +118 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/scan.d.ts +2 -0
- package/dist/core/scan.js +129 -0
- package/dist/core/scan.js.map +1 -0
- package/dist/core/severity.d.ts +7 -0
- package/dist/core/severity.js +26 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/types.d.ts +96 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/detectors/deadAbstraction.d.ts +2 -0
- package/dist/detectors/deadAbstraction.js +115 -0
- package/dist/detectors/deadAbstraction.js.map +1 -0
- package/dist/detectors/duplicateLogic.d.ts +2 -0
- package/dist/detectors/duplicateLogic.js +81 -0
- package/dist/detectors/duplicateLogic.js.map +1 -0
- package/dist/detectors/effectComplexity.d.ts +2 -0
- package/dist/detectors/effectComplexity.js +64 -0
- package/dist/detectors/effectComplexity.js.map +1 -0
- package/dist/detectors/index.d.ts +3 -0
- package/dist/detectors/index.js +20 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/largeComponent.d.ts +2 -0
- package/dist/detectors/largeComponent.js +46 -0
- package/dist/detectors/largeComponent.js.map +1 -0
- package/dist/detectors/namingDrift.d.ts +10 -0
- package/dist/detectors/namingDrift.js +82 -0
- package/dist/detectors/namingDrift.js.map +1 -0
- package/dist/detectors/propDrilling.d.ts +2 -0
- package/dist/detectors/propDrilling.js +97 -0
- package/dist/detectors/propDrilling.js.map +1 -0
- package/dist/detectors/stateSprawl.d.ts +2 -0
- package/dist/detectors/stateSprawl.js +47 -0
- package/dist/detectors/stateSprawl.js.map +1 -0
- package/dist/detectors/todoComment.d.ts +2 -0
- package/dist/detectors/todoComment.js +45 -0
- package/dist/detectors/todoComment.js.map +1 -0
- package/dist/reporters/index.d.ts +4 -0
- package/dist/reporters/index.js +14 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +2 -0
- package/dist/reporters/jsonReporter.js +4 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +2 -0
- package/dist/reporters/markdownReporter.js +52 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/reporters/sarifReporter.d.ts +7 -0
- package/dist/reporters/sarifReporter.js +77 -0
- package/dist/reporters/sarifReporter.js.map +1 -0
- package/dist/reporters/terminalReporter.d.ts +4 -0
- package/dist/reporters/terminalReporter.js +39 -0
- package/dist/reporters/terminalReporter.js.map +1 -0
- package/dist/utils/ast.d.ts +23 -0
- package/dist/utils/ast.js +132 -0
- package/dist/utils/ast.js.map +1 -0
- package/dist/utils/color.d.ts +8 -0
- package/dist/utils/color.js +27 -0
- package/dist/utils/color.js.map +1 -0
- package/dist/utils/createIssue.d.ts +13 -0
- package/dist/utils/createIssue.js +28 -0
- package/dist/utils/createIssue.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.js +68 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/hostComponents.d.ts +12 -0
- package/dist/utils/hostComponents.js +57 -0
- package/dist/utils/hostComponents.js.map +1 -0
- package/dist/utils/identifiers.d.ts +3 -0
- package/dist/utils/identifiers.js +20 -0
- package/dist/utils/identifiers.js.map +1 -0
- package/dist/utils/lines.d.ts +8 -0
- package/dist/utils/lines.js +19 -0
- package/dist/utils/lines.js.map +1 -0
- package/dist/utils/similarity.d.ts +8 -0
- package/dist/utils/similarity.js +63 -0
- package/dist/utils/similarity.js.map +1 -0
- package/docs/architecture.md +68 -0
- package/docs/example-report.md +141 -0
- package/docs/good-first-issues.md +63 -0
- package/docs/rules.md +139 -0
- package/docs/showcase-expensify-app.md +119 -0
- package/package.json +60 -0
- package/schema/debtlens.config.schema.json +116 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mergeConfig.js","sourceRoot":"","sources":["../../src/config/mergeConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,UAA0B,EAAE,UAAsB;IAC5F,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAErD,OAAO;QACL,GAAG;QACH,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC;QAC5B,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,IAAI,aAAa,CAAC,OAAO;QACtG,OAAO,EAAE;YACP,GAAG,aAAa,CAAC,OAAO;YACxB,GAAG,CAAC,UAAU,CAAC,OAAO,IAAI,EAAE,CAAC;YAC7B,GAAG,CAAC,UAAU,CAAC,OAAO,IAAI,EAAE,CAAC;SAC9B;QACD,WAAW,EAAE,UAAU,CAAC,WAAW,IAAI,UAAU,CAAC,WAAW,IAAI,aAAa,CAAC,WAAW;QAC1F,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK;QACrE,UAAU,EAAE;YACV,GAAG,aAAa,CAAC,UAAU;YAC3B,GAAG,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,CAAC;YAChC,GAAG,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,CAAC;SACjC;QACD,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,QAAQ,IAAI,aAAa,CAAC,QAAQ;QAC9E,UAAU,EAAE,EAAE,GAAG,aAAa,CAAC,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE;QAC7E,YAAY,EAAE,UAAU,CAAC,YAAY;KACtC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SCHEMA_ID = "https://raw.githubusercontent.com/ColumbusLabs/debtlens/main/schema/debtlens.config.schema.json";
|
|
2
|
+
/**
|
|
3
|
+
* Build the JSON Schema for `debtlens.config.json`. Generated from the live rule list,
|
|
4
|
+
* severity set, and default threshold keys so it cannot drift from the code. A test
|
|
5
|
+
* asserts the committed schema file matches this output.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildConfigSchema(): Record<string, unknown>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { severities } from "../core/severity.js";
|
|
2
|
+
import { detectorIds } from "../detectors/index.js";
|
|
3
|
+
import { defaultConfig } from "./defaults.js";
|
|
4
|
+
export const SCHEMA_ID = "https://raw.githubusercontent.com/ColumbusLabs/debtlens/main/schema/debtlens.config.schema.json";
|
|
5
|
+
/**
|
|
6
|
+
* Build the JSON Schema for `debtlens.config.json`. Generated from the live rule list,
|
|
7
|
+
* severity set, and default threshold keys so it cannot drift from the code. A test
|
|
8
|
+
* asserts the committed schema file matches this output.
|
|
9
|
+
*/
|
|
10
|
+
export function buildConfigSchema() {
|
|
11
|
+
const knownThresholds = Object.fromEntries(Object.keys(defaultConfig.thresholds).map((key) => [key, { type: "number" }]));
|
|
12
|
+
return {
|
|
13
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
14
|
+
$id: SCHEMA_ID,
|
|
15
|
+
title: "DebtLens configuration",
|
|
16
|
+
description: "Configuration for the DebtLens static-analysis CLI.",
|
|
17
|
+
type: "object",
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
$schema: { type: "string" },
|
|
21
|
+
include: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: { type: "string" },
|
|
24
|
+
description: "Glob patterns to scan.",
|
|
25
|
+
},
|
|
26
|
+
exclude: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
description: "Glob patterns to skip.",
|
|
30
|
+
},
|
|
31
|
+
minSeverity: {
|
|
32
|
+
enum: [...severities],
|
|
33
|
+
description: "Lowest severity to report.",
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
type: "array",
|
|
37
|
+
uniqueItems: true,
|
|
38
|
+
items: { enum: [...detectorIds] },
|
|
39
|
+
description: "Rule ids to run. Omit to run all rules.",
|
|
40
|
+
},
|
|
41
|
+
maxFiles: {
|
|
42
|
+
type: "integer",
|
|
43
|
+
minimum: 1,
|
|
44
|
+
description: "Maximum number of files to scan.",
|
|
45
|
+
},
|
|
46
|
+
thresholds: {
|
|
47
|
+
type: "object",
|
|
48
|
+
description: "Per-rule numeric threshold overrides.",
|
|
49
|
+
properties: knownThresholds,
|
|
50
|
+
additionalProperties: { type: "number" },
|
|
51
|
+
},
|
|
52
|
+
vocabulary: {
|
|
53
|
+
type: "object",
|
|
54
|
+
description: "Naming-drift concept groups (concept id -> competing terms).",
|
|
55
|
+
additionalProperties: {
|
|
56
|
+
type: "array",
|
|
57
|
+
items: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,CAAC,MAAM,SAAS,GACpB,iGAAiG,CAAC;AAEpG;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAC9E,CAAC;IAEF,OAAO;QACL,OAAO,EAAE,yCAAyC;QAClD,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,qDAAqD;QAClE,IAAI,EAAE,QAAQ;QACd,oBAAoB,EAAE,KAAK;QAC3B,UAAU,EAAE;YACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC3B,OAAO,EAAE;gBACP,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,wBAAwB;aACtC;YACD,OAAO,EAAE;gBACP,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,wBAAwB;aACtC;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,CAAC,GAAG,UAAU,CAAC;gBACrB,WAAW,EAAE,4BAA4B;aAC1C;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,OAAO;gBACb,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE;gBACjC,WAAW,EAAE,yCAAyC;aACvD;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,CAAC;gBACV,WAAW,EAAE,kCAAkC;aAChD;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,uCAAuC;gBACpD,UAAU,EAAE,eAAe;gBAC3B,oBAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aACzC;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,8DAA8D;gBAC3E,oBAAoB,EAAE;oBACpB,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBAC1B;aACF;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DebtLensConfig } from "../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical starter config written by `debtlens init`. Kept in code (not read from
|
|
4
|
+
* the repo's example file) so it works when DebtLens is installed from npm, where
|
|
5
|
+
* only `dist/`, `README`, `LICENSE`, and `docs/` are published.
|
|
6
|
+
*/
|
|
7
|
+
export declare const configTemplate: DebtLensConfig & {
|
|
8
|
+
$schema: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function renderConfigFile(): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical starter config written by `debtlens init`. Kept in code (not read from
|
|
3
|
+
* the repo's example file) so it works when DebtLens is installed from npm, where
|
|
4
|
+
* only `dist/`, `README`, `LICENSE`, and `docs/` are published.
|
|
5
|
+
*/
|
|
6
|
+
export const configTemplate = {
|
|
7
|
+
$schema: "https://raw.githubusercontent.com/ColumbusLabs/debtlens/main/schema/debtlens.config.schema.json",
|
|
8
|
+
include: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
9
|
+
exclude: ["node_modules/**", "dist/**", "build/**", ".next/**", "coverage/**"],
|
|
10
|
+
minSeverity: "low",
|
|
11
|
+
rules: [
|
|
12
|
+
"large-component",
|
|
13
|
+
"state-sprawl",
|
|
14
|
+
"effect-complexity",
|
|
15
|
+
"duplicate-logic",
|
|
16
|
+
"dead-abstraction",
|
|
17
|
+
"prop-drilling",
|
|
18
|
+
"todo-comment",
|
|
19
|
+
"naming-drift",
|
|
20
|
+
],
|
|
21
|
+
thresholds: {
|
|
22
|
+
"large-component.maxLines": 250,
|
|
23
|
+
"state-sprawl.maxStatefulHooks": 6,
|
|
24
|
+
"effect-complexity.maxLines": 30,
|
|
25
|
+
"duplicate-logic.minSimilarity": 0.86,
|
|
26
|
+
"duplicate-logic.minLines": 8,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export function renderConfigFile() {
|
|
30
|
+
return `${JSON.stringify(configTemplate, null, 2)}\n`;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/config/template.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAyC;IAClE,OAAO,EAAE,iGAAiG;IAC1G,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,OAAO,EAAE,CAAC,iBAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,CAAC;IAC9E,WAAW,EAAE,KAAK;IAClB,KAAK,EAAE;QACL,iBAAiB;QACjB,cAAc;QACd,mBAAmB;QACnB,iBAAiB;QACjB,kBAAkB;QAClB,eAAe;QACf,cAAc;QACd,cAAc;KACf;IACD,UAAU,EAAE;QACV,0BAA0B,EAAE,GAAG;QAC/B,+BAA+B,EAAE,CAAC;QAClC,4BAA4B,EAAE,EAAE;QAChC,+BAA+B,EAAE,IAAI;QACrC,0BAA0B,EAAE,CAAC;KAC9B;CACF,CAAC;AAEF,MAAM,UAAU,gBAAgB;IAC9B,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DebtIssue, ScanResult } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_BASELINE_FILENAME = "debtlens-baseline.json";
|
|
3
|
+
export interface Baseline {
|
|
4
|
+
version: number;
|
|
5
|
+
generatedAt: string;
|
|
6
|
+
/** Map of issue fingerprint -> number of occurrences captured. */
|
|
7
|
+
fingerprints: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Stable fingerprint for an issue. Deliberately excludes the raw line number so a
|
|
11
|
+
* pre-existing finding stays suppressed when surrounding code moves it up or down.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeFingerprint(issue: DebtIssue): string;
|
|
14
|
+
export declare function createBaseline(issues: DebtIssue[]): Baseline;
|
|
15
|
+
export declare function writeBaseline(cwd: string, path: string, baseline: Baseline): string;
|
|
16
|
+
export declare function loadBaseline(cwd: string, path: string): Baseline;
|
|
17
|
+
/**
|
|
18
|
+
* Drop issues already captured in the baseline. Occurrence counts are respected, so
|
|
19
|
+
* adding a new instance of an already-baselined pattern still surfaces the new one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function filterIssues(issues: DebtIssue[], baseline: Baseline): DebtIssue[];
|
|
22
|
+
/** Apply a baseline to a scan result, returning a new result with a recomputed summary. */
|
|
23
|
+
export declare function applyBaseline(result: ScanResult, baseline: Baseline): ScanResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export const DEFAULT_BASELINE_FILENAME = "debtlens-baseline.json";
|
|
4
|
+
const BASELINE_VERSION = 1;
|
|
5
|
+
// Field delimiter for the fingerprint. normalizeText() collapses all whitespace to a
|
|
6
|
+
// single space, so a newline never appears inside a field, making it a delimiter no
|
|
7
|
+
// field value can span across.
|
|
8
|
+
const FIELD_SEPARATOR = "\n";
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a textual part of an issue so the fingerprint survives line shifts and
|
|
11
|
+
* count drift. Digit runs (line numbers AND counts) collapse to `#` and whitespace is
|
|
12
|
+
* squashed — what's left is the structural shape of the finding.
|
|
13
|
+
*
|
|
14
|
+
* Intentional tradeoff: because counts are normalized too, a finding that merely gets
|
|
15
|
+
* "worse" (e.g. a component growing from 250 to 900 lines) keeps the same fingerprint
|
|
16
|
+
* and stays suppressed. A baseline is for not re-nagging about known debt, not for
|
|
17
|
+
* tracking its magnitude, so this is the behavior we want.
|
|
18
|
+
*/
|
|
19
|
+
function normalizeText(value) {
|
|
20
|
+
return value.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Stable fingerprint for an issue. Deliberately excludes the raw line number so a
|
|
24
|
+
* pre-existing finding stays suppressed when surrounding code moves it up or down.
|
|
25
|
+
*/
|
|
26
|
+
export function computeFingerprint(issue) {
|
|
27
|
+
const parts = [
|
|
28
|
+
issue.ruleId,
|
|
29
|
+
issue.file,
|
|
30
|
+
normalizeText(issue.message),
|
|
31
|
+
normalizeText((issue.evidence ?? []).join("\n")),
|
|
32
|
+
].join(FIELD_SEPARATOR);
|
|
33
|
+
return fnv1a(parts);
|
|
34
|
+
}
|
|
35
|
+
export function createBaseline(issues) {
|
|
36
|
+
const fingerprints = {};
|
|
37
|
+
for (const issue of issues) {
|
|
38
|
+
const fp = computeFingerprint(issue);
|
|
39
|
+
fingerprints[fp] = (fingerprints[fp] ?? 0) + 1;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
version: BASELINE_VERSION,
|
|
43
|
+
generatedAt: new Date().toISOString(),
|
|
44
|
+
fingerprints,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function writeBaseline(cwd, path, baseline) {
|
|
48
|
+
const target = resolve(cwd, path);
|
|
49
|
+
writeFileSync(target, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
50
|
+
return target;
|
|
51
|
+
}
|
|
52
|
+
export function loadBaseline(cwd, path) {
|
|
53
|
+
const target = resolve(cwd, path);
|
|
54
|
+
if (!existsSync(target)) {
|
|
55
|
+
throw new Error(`Baseline file not found at ${target}. Run with --write-baseline first.`);
|
|
56
|
+
}
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(readFileSync(target, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
throw new Error(`Could not parse baseline at ${target}: ${message}`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed !== "object" ||
|
|
66
|
+
parsed === null ||
|
|
67
|
+
typeof parsed.fingerprints !== "object") {
|
|
68
|
+
throw new Error(`Invalid baseline file at ${target}: missing "fingerprints".`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Drop issues already captured in the baseline. Occurrence counts are respected, so
|
|
74
|
+
* adding a new instance of an already-baselined pattern still surfaces the new one.
|
|
75
|
+
*/
|
|
76
|
+
export function filterIssues(issues, baseline) {
|
|
77
|
+
const remaining = new Map(Object.entries(baseline.fingerprints));
|
|
78
|
+
const kept = [];
|
|
79
|
+
for (const issue of issues) {
|
|
80
|
+
const fp = computeFingerprint(issue);
|
|
81
|
+
const budget = remaining.get(fp) ?? 0;
|
|
82
|
+
if (budget > 0) {
|
|
83
|
+
remaining.set(fp, budget - 1);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
kept.push(issue);
|
|
87
|
+
}
|
|
88
|
+
return kept;
|
|
89
|
+
}
|
|
90
|
+
/** Apply a baseline to a scan result, returning a new result with a recomputed summary. */
|
|
91
|
+
export function applyBaseline(result, baseline) {
|
|
92
|
+
const issues = filterIssues(result.issues, baseline);
|
|
93
|
+
const bySeverity = { info: 0, low: 0, medium: 0, high: 0 };
|
|
94
|
+
const byRule = {};
|
|
95
|
+
for (const issue of issues) {
|
|
96
|
+
bySeverity[issue.severity] += 1;
|
|
97
|
+
byRule[issue.ruleId] = (byRule[issue.ruleId] ?? 0) + 1;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
issues,
|
|
102
|
+
summary: {
|
|
103
|
+
...result.summary,
|
|
104
|
+
totalIssues: issues.length,
|
|
105
|
+
bySeverity,
|
|
106
|
+
byRule,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function fnv1a(value) {
|
|
111
|
+
let hash = 2166136261;
|
|
112
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
113
|
+
hash ^= value.charCodeAt(i);
|
|
114
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
115
|
+
}
|
|
116
|
+
return `dl_${(hash >>> 0).toString(16)}`;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/core/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { Project, ScriptTarget, ts } from "ts-morph";
|
|
5
|
+
import { allDetectors } from "../detectors/index.js";
|
|
6
|
+
import { compareSeverityDesc, meetsMinSeverity } from "./severity.js";
|
|
7
|
+
export async function scan(options) {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
const filePaths = await resolveFilePaths(options);
|
|
10
|
+
const project = new Project({
|
|
11
|
+
compilerOptions: {
|
|
12
|
+
allowJs: true,
|
|
13
|
+
checkJs: false,
|
|
14
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
15
|
+
target: ScriptTarget.ES2022,
|
|
16
|
+
skipLibCheck: true,
|
|
17
|
+
},
|
|
18
|
+
skipAddingFilesFromTsConfig: true,
|
|
19
|
+
});
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const absolutePath of filePaths) {
|
|
22
|
+
const sourceFile = project.addSourceFileAtPathIfExists(absolutePath);
|
|
23
|
+
if (!sourceFile)
|
|
24
|
+
continue;
|
|
25
|
+
files.push({
|
|
26
|
+
absolutePath,
|
|
27
|
+
relativePath: relative(options.target, absolutePath).replaceAll("\\", "/"),
|
|
28
|
+
content: readFileSync(absolutePath, "utf8"),
|
|
29
|
+
sourceFile,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const detectors = selectDetectors(options.rules);
|
|
33
|
+
const issues = [];
|
|
34
|
+
for (const detector of detectors) {
|
|
35
|
+
const detectorIssues = await detector.detect({
|
|
36
|
+
project,
|
|
37
|
+
files,
|
|
38
|
+
options,
|
|
39
|
+
getThreshold: (key, fallback) => getThreshold(options, key, fallback),
|
|
40
|
+
});
|
|
41
|
+
issues.push(...detectorIssues.filter((issue) => meetsMinSeverity(issue.severity, options.minSeverity)));
|
|
42
|
+
}
|
|
43
|
+
issues.sort((a, b) => {
|
|
44
|
+
const severity = compareSeverityDesc(a.severity, b.severity);
|
|
45
|
+
if (severity !== 0)
|
|
46
|
+
return severity;
|
|
47
|
+
const byFile = a.file.localeCompare(b.file);
|
|
48
|
+
if (byFile !== 0)
|
|
49
|
+
return byFile;
|
|
50
|
+
return (a.location?.startLine ?? 0) - (b.location?.startLine ?? 0);
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
issues,
|
|
54
|
+
summary: {
|
|
55
|
+
totalIssues: issues.length,
|
|
56
|
+
bySeverity: {
|
|
57
|
+
info: issues.filter((issue) => issue.severity === "info").length,
|
|
58
|
+
low: issues.filter((issue) => issue.severity === "low").length,
|
|
59
|
+
medium: issues.filter((issue) => issue.severity === "medium").length,
|
|
60
|
+
high: issues.filter((issue) => issue.severity === "high").length,
|
|
61
|
+
},
|
|
62
|
+
byRule: issues.reduce((accumulator, issue) => {
|
|
63
|
+
accumulator[issue.ruleId] = (accumulator[issue.ruleId] ?? 0) + 1;
|
|
64
|
+
return accumulator;
|
|
65
|
+
}, {}),
|
|
66
|
+
filesScanned: files.length,
|
|
67
|
+
rulesRun: detectors.length,
|
|
68
|
+
elapsedMs: Date.now() - startedAt,
|
|
69
|
+
},
|
|
70
|
+
options: {
|
|
71
|
+
target: options.target,
|
|
72
|
+
include: options.include,
|
|
73
|
+
exclude: options.exclude,
|
|
74
|
+
minSeverity: options.minSeverity,
|
|
75
|
+
rules: options.rules,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function resolveFilePaths(options) {
|
|
80
|
+
const stats = statSync(options.target);
|
|
81
|
+
const isFile = stats.isFile();
|
|
82
|
+
// Canonicalize so the --changed set (paths from git, which resolves symlinks like
|
|
83
|
+
// macOS /var -> /private/var) matches fast-glob's output, which does not.
|
|
84
|
+
const changed = options.changedFiles
|
|
85
|
+
? new Set(options.changedFiles.map(canonicalize))
|
|
86
|
+
: undefined;
|
|
87
|
+
if (isFile) {
|
|
88
|
+
if (changed && !changed.has(canonicalize(options.target)))
|
|
89
|
+
return [];
|
|
90
|
+
return [options.target];
|
|
91
|
+
}
|
|
92
|
+
let paths = await fg(options.include, {
|
|
93
|
+
cwd: options.target,
|
|
94
|
+
absolute: true,
|
|
95
|
+
onlyFiles: true,
|
|
96
|
+
ignore: options.exclude,
|
|
97
|
+
dot: false,
|
|
98
|
+
unique: true,
|
|
99
|
+
});
|
|
100
|
+
if (changed) {
|
|
101
|
+
paths = paths.filter((path) => changed.has(canonicalize(path)));
|
|
102
|
+
}
|
|
103
|
+
return paths.slice(0, options.maxFiles ?? paths.length);
|
|
104
|
+
}
|
|
105
|
+
function canonicalize(path) {
|
|
106
|
+
try {
|
|
107
|
+
return realpathSync.native(path);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function selectDetectors(ruleIds) {
|
|
114
|
+
if (!ruleIds || ruleIds.length === 0) {
|
|
115
|
+
return allDetectors;
|
|
116
|
+
}
|
|
117
|
+
const requested = new Set(ruleIds);
|
|
118
|
+
const selected = allDetectors.filter((detector) => requested.has(detector.id));
|
|
119
|
+
const missing = [...requested].filter((ruleId) => !allDetectors.some((detector) => detector.id === ruleId));
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
throw new Error(`Unknown DebtLens rule(s): ${missing.join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
return selected;
|
|
124
|
+
}
|
|
125
|
+
function getThreshold(options, key, fallback) {
|
|
126
|
+
const value = options.thresholds[key];
|
|
127
|
+
return Number.isFinite(value) ? value : fallback;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/core/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Severity } from "./types.js";
|
|
2
|
+
export declare const severityRank: Record<Severity, number>;
|
|
3
|
+
export declare const severities: Severity[];
|
|
4
|
+
export declare function isSeverity(value: string | undefined): value is Severity;
|
|
5
|
+
export declare function parseSeverity(value: string | undefined, fallback: Severity): Severity;
|
|
6
|
+
export declare function meetsMinSeverity(severity: Severity, minSeverity: Severity): boolean;
|
|
7
|
+
export declare function compareSeverityDesc(a: Severity, b: Severity): number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const severityRank = {
|
|
2
|
+
info: 0,
|
|
3
|
+
low: 1,
|
|
4
|
+
medium: 2,
|
|
5
|
+
high: 3,
|
|
6
|
+
};
|
|
7
|
+
export const severities = ["info", "low", "medium", "high"];
|
|
8
|
+
export function isSeverity(value) {
|
|
9
|
+
return value === "info" || value === "low" || value === "medium" || value === "high";
|
|
10
|
+
}
|
|
11
|
+
export function parseSeverity(value, fallback) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return fallback;
|
|
14
|
+
const normalized = value.toLowerCase();
|
|
15
|
+
if (!isSeverity(normalized)) {
|
|
16
|
+
throw new Error(`Invalid severity "${value}". Expected one of: ${severities.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
export function meetsMinSeverity(severity, minSeverity) {
|
|
21
|
+
return severityRank[severity] >= severityRank[minSeverity];
|
|
22
|
+
}
|
|
23
|
+
export function compareSeverityDesc(a, b) {
|
|
24
|
+
return severityRank[b] - severityRank[a];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=severity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"severity.js","sourceRoot":"","sources":["../../src/core/severity.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,IAAI,EAAE,CAAC;IACP,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAe,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAExE,MAAM,UAAU,UAAU,CAAC,KAAyB;IAClD,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,MAAM,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAyB,EAAE,QAAkB;IACzE,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,uBAAuB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAkB,EAAE,WAAqB;IACxE,OAAO,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,CAAW,EAAE,CAAW;IAC1D,OAAO,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Project, SourceFile } from "ts-morph";
|
|
2
|
+
export type Severity = "info" | "low" | "medium" | "high";
|
|
3
|
+
export type OutputFormat = "terminal" | "json" | "markdown" | "sarif";
|
|
4
|
+
export interface IssueLocation {
|
|
5
|
+
startLine: number;
|
|
6
|
+
startColumn?: number;
|
|
7
|
+
endLine?: number;
|
|
8
|
+
endColumn?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DebtIssue {
|
|
11
|
+
id: string;
|
|
12
|
+
ruleId: string;
|
|
13
|
+
ruleName: string;
|
|
14
|
+
severity: Severity;
|
|
15
|
+
confidence: number;
|
|
16
|
+
message: string;
|
|
17
|
+
file: string;
|
|
18
|
+
location?: IssueLocation;
|
|
19
|
+
evidence?: string[];
|
|
20
|
+
suggestion?: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface SourceFileInfo {
|
|
24
|
+
absolutePath: string;
|
|
25
|
+
relativePath: string;
|
|
26
|
+
content: string;
|
|
27
|
+
sourceFile: SourceFile;
|
|
28
|
+
}
|
|
29
|
+
export interface ScanThresholds {
|
|
30
|
+
[key: string]: number;
|
|
31
|
+
}
|
|
32
|
+
export interface DebtLensConfig {
|
|
33
|
+
include?: string[];
|
|
34
|
+
exclude?: string[];
|
|
35
|
+
minSeverity?: Severity;
|
|
36
|
+
rules?: string[];
|
|
37
|
+
thresholds?: ScanThresholds;
|
|
38
|
+
maxFiles?: number;
|
|
39
|
+
/** Concept id -> competing term variants, used by the naming-drift rule. */
|
|
40
|
+
vocabulary?: Record<string, string[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface ScanOptions {
|
|
43
|
+
cwd: string;
|
|
44
|
+
target: string;
|
|
45
|
+
include: string[];
|
|
46
|
+
exclude: string[];
|
|
47
|
+
minSeverity: Severity;
|
|
48
|
+
rules?: string[];
|
|
49
|
+
thresholds: ScanThresholds;
|
|
50
|
+
maxFiles?: number;
|
|
51
|
+
vocabulary?: Record<string, string[]>;
|
|
52
|
+
/** When set, only scan files whose absolute path is in this list (--changed mode). */
|
|
53
|
+
changedFiles?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface CliOptions {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
include?: string[];
|
|
58
|
+
exclude?: string[];
|
|
59
|
+
minSeverity?: Severity;
|
|
60
|
+
rules?: string[];
|
|
61
|
+
thresholds?: ScanThresholds;
|
|
62
|
+
maxFiles?: number;
|
|
63
|
+
format?: OutputFormat;
|
|
64
|
+
output?: string;
|
|
65
|
+
failOn?: Severity;
|
|
66
|
+
configPath?: string;
|
|
67
|
+
noColor?: boolean;
|
|
68
|
+
changedFiles?: string[];
|
|
69
|
+
}
|
|
70
|
+
export interface DetectorContext {
|
|
71
|
+
project: Project;
|
|
72
|
+
files: SourceFileInfo[];
|
|
73
|
+
options: ScanOptions;
|
|
74
|
+
getThreshold: (key: string, fallback: number) => number;
|
|
75
|
+
}
|
|
76
|
+
export interface Detector {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
description: string;
|
|
80
|
+
defaultSeverity: Severity;
|
|
81
|
+
tags: string[];
|
|
82
|
+
detect: (context: DetectorContext) => Promise<DebtIssue[]> | DebtIssue[];
|
|
83
|
+
}
|
|
84
|
+
export interface ScanSummary {
|
|
85
|
+
totalIssues: number;
|
|
86
|
+
bySeverity: Record<Severity, number>;
|
|
87
|
+
byRule: Record<string, number>;
|
|
88
|
+
filesScanned: number;
|
|
89
|
+
rulesRun: number;
|
|
90
|
+
elapsedMs: number;
|
|
91
|
+
}
|
|
92
|
+
export interface ScanResult {
|
|
93
|
+
issues: DebtIssue[];
|
|
94
|
+
summary: ScanSummary;
|
|
95
|
+
options: Pick<ScanOptions, "target" | "include" | "exclude" | "minSeverity" | "rules">;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|