Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ All notable changes to DebtLens are documented here. This project adheres to
- Opt-in `feature-flags` pack with `stale-feature-flag` detector.
- `--concurrency` and `--cache-dir` scan controls for large-repo performance.
- `--ownership-report` scorecards with count and payoff leaderboards.
- HTML reports now include import-graph SVG and directory debt treemap views.
- HTML reports now include import-graph SVG and directory debt treemap views
([#264](https://github.com/ColumbusLabs/DebtLens/issues/264)).
- Core rules: `long-parameter-list`, `god-file`, and `cognitive-complexity` for function/module
design smells beyond single-axis size checks.
design smells beyond single-axis size checks
([#260](https://github.com/ColumbusLabs/DebtLens/issues/260),
[#261](https://github.com/ColumbusLabs/DebtLens/issues/261),
[#262](https://github.com/ColumbusLabs/DebtLens/issues/262)).
- Config `budgets` block and `debtlens scan --budget-report` for per-area debt SLO gating.
- `debtlens scan --format badge` emits a self-contained SVG badge plus shields.io endpoint JSON.
- `debtlens scan --format badge` emits a self-contained SVG badge plus shields.io endpoint JSON
([#265](https://github.com/ColumbusLabs/DebtLens/issues/265)).
- Scan results now warn when matched files exceed `maxFiles`; terminal output prints the
advisory and JSON reports include it in `summary.warnings` while keeping
`summary.filesScanned` as the actual scanned count.
Expand Down
29 changes: 10 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -799,25 +799,22 @@ jobs:
run: echo "DebtLens total=${{ steps.debtlens.outputs.total-issues }} top=${{ steps.debtlens.outputs.top-rule }}"
```

A Shields endpoint badge can be generated from the artifact by publishing a tiny JSON file derived from `summary.totalIssues`:
Generate a README status badge directly from a scan:

```json
{
"schemaVersion": 1,
"label": "DebtLens",
"message": "12 issues",
"color": "orange"
}
```bash
debtlens scan . --format badge --output debtlens-badge.svg
```

For example:
That writes a self-contained SVG and a sibling `debtlens-badge.json` shields.io endpoint
payload. Color thresholds are configurable via the `badge` config block (`greenMax` /
`yellowMax`). Publish the JSON from any static endpoint:

```bash
jq '{schemaVersion: 1, label: "DebtLens", message: (.summary.totalIssues|tostring + " issues"), color: (if .summary.totalIssues == 0 then "brightgreen" elif .summary.bySeverity.high > 0 then "red" else "orange" end)}' debtlens-report.json > debtlens-badge.json
```markdown
![DebtLens](https://img.shields.io/endpoint?url=https://example.com/debtlens-badge.json)
```

For a stricter "0 new high debt" badge after `--baseline` or `--diff-base`, derive the
message from the remaining high-severity issues in the filtered report:
You can still derive a custom badge from a JSON report artifact with `jq` when you need a
stricter "0 new high debt" message after `--baseline` or `--diff-base`:

```bash
jq '{
Expand All @@ -828,12 +825,6 @@ jq '{
}' debtlens-report.json > debtlens-high-badge.json
```

Publish that JSON file from any static endpoint and use the Shields endpoint badge:

```markdown
![DebtLens](https://img.shields.io/endpoint?url=https://example.com/debtlens-high-badge.json)
```

Other CI templates: [GitLab](./docs/ci-gitlab.md), [Bitbucket](./docs/ci-bitbucket.md), and [Azure Pipelines](./docs/ci-azure.md).

For local hooks, see [pre-commit hooks](./docs/pre-commit.md). For monorepo rollout, see [per-package baselines](./docs/monorepo-baselines.md).
Expand Down
11 changes: 10 additions & 1 deletion docs/report-gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ reviewers already work.
| Markdown | `debtlens scan examples/react --format markdown --output debtlens-report.md` | PR descriptions, release notes, and maintainer handoffs. |
| PR comment | `debtlens scan examples/react --format pr-comment --output debtlens-pr-comment.md` | Compact GitHub review comments with prioritized fix targets and optional caps. |
| SARIF | `debtlens scan examples/react --format sarif --sarif-category examples-react --output debtlens.sarif` | GitHub code scanning and compatible quality dashboards. |
| HTML | `debtlens scan examples/react --format html --output debtlens-report.html` | Shareable human-readable artifact. |
| HTML | `debtlens scan examples/react --format html --output debtlens-report.html` | Shareable human-readable artifact with findings, import-graph SVG, and debt treemap. |
| Badge | `debtlens scan examples/react --format badge --output debtlens-badge.svg` | README / status badge SVG; also writes shields.io endpoint JSON beside the SVG. |
| JUnit | `debtlens scan examples/react --format junit --junit-fail-on high --output debtlens-junit.xml` | CI systems that surface test-style failures while keeping lower severities visible. |
| GitLab Code Quality | `debtlens scan examples/react --format gitlab-codequality --output gl-code-quality-report.json` | Native GitLab Merge Request Code Quality widgets. |

HTML reports are self-contained (inline SVG, no CDN assets). When the scan builds an
import graph for TypeScript/JavaScript files, the HTML includes an import-graph view with
cycle edges highlighted, plus a directory debt treemap sized by finding density.

Badge output writes a self-contained SVG. When `--output` ends in `.svg`, DebtLens also
writes a sibling `*.json` shields.io endpoint payload. Configure color thresholds with a
`badge` block in `debtlens.config.json` (`greenMax` / `yellowMax`).

The GitHub Action runs one canonical JSON scan and renders requested reports from that
result so counts, filters, baselines, suppressions, and source links stay aligned.
When `hotspots` is enabled, reports can include optional git-derived hotspot
Expand Down
6 changes: 6 additions & 0 deletions docs/rule-packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ For a user-facing selection table, see [`pack-chooser.md`](./pack-chooser.md).
| `swallowed-error` | **core** | Catch blocks that only log without rethrowing or returning | Medium |
| `floating-promise` | **core** | Unawaited promise-returning calls and effect fire-and-forget | Medium |
| `commented-out-code` | **core** | Contiguous comment lines that look like dead code | Low |
| `long-parameter-list` | **core** | Functions with too many parameters or boolean flag parameters | Medium |
| `god-file` | **core** | Kitchen-sink modules that exceed multiple sprawl thresholds together | Medium |
| `cognitive-complexity` | **core** | Sonar-style cognitive complexity that penalizes nesting more than flat branching | Medium |
| `large-component` | **react** | React-style components with too many lines, hooks, or branch points | Medium |
| `state-sprawl` | **react** | Components/hooks with many local stateful hooks | Medium |
| `effect-complexity` | **react** | Long or overloaded React effect hooks | Medium |
Expand Down Expand Up @@ -111,6 +114,9 @@ These apply to any TypeScript or JavaScript codebase:
- **`swallowed-error`** — catch blocks that only log without rethrowing or returning a handled result.
- **`floating-promise`** — promise-returning calls that are not awaited, returned, void-marked, or error-handled.
- **`commented-out-code`** — contiguous comment blocks that look like dead code.
- **`long-parameter-list`** — functions with too many parameters or multiple boolean flags.
- **`god-file`** — kitchen-sink modules that cross several independent sprawl axes at once.
- **`cognitive-complexity`** — nested control-flow difficulty complementary to `complex-control-flow`.

Future core rules may expand these signals into language-specific packs or richer project graph analysis.

Expand Down
11 changes: 10 additions & 1 deletion docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,17 +1102,26 @@ When this is a false positive:

## `cognitive-complexity`

Scores functions with a Sonar-style cognitive complexity model that penalizes nesting more than flat branching.
Scores functions with a Sonar-style cognitive complexity model that penalizes nesting more than flat branching. Prefer this when readability/nesting is the concern; keep `complex-control-flow` when raw branch/cyclomatic pressure is the concern.

Default thresholds:

- `cognitive-complexity.max`: `15`

Scoring model (summary):

- +1 for each control-flow structure (`if`, loops, `catch`, ternary, etc.)
- +1 additional per nesting level when that structure is nested
- sequences of boolean operators add incremental cost
- a flat `switch` does not accumulate nesting the way nested `if` chains do

When this is a false positive:

- generated parsers or dispatch tables with intentionally flat `switch` blocks
- compare with `complex-control-flow` when only cyclomatic count is high

Confidence scales with how far the score exceeds `cognitive-complexity.max`.

## `python-error-handling`

Flags Python `try/except` handlers that are empty (`pass` only), bare `except:`, or broad `except Exception:` blocks that only log without meaningful handling.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const baselineFlags = [
"--parallel",
"--batch-size",
];
const formats = ["terminal", "json", "markdown", "pr-comment", "sarif", "html", "junit", "gitlab-codequality"];
const formats = ["terminal", "json", "markdown", "pr-comment", "sarif", "html", "junit", "gitlab-codequality", "badge"];
const baselineFormats = ["terminal", "json"];
const compareFormats = ["terminal", "markdown", "json"];
const scanFlags = [...SCAN_ARG_FLAGS, "--from-eslint", "--debounce"];
Expand Down
3 changes: 2 additions & 1 deletion tests/cli/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe("debtlens completions", () => {
assert.match(result.stdout, /\$\{COMP_WORDS\[1\]\}" == "compare" && "\$prev" == "--format"/);
assert.match(result.stdout, /terminal markdown json/);
assert.match(result.stdout, /gitlab-codequality/);
assert.match(result.stdout, /badge/);
assert.match(result.stdout, /--dry-run/);
assert.match(result.stdout, /--audit-suppressions/);
assert.match(result.stdout, /--hotspots/);
Expand Down Expand Up @@ -78,7 +79,7 @@ describe("debtlens completions", () => {
assert.match(fish.stdout, /-l codeowners/);
assert.match(fish.stdout, /-l junit-fail-on -a "info low medium high"/);
assert.match(fish.stdout, /-l gate -a "advisory new-code strict-new-code legacy-baseline"/);
assert.match(fish.stdout, /not __fish_seen_subcommand_from baseline compare" -l format -a "terminal json markdown pr-comment sarif html junit gitlab-codequality"/);
assert.match(fish.stdout, /not __fish_seen_subcommand_from baseline compare" -l format -a "terminal json markdown pr-comment sarif html junit gitlab-codequality badge"/);
assert.match(fish.stdout, /__fish_seen_subcommand_from compare" -l format -a "terminal markdown json"/);
assert.match(fish.stdout, /__fish_seen_subcommand_from baseline" -l format -a "terminal json"/);
});
Expand Down
11 changes: 11 additions & 0 deletions tests/docs/ciProviderDocs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ describe("provider CI docs", () => {
assert.match(reportGalleryDocs, /CODEOWNERS-based owner\s+summaries/);
});

it("documents HTML graph/treemap and badge report surfaces", () => {
assert.match(reportGalleryDocs, /--format html --output debtlens-report\.html/);
assert.match(reportGalleryDocs, /import-graph SVG/);
assert.match(reportGalleryDocs, /debt treemap/);
assert.match(reportGalleryDocs, /self-contained \(inline SVG, no CDN assets\)/);
assert.match(reportGalleryDocs, /--format badge --output debtlens-badge\.svg/);
assert.match(reportGalleryDocs, /shields\.io endpoint/);
assert.match(readme, /--format badge --output debtlens-badge\.svg/);
assert.match(readme, /debtlens-badge\.json/);
});

it("documents GitLab Code Quality baseline and new-code gates", () => {
assert.match(gitLabDocs, /--format gitlab-codequality --output gl-code-quality-report\.json/);
assert.match(gitLabDocs, /reports:\n\s+codequality: gl-code-quality-report\.json/);
Expand Down
14 changes: 14 additions & 0 deletions tests/reporters/graphReporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ describe("graph reporter", () => {
assert.match(treemap, /<svg/);
});

it("highlights cycle edges in the import graph", () => {
const graph: ImportGraph = {
nodes: ["a.ts", "b.ts", "c.ts"],
edges: [
{ from: "a.ts", to: "b.ts", inCycle: true },
{ from: "b.ts", to: "c.ts", inCycle: false },
],
cycles: [["a.ts", "b.ts"]],
};
const svg = renderImportGraphSvg(graph);
assert.match(svg, /stroke="#e05d44"/);
assert.match(svg, /stroke="#8c959f"/);
});

it("escapes graph and treemap labels", () => {
const graph: ImportGraph = {
nodes: ["src/<owner>&file.ts", "b.ts"],
Expand Down
33 changes: 33 additions & 0 deletions tests/reporters/htmlReporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@ describe("html reporter", () => {
assert.match(html, /Top payoff targets/);
assert.match(html, /1 low-severity finding/);
assert.match(html, /Debt Heatmap/);
assert.match(html, /Debt treemap/);
assert.match(html, /aria-label="Debt treemap"/);
assert.doesNotMatch(html, /https?:\/\//);
});

it("renders import graph with cycle edges when summary.importGraph is present", () => {
const result = makeResult([{
id: "1",
fingerprint: "1",
ruleId: "import-cycle",
ruleName: "Import cycle",
severity: "medium",
confidence: 0.8,
message: "Cycle detected",
file: "a.ts",
location: { startLine: 1 },
tags: [],
}]);
result.summary.importGraph = {
nodes: ["a.ts", "b.ts"],
edges: [
{ from: "a.ts", to: "b.ts", inCycle: true },
{ from: "b.ts", to: "a.ts", inCycle: true },
],
cycles: [["a.ts", "b.ts"]],
};

const html = renderHtml(result);

assert.match(html, /Import graph/);
assert.match(html, /aria-label="Import graph"/);
assert.match(html, /stroke="#e05d44"/);
assert.doesNotMatch(html, /https?:\/\//);
});

it("renders an empty state", () => {
Expand Down