Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/config/packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const CORE_RULES = [
"long-parameter-list",
"god-file",
"cognitive-complexity",
"generated-code-leakage",
] as const;

const REACT_RULES = [
Expand Down Expand Up @@ -85,6 +86,7 @@ const AI_ASSISTED_MAINTAINER_RULES = [
"empty-catch",
"swallowed-error",
"commented-out-code",
"generated-code-leakage",
] as const;

const OSS_MAINTAINER_RULES = [
Expand Down
137 changes: 137 additions & 0 deletions src/detectors/generatedCodeLeakage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import path from "node:path";
import type { DebtIssue, Detector, DetectorContext, SourceFileInfo } from "../core/types.js";
import { createIssue } from "../utils/createIssue.js";

/**
* Path segments and file markers that indicate generated, test-fixture,
* or scaffold output that should not be imported by production source.
*/
const DEFAULT_GENERATED_PATTERNS: RegExp[] = [
/(^|\/)__generated__\//,
/(^|\/)__mocks__\//,
/(^|\/)__tests__\//,
/(^|\/)__snapshots__\//,
/(^|\/)__fixtures__\//,
/(^|\/)\.(fixtures|mocks)\//,
/(^|\/)fixtures\//,
/(^|\/)test-utils\//,
/(^|\/)testing\//,
/\.generated(\.[cm]?[jt]sx?)?$/,
/\.mock(\.[cm]?[jt]sx?)?$/,
];

const TEST_FILE_PATTERN = /\.(test|spec|stories|story)\.[cm]?[jt]sx?$/;
const SETUP_FILE_PATTERN = /(^|\/)(__tests__|__mocks__|__snapshots__|__fixtures__|test-utils|testing|\.fixtures|\.mocks|fixtures)\//;

function isTestFile(relativePath: string): boolean {
return TEST_FILE_PATTERN.test(relativePath) || SETUP_FILE_PATTERN.test(relativePath);
}

function isGeneratedPath(importPath: string): boolean {
return DEFAULT_GENERATED_PATTERNS.some((pattern) => pattern.test(importPath));
}

function hasGeneratedMarker(file: SourceFileInfo): boolean {
const leadingText = file.content.slice(0, 500);
return /@generated\b/.test(leadingText) || /\/\*\*?\s*AUTO[- ]?GENERATED/i.test(leadingText);
}

export const generatedCodeLeakageDetector: Detector = {
id: "generated-code-leakage",
name: "Generated-code leakage",
description:
"Flags production source files that import from generated, mock, fixture, or test directories.",
defaultSeverity: "medium",
tags: ["imports", "cleanup", "ai-debt", "maintainability"],
detect(context: DetectorContext): DebtIssue[] {
const issues: DebtIssue[] = [];
const knownFiles = new Map<string, SourceFileInfo>();
for (const file of context.files) {
knownFiles.set(file.relativePath, file);
}

for (const file of context.files) {
if (isTestFile(file.relativePath)) continue;

for (const declaration of file.sourceFile.getImportDeclarations()) {
const specifier = declaration.getModuleSpecifierValue();
if (!specifier.startsWith(".")) continue;

const resolvedRelative = resolveImportTarget(
file.relativePath,
specifier,
knownFiles,
);

const importStartLine = declaration.getStartLineNumber();

if (isGeneratedPath(specifier)) {
issues.push(
createIssue({
detector: generatedCodeLeakageDetector,
severity: "medium",
confidence: 0.85,
file: file.relativePath,
location: { startLine: importStartLine },
message: `Production file imports from a generated/test path: "${specifier}".`,
evidence: [
`import specifier: ${specifier}`,
`in: ${file.relativePath}:${importStartLine}`,
],
suggestion:
"Extract the real dependency into a production module, or move the consuming code into the test tree.",
}),
);
continue;
}

if (resolvedRelative) {
const resolvedFile = knownFiles.get(resolvedRelative);
if (resolvedFile && hasGeneratedMarker(resolvedFile)) {
issues.push(
createIssue({
detector: generatedCodeLeakageDetector,
severity: "medium",
confidence: 0.75,
file: file.relativePath,
location: { startLine: importStartLine },
message: `Production file imports "${specifier}", which is marked @generated.`,
evidence: [
`import specifier: ${specifier}`,
`resolved to: ${resolvedRelative}`,
`marker: @generated header in target file`,
],
suggestion:
"If this import is intentional (e.g. codegen output consumed at build time), suppress with a debtlens-disable comment. Otherwise move the dependency to a proper source module.",
}),
);
}
}
}
}

return issues;
},
};

function resolveImportTarget(
importerPath: string,
specifier: string,
knownFiles: Map<string, SourceFileInfo>,
): string | undefined {
const base = path.posix.normalize(
path.posix.join(path.posix.dirname(importerPath), specifier),
);
const candidates = [
base,
`${base}.ts`,
`${base}.tsx`,
`${base}.js`,
`${base}.jsx`,
path.posix.join(base, "index.ts"),
path.posix.join(base, "index.tsx"),
path.posix.join(base, "index.js"),
path.posix.join(base, "index.jsx"),
];
return candidates.find((c) => knownFiles.has(c));
}
2 changes: 2 additions & 0 deletions src/detectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { deadAbstractionDetector } from "./deadAbstraction.js";
import { emptyCatchDetector, swallowedErrorDetector } from "./errorHandling.js";
import { featureFlagDebtDetector } from "./featureFlagDebt.js";
import { floatingPromiseDetector } from "./floatingPromise.js";
import { generatedCodeLeakageDetector } from "./generatedCodeLeakage.js";
import { duplicateLogicDetector } from "./duplicateLogic.js";
import { duplicatedLiteralDetector } from "./duplicatedLiteral.js";
import { effectComplexityDetector } from "./effectComplexity.js";
Expand Down Expand Up @@ -125,6 +126,7 @@ export const allDetectors: Detector[] = [
instructionDuplicationDetector,
instructionContradictionDetector,
featureFlagDebtDetector,
generatedCodeLeakageDetector,
];

export const detectorIds = allDetectors.map((detector) => detector.id);
229 changes: 229 additions & 0 deletions tests/detectors/generatedCodeLeakage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { generatedCodeLeakageDetector } from "../../src/detectors/generatedCodeLeakage.js";
import { runDetector } from "../helpers/runDetector.js";

describe("generated-code-leakage detector", () => {
it("flags a production file importing from __generated__", async () => {
const prod = `
import { UserFragment } from "./__generated__/graphql";

export function useUser() {
return UserFragment;
}
`;
const generated = `
export const UserFragment = {};
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"hooks/useUser.ts": prod,
"hooks/__generated__/graphql.ts": generated,
});

assert.equal(issues.length, 1);
assert.equal(issues[0]?.ruleId, "generated-code-leakage");
assert.match(issues[0]?.message ?? "", /generated\/test path/);
});

it("flags a production file importing from __mocks__", async () => {
const prod = `
import { mockDb } from "./__mocks__/database";

export const db = mockDb;
`;
const mock = `
export const mockDb = { query: () => [] };
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"services/db.ts": prod,
"services/__mocks__/database.ts": mock,
});

assert.equal(issues.length, 1);
assert.match(issues[0]?.message ?? "", /__mocks__/);
});

it("flags a production file importing from __fixtures__", async () => {
const prod = `
import { sampleOrder } from "../__fixtures__/orders";

export function getDefaultOrder() {
return sampleOrder;
}
`;
const fixture = `
export const sampleOrder = { id: "123", total: 100 };
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"services/orders.ts": prod,
"__fixtures__/orders.ts": fixture,
});

assert.equal(issues.length, 1);
assert.match(issues[0]?.message ?? "", /__fixtures__/);
});

it("flags a production file importing a .mock.ts file", async () => {
const prod = `
import { fakeClient } from "./api.mock";

export const client = fakeClient;
`;
const mock = `
export const fakeClient = { get: () => ({}) };
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"lib/client.ts": prod,
"lib/api.mock.ts": mock,
});

assert.equal(issues.length, 1);
assert.match(issues[0]?.message ?? "", /\.mock/);
});

it("flags a production file importing from fixtures/", async () => {
const prod = `
import { seedData } from "../fixtures/seed";

export function init() {
return seedData;
}
`;
const fixture = `
export const seedData = [1, 2, 3];
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"src/init.ts": prod,
"fixtures/seed.ts": fixture,
});

assert.equal(issues.length, 1);
});

it("flags a production file importing a file with @generated marker", async () => {
const prod = `
import { schema } from "./schema";

export function getSchema() {
return schema;
}
`;
const generated = `
/** @generated by prisma — do not edit */
export const schema = {};
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"lib/getSchema.ts": prod,
"lib/schema.ts": generated,
});

assert.equal(issues.length, 1);
assert.match(issues[0]?.message ?? "", /@generated/);
assert.equal(issues[0]?.confidence, 0.75);
});

it("does NOT flag test files importing from __fixtures__", async () => {
const test = `
import { sampleOrder } from "../__fixtures__/orders";

describe("orders", () => {
it("works", () => expect(sampleOrder).toBeDefined());
});
`;
const fixture = `
export const sampleOrder = { id: "123" };
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"services/orders.test.ts": test,
"__fixtures__/orders.ts": fixture,
});

assert.equal(issues.length, 0);
});

it("does NOT flag test files importing from __mocks__", async () => {
const test = `
import { mockDb } from "./__mocks__/database";

test("db", () => {});
`;
const mock = `
export const mockDb = {};
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"services/db.spec.ts": test,
"services/__mocks__/database.ts": mock,
});

assert.equal(issues.length, 0);
});

it("does NOT flag files inside __tests__ importing from __fixtures__", async () => {
const test = `
import { data } from "../__fixtures__/data";

test("it", () => {});
`;
const fixture = `
export const data = {};
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"__tests__/integration.ts": test,
"__fixtures__/data.ts": fixture,
});

assert.equal(issues.length, 0);
});

it("does NOT flag a production file importing from a normal module", async () => {
const prod = `
import { helper } from "./utils";

export function main() {
return helper();
}
`;
const utils = `
export function helper() { return 42; }
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"src/main.ts": prod,
"src/utils.ts": utils,
});

assert.equal(issues.length, 0);
});

it("does NOT flag non-relative (bare) imports", async () => {
const prod = `
import React from "react";
import { z } from "zod";

export function App() { return null; }
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"App.tsx": prod,
});

assert.equal(issues.length, 0);
});

it("does NOT flag a production file importing a normal file without @generated", async () => {
const prod = `
import { schema } from "./schema";

export function getSchema() {
return schema;
}
`;
const normal = `
export const schema = {};
`;
const issues = await runDetector(generatedCodeLeakageDetector, {
"lib/getSchema.ts": prod,
"lib/schema.ts": normal,
});

assert.equal(issues.length, 0);
});
});