From c935f7e4b6f4b194f00b708c01511c3e77a47b8b Mon Sep 17 00:00:00 2001 From: nitinryali Date: Mon, 1 Jun 2026 21:28:58 +0530 Subject: [PATCH] fix/issue-1 --- schema/debtlens.config.schema.json | 14 +++++++++++++ src/config/defaults.ts | 3 +++ src/config/mergeConfig.ts | 4 ++++ src/config/schema.ts | 12 +++++++++++ src/core/types.ts | 6 ++++++ src/detectors/propDrilling.ts | 3 ++- src/utils/hostComponents.ts | 10 ++++++++-- tests/detectors/propDrilling.test.ts | 30 ++++++++++++++++++++++++++++ tests/helpers/runDetector.ts | 3 +++ 9 files changed, 82 insertions(+), 3 deletions(-) diff --git a/schema/debtlens.config.schema.json b/schema/debtlens.config.schema.json index bfbc93b..7bcc7c6 100644 --- a/schema/debtlens.config.schema.json +++ b/schema/debtlens.config.schema.json @@ -111,6 +111,20 @@ "type": "string" } } + }, + "propDrilling": { + "type": "object", + "description": "Prop-drilling rule configuration.", + "properties": { + "ignoreComponents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional UI primitive component names to ignore (extends built-in host components)." + } + }, + "additionalProperties": false } } } diff --git a/src/config/defaults.ts b/src/config/defaults.ts index f640bc1..54d5dd1 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -38,4 +38,7 @@ export const defaultConfig: Required = { }, maxFiles: 2000, vocabulary: {}, + propDrilling: { + ignoreComponents: [], + }, }; diff --git a/src/config/mergeConfig.ts b/src/config/mergeConfig.ts index 05259df..6bdc8c3 100644 --- a/src/config/mergeConfig.ts +++ b/src/config/mergeConfig.ts @@ -23,6 +23,10 @@ export function mergeConfig(target: string, fileConfig: DebtLensConfig, cliOptio }, maxFiles: cliOptions.maxFiles ?? fileConfig.maxFiles ?? defaultConfig.maxFiles, vocabulary: { ...defaultConfig.vocabulary, ...(fileConfig.vocabulary ?? {}) }, + propDrillingIgnoreComponents: [ + ...(defaultConfig.propDrilling?.ignoreComponents ?? []), + ...(fileConfig.propDrilling?.ignoreComponents ?? []), + ], changedFiles: cliOptions.changedFiles, }; } diff --git a/src/config/schema.ts b/src/config/schema.ts index 22e2fef..6a6d575 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -63,6 +63,18 @@ export function buildConfigSchema(): Record { items: { type: "string" }, }, }, + propDrilling: { + type: "object", + description: "Prop-drilling rule configuration.", + properties: { + ignoreComponents: { + type: "array", + items: { type: "string" }, + description: "Additional UI primitive component names to ignore (extends built-in host components).", + }, + }, + additionalProperties: false, + }, }, }; } diff --git a/src/core/types.ts b/src/core/types.ts index a20a899..575dc0b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -44,6 +44,10 @@ export interface DebtLensConfig { maxFiles?: number; /** Concept id -> competing term variants, used by the naming-drift rule. */ vocabulary?: Record; + /** Prop-drilling rule configuration. */ + propDrilling?: { + ignoreComponents?: string[]; + }; } export interface ScanOptions { @@ -58,6 +62,8 @@ export interface ScanOptions { vocabulary?: Record; /** When set, only scan files whose absolute path is in this list (--changed mode). */ changedFiles?: string[]; + /** Prop-drilling rule configuration. */ + propDrillingIgnoreComponents?: string[]; } export interface CliOptions { diff --git a/src/detectors/propDrilling.ts b/src/detectors/propDrilling.ts index e60bb0a..be0c2bd 100644 --- a/src/detectors/propDrilling.ts +++ b/src/detectors/propDrilling.ts @@ -14,6 +14,7 @@ export const propDrillingDetector: Detector = { detect(context: DetectorContext): DebtIssue[] { const issues: DebtIssue[] = []; const maxForwardedProps = context.getThreshold("prop-drilling.maxForwardedProps", 4); + const customIgnoreComponents = context.options.propDrillingIgnoreComponents; for (const file of context.files) { for (const fn of collectFunctionLikes(file)) { @@ -33,7 +34,7 @@ export const propDrillingDetector: Detector = { const tagName = jsx.getTagNameNode().getText(); // Skip host elements (lowercase DOM tags) and UI primitives (RN/icon // components are PascalCase but aren't user-defined children). - if (/^[a-z]/.test(tagName) || isHostComponent(tagName)) continue; + if (/^[a-z]/.test(tagName) || isHostComponent(tagName, customIgnoreComponents)) continue; const childForwarded = new Set(); for (const attribute of jsx.getAttributes()) { if (!Node.isJsxAttribute(attribute)) continue; diff --git a/src/utils/hostComponents.ts b/src/utils/hostComponents.ts index b1800cb..bad7ccf 100644 --- a/src/utils/hostComponents.ts +++ b/src/utils/hostComponents.ts @@ -50,8 +50,14 @@ export const HOST_COMPONENTS = new Set([ /** * Returns true if a JSX tag name refers to a host primitive (so it should be ignored * as a "child component"). Handles namespaced tags like `Animated.View`. + * + * @param tagName - The JSX tag name to check + * @param customIgnoreComponents - Optional array of additional component names to treat as host primitives */ -export function isHostComponent(tagName: string): boolean { +export function isHostComponent(tagName: string, customIgnoreComponents?: string[]): boolean { const base = tagName.split(".").pop() ?? tagName; - return HOST_COMPONENTS.has(base) || HOST_COMPONENTS.has(tagName); + const allComponents = customIgnoreComponents + ? new Set([...HOST_COMPONENTS, ...customIgnoreComponents]) + : HOST_COMPONENTS; + return allComponents.has(base) || allComponents.has(tagName); } diff --git a/tests/detectors/propDrilling.test.ts b/tests/detectors/propDrilling.test.ts index 75de6d9..920489e 100644 --- a/tests/detectors/propDrilling.test.ts +++ b/tests/detectors/propDrilling.test.ts @@ -68,4 +68,34 @@ export function Parent({ a, b, c, d, e }: Props) { const issues = await runDetector(propDrillingDetector, { "Parent.tsx": src }); assert.equal(issues.length, 1); }); + + it("does NOT flag a component forwarding to a custom ignored component", async () => { + const src = ` +export function Parent({ a, b, c, d, e }: Props) { + return ; +} +`; + const issues = await runDetector(propDrillingDetector, { "Parent.tsx": src }, { + propDrillingIgnoreComponents: ["CustomButton"], + }); + assert.equal(issues.length, 0); + }); + + it("flags forwarding to a user-defined child even when other custom components are ignored", async () => { + const src = ` +export function Parent({ a, b, c, d, e, f }: Props) { + return ( + <> + + + + ); +} +`; + const issues = await runDetector(propDrillingDetector, { "Parent.tsx": src }, { + propDrillingIgnoreComponents: ["CustomButton"], + }); + assert.equal(issues.length, 1); + assert.match(issues[0]?.message ?? "", /forwards 4 props/); + }); }); diff --git a/tests/helpers/runDetector.ts b/tests/helpers/runDetector.ts index 781769a..ec1d51c 100644 --- a/tests/helpers/runDetector.ts +++ b/tests/helpers/runDetector.ts @@ -14,6 +14,8 @@ export interface RunDetectorOptions { minSeverity?: ScanOptions["minSeverity"]; /** Naming-drift vocabulary override. */ vocabulary?: Record; + /** Prop-drilling custom ignore components. */ + propDrillingIgnoreComponents?: string[]; } /** @@ -60,6 +62,7 @@ export async function runDetector( rules: undefined, maxFiles: undefined, vocabulary: options.vocabulary, + propDrillingIgnoreComponents: options.propDrillingIgnoreComponents, }; const issues = await detector.detect({