From b8dc282fde5f89521067b861780dcbf2319a2f90 Mon Sep 17 00:00:00 2001 From: aah20 Date: Tue, 18 Aug 2026 11:24:20 +0300 Subject: [PATCH] feat: add generated-code leakage detector (#313) Flags production source files that import from generated, mock, fixture, or test directories (__generated__, __mocks__, __fixtures__, fixtures/, test-utils/, .mock.ts, .generated.ts) and files with @generated headers. - Path-based detection (high confidence 0.85) for known generated/test directory patterns and file suffixes - Content-based detection (confidence 0.75) for @generated markers in the first 500 chars of resolved import targets - Excludes test files (.test, .spec, .stories, __tests__/) from flagging - Added to core and ai-assisted-maintainer packs - 12 test cases covering positive detections and false-positive guards Closes #313 Co-authored-by: Cursor --- src/config/packs.ts | 2 + src/detectors/generatedCodeLeakage.ts | 137 +++++++++++ src/detectors/index.ts | 2 + tests/detectors/generatedCodeLeakage.test.ts | 229 +++++++++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 src/detectors/generatedCodeLeakage.ts create mode 100644 tests/detectors/generatedCodeLeakage.test.ts diff --git a/src/config/packs.ts b/src/config/packs.ts index 39d8ea6..8ea1a0d 100644 --- a/src/config/packs.ts +++ b/src/config/packs.ts @@ -35,6 +35,7 @@ const CORE_RULES = [ "long-parameter-list", "god-file", "cognitive-complexity", + "generated-code-leakage", ] as const; const REACT_RULES = [ @@ -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 = [ diff --git a/src/detectors/generatedCodeLeakage.ts b/src/detectors/generatedCodeLeakage.ts new file mode 100644 index 0000000..f83a1a2 --- /dev/null +++ b/src/detectors/generatedCodeLeakage.ts @@ -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(); + 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 | 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)); +} diff --git a/src/detectors/index.ts b/src/detectors/index.ts index d3414a5..157d24c 100644 --- a/src/detectors/index.ts +++ b/src/detectors/index.ts @@ -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"; @@ -125,6 +126,7 @@ export const allDetectors: Detector[] = [ instructionDuplicationDetector, instructionContradictionDetector, featureFlagDebtDetector, + generatedCodeLeakageDetector, ]; export const detectorIds = allDetectors.map((detector) => detector.id); diff --git a/tests/detectors/generatedCodeLeakage.test.ts b/tests/detectors/generatedCodeLeakage.test.ts new file mode 100644 index 0000000..aa6c435 --- /dev/null +++ b/tests/detectors/generatedCodeLeakage.test.ts @@ -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); + }); +});