code-gauge 1.8.0 → 1.10.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.
@@ -1,2 +1,2 @@
1
- "use strict";var o=require("node:fs/promises"),e=require("node:path");const r={fileLoc:500,functionLoc:120,componentLoc:350,cognitive:25,cyclomatic:20,call:50,import:25,fanOut:10,parameter:8,duplicateBlock:2,transitiveDependency:25,structuralBreadth:8,structuralCoordination:300,stateMutation:50,duplicateSymbolGroup:5},t="code-gauge.config.json",n=["javascript","jsx","typescript","tsx","python","go","rust","react"],i={python:{stateMutation:90,structuralCoordination:350},react:{import:30}};const s={fileLoc:"fileLocThreshold",functionLoc:"functionLocThreshold",componentLoc:"componentLocThreshold",cognitive:"cognitiveThreshold",cyclomatic:"cyclomaticThreshold",call:"callThreshold",import:"importThreshold",fanOut:"fanOutThreshold",parameter:"parameterThreshold",duplicateBlock:"duplicateBlockThreshold",transitiveDependency:"transitiveDependencyThreshold",structuralBreadth:"structuralBreadthThreshold",structuralCoordination:"structuralCoordinationThreshold",stateMutation:"stateMutationThreshold",duplicateSymbolGroup:"duplicateSymbolGroupThreshold"};function a(o,e){const r={};for(const t of n){const n={...o[t],...e?.[t]};Object.keys(n).length>0&&(r[t]=n)}return r}async function l(e){try{return(await o.stat(e)).isFile()}catch{return!1}}function c(o,e,t){if("object"!=typeof o||null===o||Array.isArray(o))throw new Error(`Config file "${t}": "${e}" must be an object.`);const n={};for(const[i,s]of Object.entries(o)){if(!(i in r))throw new Error(`Config file "${t}": unknown threshold "${i}" in "${e}".`);n[i]=f(s,`${e}.${i}`,t)}return n}function f(o,e,r){if("number"!=typeof o||!Number.isSafeInteger(o)||o<1)throw new Error(`Config file "${r}": "${e}" must be a positive integer.`);return o}function h(o,e,r){if("boolean"!=typeof o)throw new TypeError(`Config file "${r}": "${e}" must be a boolean.`);return o}function u(o){return o instanceof Error?o.message:String(o)}exports.configFileName=t,exports.defaultMaxFindings=20,exports.defaultProfileThresholds=i,exports.defaultThresholds=r,exports.loadConfig=async function(r,i){const s=r??await async function(o){let r=o;for(;;){const o=e.join(r,t);if(await l(o))return o;const n=e.dirname(r);if(n===r)return;r=n}}(i);if(!s)return{};let a,d;try{a=await o.readFile(s,"utf8")}catch(o){if(r)throw new Error(`Cannot read config file "${s}": ${u(o)}`);return{}}try{d=JSON.parse(a)}catch(o){throw new Error(`Invalid JSON in config file "${s}": ${u(o)}`)}return function(o,e){if("object"!=typeof o||null===o||Array.isArray(o))throw new Error(`Config file "${e}" must contain a JSON object.`);const r=o,t={};void 0!==r.thresholds&&(t.thresholds=c(r.thresholds,"thresholds",e));if(void 0!==r.languageThresholds){if("object"!=typeof r.languageThresholds||null===r.languageThresholds||Array.isArray(r.languageThresholds))throw new Error(`Config file "${e}": "languageThresholds" must be an object.`);const o={};for(const[t,i]of Object.entries(r.languageThresholds)){if(!n.includes(t))throw new Error(`Config file "${e}": unknown language profile "${t}" (expected one of ${n.join(", ")}).`);o[t]=c(i,`languageThresholds.${t}`,e)}t.languageThresholds=o}void 0!==r.maxFindings&&(t.maxFindings=f(r.maxFindings,"maxFindings",e));for(const o of["includeTests","failOnRisk","failOnError"])void 0!==r[o]&&(t[o]=h(r[o],o,e));if(void 0!==r.tsconfig){if("string"!=typeof r.tsconfig)throw new TypeError(`Config file "${e}": "tsconfig" must be a string.`);t.tsconfig=r.tsconfig}return t}(d,s)},exports.profileKeys=n,exports.resolveOptions=function(o,e){const t={...r};for(const r of Object.keys(t))t[r]=o[s[r]]??e.thresholds?.[r]??t[r];return{thresholds:t,profileThresholds:a(i,e.languageThresholds),maxFindings:o.maxFindings??e.maxFindings??20,includeTests:o.includeTests??e.includeTests??!1,failOnRisk:o.failOnRisk??e.failOnRisk??!1,failOnError:o.failOnError??e.failOnError??!1,json:o.json??!1,tsconfig:o.tsconfig??e.tsconfig}},exports.resolveThresholds=function(o,e,r){let t=o.thresholds;const n=o.profileThresholds[e];return n&&(t={...t,...n}),r&&o.profileThresholds.react&&(t={...t,...o.profileThresholds.react}),t};
2
- //# sourceMappingURL=cliConfig.cjs.map
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs");let t=require("node:fs/promises"),n=require("node:path");n=e.__toESM(n,1);const r={fileLoc:500,functionLoc:120,componentLoc:350,cognitive:25,cyclomatic:20,call:50,import:25,fanOut:10,parameter:8,duplicateBlock:2,duplicationRatioPercent:30,transitiveDependency:25,structuralBreadth:8,structuralCoordination:300,stateMutation:50,duplicateSymbolGroup:5},i=`code-gauge.config.json`,a=[`javascript`,`jsx`,`typescript`,`tsx`,`python`,`go`,`rust`,`java`,`ruby`,`c`,`cpp`,`react`],o={python:{stateMutation:90,structuralCoordination:350},ruby:{stateMutation:90,structuralCoordination:350},react:{import:30}};function s(e,t,n){let r=e.thresholds,i=e.profileThresholds[t];return i&&(r={...r,...i}),n&&e.profileThresholds.react&&(r={...r,...e.profileThresholds.react}),r}const c={fileLoc:`fileLocThreshold`,functionLoc:`functionLocThreshold`,componentLoc:`componentLocThreshold`,cognitive:`cognitiveThreshold`,cyclomatic:`cyclomaticThreshold`,call:`callThreshold`,import:`importThreshold`,fanOut:`fanOutThreshold`,parameter:`parameterThreshold`,duplicateBlock:`duplicateBlockThreshold`,duplicationRatioPercent:`duplicationRatioPercentThreshold`,transitiveDependency:`transitiveDependencyThreshold`,structuralBreadth:`structuralBreadthThreshold`,structuralCoordination:`structuralCoordinationThreshold`,stateMutation:`stateMutationThreshold`,duplicateSymbolGroup:`duplicateSymbolGroupThreshold`};function l(e,t){let n={...r};for(let r of Object.keys(n))n[r]=e[c[r]]??t.thresholds?.[r]??n[r];return{thresholds:n,profileThresholds:u(o,t.languageThresholds),maxFindings:e.maxFindings??t.maxFindings??20,largestFiles:e.largestFiles??t.largestFiles??0,includeTests:e.includeTests??t.includeTests??!1,failOnRisk:e.failOnRisk??t.failOnRisk??!1,failOnError:e.failOnError??t.failOnError??!1,json:e.json??!1,tsconfig:e.tsconfig??t.tsconfig}}function u(e,t){let n={};for(let r of a){let i={...e[r],...t?.[r]};Object.keys(i).length>0&&(n[r]=i)}return n}async function d(e,n){let r=e??await f(n);if(!r)return{};let i;try{i=await(0,t.readFile)(r,`utf8`)}catch(t){if(e)throw Error(`Cannot read config file "${r}": ${v(t)}`);return{}}let a;try{a=JSON.parse(i)}catch(e){throw Error(`Invalid JSON in config file "${r}": ${v(e)}`)}return m(a,r)}async function f(e){let t=e;for(;;){let e=n.default.join(t,i);if(await p(e))return e;let r=n.default.dirname(t);if(r===t)return;t=r}}async function p(e){try{return(await(0,t.stat)(e)).isFile()}catch{return!1}}function m(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r={};if(n.thresholds!==void 0&&(r.thresholds=h(n.thresholds,`thresholds`,t)),n.languageThresholds!==void 0){if(typeof n.languageThresholds!=`object`||n.languageThresholds===null||Array.isArray(n.languageThresholds))throw Error(`Config file "${t}": "languageThresholds" must be an object.`);let e={};for(let[r,i]of Object.entries(n.languageThresholds)){if(!a.includes(r))throw Error(`Config file "${t}": unknown language profile "${r}" (expected one of ${a.join(`, `)}).`);e[r]=h(i,`languageThresholds.${r}`,t)}r.languageThresholds=e}n.maxFindings!==void 0&&(r.maxFindings=g(n.maxFindings,`maxFindings`,t)),n.largestFiles!==void 0&&(r.largestFiles=g(n.largestFiles,`largestFiles`,t));for(let e of[`includeTests`,`failOnRisk`,`failOnError`])n[e]!==void 0&&(r[e]=_(n[e],e,t));if(n.tsconfig!==void 0){if(typeof n.tsconfig!=`string`)throw TypeError(`Config file "${t}": "tsconfig" must be a string.`);r.tsconfig=n.tsconfig}return r}function h(e,t,n){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${n}": "${t}" must be an object.`);let i={};for(let[a,o]of Object.entries(e)){if(!(a in r))throw Error(`Config file "${n}": unknown threshold "${a}" in "${t}".`);let e=g(o,`${t}.${a}`,n);if(a===`duplicationRatioPercent`&&e>100)throw Error(`Config file "${n}": "${t}.${a}" must be between 1 and 100.`);i[a]=e}return i}function g(e,t,n){if(typeof e!=`number`||!Number.isSafeInteger(e)||e<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return e}function _(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function v(e){return e instanceof Error?e.message:String(e)}exports.configFileName=i,exports.loadConfig=d,exports.resolveOptions=l,exports.resolveThresholds=s;
2
+ //# sourceMappingURL=cliConfig.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"cliConfig.cjs","sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Risk thresholds; a finding is reported when the measured value is greater than or equal to the threshold. */\nexport interface Thresholds {\n fileLoc: number;\n functionLoc: number;\n componentLoc: number;\n cognitive: number;\n cyclomatic: number;\n call: number;\n import: number;\n fanOut: number;\n parameter: number;\n duplicateBlock: number;\n transitiveDependency: number;\n structuralBreadth: number;\n structuralCoordination: number;\n stateMutation: number;\n duplicateSymbolGroup: number;\n}\n\n// Defaults tuned against blind human labels across five representative WillBooster/WillBoosterLab\n// repositories to maximize F1 (precision without sacrificing recall); see PR for the evaluation.\nexport const defaultThresholds: Thresholds = {\n fileLoc: 500,\n functionLoc: 120,\n componentLoc: 350,\n cognitive: 25,\n cyclomatic: 20,\n call: 50,\n import: 25,\n fanOut: 10,\n parameter: 8,\n duplicateBlock: 2,\n transitiveDependency: 25,\n structuralBreadth: 8,\n structuralCoordination: 300,\n stateMutation: 50,\n duplicateSymbolGroup: 5,\n};\n\nexport const defaultMaxFindings = 20;\nexport const configFileName = 'code-gauge.config.json';\n\n/**\n * Profile keys for per-language and React-specific threshold overrides. A file resolves its\n * thresholds as base → its language profile → the `react` profile (when it contains a component).\n */\nexport const profileKeys = ['javascript', 'jsx', 'typescript', 'tsx', 'python', 'go', 'rust', 'react'] as const;\nexport type ProfileKey = (typeof profileKeys)[number];\n\n/**\n * Built-in per-profile overrides, calibrated because some metric distributions differ sharply by\n * language/type: Python treats every binding as an assignment (so `stateMutation` runs ~10x higher)\n * and coordinates more per file, while React files import roughly twice as many sources as pure TS.\n */\nexport const defaultProfileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {\n python: { stateMutation: 90, structuralCoordination: 350 },\n react: { import: 30 },\n};\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n thresholds?: Partial<Thresholds>;\n /** Per-profile overrides keyed by language name or `react`; merged over `thresholds` for matching files. */\n languageThresholds?: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n tsconfig?: string;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n thresholds: Thresholds;\n profileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings: number;\n includeTests: boolean;\n failOnRisk: boolean;\n failOnError: boolean;\n json: boolean;\n tsconfig?: string;\n}\n\n/**\n * Resolves the thresholds for a single file: the base thresholds overlaid with its language profile\n * and then, when the file contains a React component, the `react` profile.\n */\nexport function resolveThresholds(options: ResolvedOptions, language: string, isReact: boolean): Thresholds {\n let thresholds = options.thresholds;\n const languageOverride = options.profileThresholds[language as ProfileKey];\n if (languageOverride) {\n thresholds = { ...thresholds, ...languageOverride };\n }\n if (isReact && options.profileThresholds.react) {\n thresholds = { ...thresholds, ...options.profileThresholds.react };\n }\n return thresholds;\n}\n\n/** Raw command-line options; every threshold is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n fileLocThreshold?: number;\n functionLocThreshold?: number;\n componentLocThreshold?: number;\n cognitiveThreshold?: number;\n cyclomaticThreshold?: number;\n callThreshold?: number;\n importThreshold?: number;\n fanOutThreshold?: number;\n parameterThreshold?: number;\n duplicateBlockThreshold?: number;\n transitiveDependencyThreshold?: number;\n structuralBreadthThreshold?: number;\n structuralCoordinationThreshold?: number;\n stateMutationThreshold?: number;\n duplicateSymbolGroupThreshold?: number;\n maxFindings?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n json?: boolean;\n tsconfig?: string;\n}\n\n/** Maps each threshold to the matching command-line flag; the config key equals the flag without the `-threshold` suffix. */\nconst thresholdCliKeys: Record<keyof Thresholds, keyof CliOptions> = {\n fileLoc: 'fileLocThreshold',\n functionLoc: 'functionLocThreshold',\n componentLoc: 'componentLocThreshold',\n cognitive: 'cognitiveThreshold',\n cyclomatic: 'cyclomaticThreshold',\n call: 'callThreshold',\n import: 'importThreshold',\n fanOut: 'fanOutThreshold',\n parameter: 'parameterThreshold',\n duplicateBlock: 'duplicateBlockThreshold',\n transitiveDependency: 'transitiveDependencyThreshold',\n structuralBreadth: 'structuralBreadthThreshold',\n structuralCoordination: 'structuralCoordinationThreshold',\n stateMutation: 'stateMutationThreshold',\n duplicateSymbolGroup: 'duplicateSymbolGroupThreshold',\n};\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n const thresholds = { ...defaultThresholds };\n for (const key of Object.keys(thresholds) as (keyof Thresholds)[]) {\n thresholds[key] = (cli[thresholdCliKeys[key]] as number | undefined) ?? config.thresholds?.[key] ?? thresholds[key];\n }\n\n return {\n thresholds,\n profileThresholds: mergeProfileThresholds(defaultProfileThresholds, config.languageThresholds),\n maxFindings: cli.maxFindings ?? config.maxFindings ?? defaultMaxFindings,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnRisk: cli.failOnRisk ?? config.failOnRisk ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n tsconfig: cli.tsconfig ?? config.tsconfig,\n };\n}\n\n/** Merges user-supplied per-profile overrides on top of the built-in ones, per profile. */\nfunction mergeProfileThresholds(\n defaults: Partial<Record<ProfileKey, Partial<Thresholds>>>,\n overrides: Partial<Record<ProfileKey, Partial<Thresholds>>> | undefined\n): Partial<Record<ProfileKey, Partial<Thresholds>>> {\n const merged: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const key of profileKeys) {\n const combined = { ...defaults[key], ...overrides?.[key] };\n if (Object.keys(combined).length > 0) {\n merged[key] = combined;\n }\n }\n return merged;\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const config: CodeGaugeConfig = {};\n\n if (raw.thresholds !== undefined) {\n config.thresholds = validateThresholdObject(raw.thresholds, 'thresholds', configFile);\n }\n\n if (raw.languageThresholds !== undefined) {\n if (\n typeof raw.languageThresholds !== 'object' ||\n raw.languageThresholds === null ||\n Array.isArray(raw.languageThresholds)\n ) {\n throw new Error(`Config file \"${configFile}\": \"languageThresholds\" must be an object.`);\n }\n const languageThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const [profile, thresholds] of Object.entries(raw.languageThresholds as Record<string, unknown>)) {\n if (!(profileKeys as readonly string[]).includes(profile)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language profile \"${profile}\" (expected one of ${profileKeys.join(', ')}).`\n );\n }\n languageThresholds[profile as ProfileKey] = validateThresholdObject(\n thresholds,\n `languageThresholds.${profile}`,\n configFile\n );\n }\n config.languageThresholds = languageThresholds;\n }\n\n if (raw.maxFindings !== undefined) {\n config.maxFindings = requirePositiveInteger(raw.maxFindings, 'maxFindings', configFile);\n }\n for (const key of ['includeTests', 'failOnRisk', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n if (raw.tsconfig !== undefined) {\n if (typeof raw.tsconfig !== 'string') {\n throw new TypeError(`Config file \"${configFile}\": \"tsconfig\" must be a string.`);\n }\n config.tsconfig = raw.tsconfig;\n }\n\n return config;\n}\n\nfunction validateThresholdObject(value: unknown, label: string, configFile: string): Partial<Thresholds> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${label}\" must be an object.`);\n }\n const thresholds: Partial<Thresholds> = {};\n for (const [key, threshold] of Object.entries(value as Record<string, unknown>)) {\n if (!(key in defaultThresholds)) {\n throw new Error(`Config file \"${configFile}\": unknown threshold \"${key}\" in \"${label}\".`);\n }\n thresholds[key as keyof Thresholds] = requirePositiveInteger(threshold, `${label}.${key}`, configFile);\n }\n return thresholds;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["defaultThresholds","fileLoc","functionLoc","componentLoc","cognitive","cyclomatic","call","import","fanOut","parameter","duplicateBlock","transitiveDependency","structuralBreadth","structuralCoordination","stateMutation","duplicateSymbolGroup","configFileName","profileKeys","defaultProfileThresholds","python","react","thresholdCliKeys","mergeProfileThresholds","defaults","overrides","merged","key","combined","Object","keys","length","async","fileExists","file","stat","isFile","validateThresholdObject","value","label","configFile","Array","isArray","Error","thresholds","threshold","entries","requirePositiveInteger","Number","isSafeInteger","requireBoolean","TypeError","formatError","error","message","String","explicitPath","targetDirectory","currentDirectory","path","join","parentDirectory","dirname","findNearestConfig","content","parsed","readFile","JSON","parse","raw","config","undefined","languageThresholds","profile","includes","maxFindings","tsconfig","validateConfig","cli","profileThresholds","includeTests","failOnRisk","failOnError","json","options","language","isReact","languageOverride"],"mappings":"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"}
1
+ {"version":3,"file":"cliConfig.cjs","names":["path"],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Risk thresholds; a finding is reported when the measured value is greater than or equal to the threshold. */\nexport interface Thresholds {\n fileLoc: number;\n functionLoc: number;\n componentLoc: number;\n cognitive: number;\n cyclomatic: number;\n call: number;\n import: number;\n fanOut: number;\n parameter: number;\n duplicateBlock: number;\n /** Percentage (1-100) of a file's code lines (comments/blanks excluded) covered by duplicates. */\n duplicationRatioPercent: number;\n transitiveDependency: number;\n structuralBreadth: number;\n structuralCoordination: number;\n stateMutation: number;\n duplicateSymbolGroup: number;\n}\n\n// Defaults tuned against blind human labels across five representative WillBooster/WillBoosterLab\n// repositories to maximize F1 (precision without sacrificing recall); see PR for the evaluation.\nexport const defaultThresholds: Thresholds = {\n fileLoc: 500,\n functionLoc: 120,\n componentLoc: 350,\n cognitive: 25,\n cyclomatic: 20,\n call: 50,\n import: 25,\n fanOut: 10,\n parameter: 8,\n duplicateBlock: 2,\n duplicationRatioPercent: 30,\n transitiveDependency: 25,\n structuralBreadth: 8,\n structuralCoordination: 300,\n stateMutation: 50,\n duplicateSymbolGroup: 5,\n};\n\nexport const defaultMaxFindings = 20;\nexport const configFileName = 'code-gauge.config.json';\n\n/**\n * Profile keys for per-language and React-specific threshold overrides. A file resolves its\n * thresholds as base → its language profile → the `react` profile (when it contains a component).\n */\nexport const profileKeys = [\n 'javascript',\n 'jsx',\n 'typescript',\n 'tsx',\n 'python',\n 'go',\n 'rust',\n 'java',\n 'ruby',\n 'c',\n 'cpp',\n 'react',\n] as const;\nexport type ProfileKey = (typeof profileKeys)[number];\n\n/**\n * Built-in per-profile overrides, calibrated because some metric distributions differ sharply by\n * language/type: Python treats every binding as an assignment (so `stateMutation` runs ~10x higher)\n * and coordinates more per file, while React files import roughly twice as many sources as pure TS.\n */\nexport const defaultProfileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {\n python: { stateMutation: 90, structuralCoordination: 350 },\n // Ruby scores state mutation via assignments like Python (bindings are assignments).\n ruby: { stateMutation: 90, structuralCoordination: 350 },\n react: { import: 30 },\n};\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n thresholds?: Partial<Thresholds>;\n /** Per-profile overrides keyed by language name or `react`; merged over `thresholds` for matching files. */\n languageThresholds?: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings?: number;\n largestFiles?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n tsconfig?: string;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n thresholds: Thresholds;\n profileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings: number;\n /** Number of largest files by code LOC to list; 0 disables the section. */\n largestFiles: number;\n includeTests: boolean;\n failOnRisk: boolean;\n failOnError: boolean;\n json: boolean;\n tsconfig?: string;\n}\n\n/**\n * Resolves the thresholds for a single file: the base thresholds overlaid with its language profile\n * and then, when the file contains a React component, the `react` profile.\n */\nexport function resolveThresholds(options: ResolvedOptions, language: string, isReact: boolean): Thresholds {\n let thresholds = options.thresholds;\n const languageOverride = options.profileThresholds[language as ProfileKey];\n if (languageOverride) {\n thresholds = { ...thresholds, ...languageOverride };\n }\n if (isReact && options.profileThresholds.react) {\n thresholds = { ...thresholds, ...options.profileThresholds.react };\n }\n return thresholds;\n}\n\n/** Raw command-line options; every threshold is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n fileLocThreshold?: number;\n functionLocThreshold?: number;\n componentLocThreshold?: number;\n cognitiveThreshold?: number;\n cyclomaticThreshold?: number;\n callThreshold?: number;\n importThreshold?: number;\n fanOutThreshold?: number;\n parameterThreshold?: number;\n duplicateBlockThreshold?: number;\n duplicationRatioPercentThreshold?: number;\n transitiveDependencyThreshold?: number;\n structuralBreadthThreshold?: number;\n structuralCoordinationThreshold?: number;\n stateMutationThreshold?: number;\n duplicateSymbolGroupThreshold?: number;\n maxFindings?: number;\n largestFiles?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n json?: boolean;\n tsconfig?: string;\n}\n\n/** Maps each threshold to the matching command-line flag; the config key equals the flag without the `-threshold` suffix. */\nconst thresholdCliKeys: Record<keyof Thresholds, keyof CliOptions> = {\n fileLoc: 'fileLocThreshold',\n functionLoc: 'functionLocThreshold',\n componentLoc: 'componentLocThreshold',\n cognitive: 'cognitiveThreshold',\n cyclomatic: 'cyclomaticThreshold',\n call: 'callThreshold',\n import: 'importThreshold',\n fanOut: 'fanOutThreshold',\n parameter: 'parameterThreshold',\n duplicateBlock: 'duplicateBlockThreshold',\n duplicationRatioPercent: 'duplicationRatioPercentThreshold',\n transitiveDependency: 'transitiveDependencyThreshold',\n structuralBreadth: 'structuralBreadthThreshold',\n structuralCoordination: 'structuralCoordinationThreshold',\n stateMutation: 'stateMutationThreshold',\n duplicateSymbolGroup: 'duplicateSymbolGroupThreshold',\n};\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n const thresholds = { ...defaultThresholds };\n for (const key of Object.keys(thresholds) as (keyof Thresholds)[]) {\n thresholds[key] = (cli[thresholdCliKeys[key]] as number | undefined) ?? config.thresholds?.[key] ?? thresholds[key];\n }\n\n return {\n thresholds,\n profileThresholds: mergeProfileThresholds(defaultProfileThresholds, config.languageThresholds),\n maxFindings: cli.maxFindings ?? config.maxFindings ?? defaultMaxFindings,\n largestFiles: cli.largestFiles ?? config.largestFiles ?? 0,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnRisk: cli.failOnRisk ?? config.failOnRisk ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n tsconfig: cli.tsconfig ?? config.tsconfig,\n };\n}\n\n/** Merges user-supplied per-profile overrides on top of the built-in ones, per profile. */\nfunction mergeProfileThresholds(\n defaults: Partial<Record<ProfileKey, Partial<Thresholds>>>,\n overrides: Partial<Record<ProfileKey, Partial<Thresholds>>> | undefined\n): Partial<Record<ProfileKey, Partial<Thresholds>>> {\n const merged: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const key of profileKeys) {\n const combined = { ...defaults[key], ...overrides?.[key] };\n if (Object.keys(combined).length > 0) {\n merged[key] = combined;\n }\n }\n return merged;\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const config: CodeGaugeConfig = {};\n\n if (raw.thresholds !== undefined) {\n config.thresholds = validateThresholdObject(raw.thresholds, 'thresholds', configFile);\n }\n\n if (raw.languageThresholds !== undefined) {\n if (\n typeof raw.languageThresholds !== 'object' ||\n raw.languageThresholds === null ||\n Array.isArray(raw.languageThresholds)\n ) {\n throw new Error(`Config file \"${configFile}\": \"languageThresholds\" must be an object.`);\n }\n const languageThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const [profile, thresholds] of Object.entries(raw.languageThresholds as Record<string, unknown>)) {\n if (!(profileKeys as readonly string[]).includes(profile)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language profile \"${profile}\" (expected one of ${profileKeys.join(', ')}).`\n );\n }\n languageThresholds[profile as ProfileKey] = validateThresholdObject(\n thresholds,\n `languageThresholds.${profile}`,\n configFile\n );\n }\n config.languageThresholds = languageThresholds;\n }\n\n if (raw.maxFindings !== undefined) {\n config.maxFindings = requirePositiveInteger(raw.maxFindings, 'maxFindings', configFile);\n }\n if (raw.largestFiles !== undefined) {\n config.largestFiles = requirePositiveInteger(raw.largestFiles, 'largestFiles', configFile);\n }\n for (const key of ['includeTests', 'failOnRisk', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n if (raw.tsconfig !== undefined) {\n if (typeof raw.tsconfig !== 'string') {\n throw new TypeError(`Config file \"${configFile}\": \"tsconfig\" must be a string.`);\n }\n config.tsconfig = raw.tsconfig;\n }\n\n return config;\n}\n\nfunction validateThresholdObject(value: unknown, label: string, configFile: string): Partial<Thresholds> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${label}\" must be an object.`);\n }\n const thresholds: Partial<Thresholds> = {};\n for (const [key, threshold] of Object.entries(value as Record<string, unknown>)) {\n if (!(key in defaultThresholds)) {\n throw new Error(`Config file \"${configFile}\": unknown threshold \"${key}\" in \"${label}\".`);\n }\n const parsed = requirePositiveInteger(threshold, `${label}.${key}`, configFile);\n if (key === 'duplicationRatioPercent' && parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"${label}.${key}\" must be between 1 and 100.`);\n }\n thresholds[key as keyof Thresholds] = parsed;\n }\n return thresholds;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
@@ -10,6 +10,8 @@ export interface Thresholds {
10
10
  fanOut: number;
11
11
  parameter: number;
12
12
  duplicateBlock: number;
13
+ /** Percentage (1-100) of a file's code lines (comments/blanks excluded) covered by duplicates. */
14
+ duplicationRatioPercent: number;
13
15
  transitiveDependency: number;
14
16
  structuralBreadth: number;
15
17
  structuralCoordination: number;
@@ -23,7 +25,7 @@ export declare const configFileName = "code-gauge.config.json";
23
25
  * Profile keys for per-language and React-specific threshold overrides. A file resolves its
24
26
  * thresholds as base → its language profile → the `react` profile (when it contains a component).
25
27
  */
26
- export declare const profileKeys: readonly ['javascript', 'jsx', 'typescript', 'tsx', 'python', 'go', 'rust', 'react'];
28
+ export declare const profileKeys: readonly ['javascript', 'jsx', 'typescript', 'tsx', 'python', 'go', 'rust', 'java', 'ruby', 'c', 'cpp', 'react'];
27
29
  export type ProfileKey = (typeof profileKeys)[number];
28
30
  /**
29
31
  * Built-in per-profile overrides, calibrated because some metric distributions differ sharply by
@@ -37,6 +39,7 @@ export interface CodeGaugeConfig {
37
39
  /** Per-profile overrides keyed by language name or `react`; merged over `thresholds` for matching files. */
38
40
  languageThresholds?: Partial<Record<ProfileKey, Partial<Thresholds>>>;
39
41
  maxFindings?: number;
42
+ largestFiles?: number;
40
43
  includeTests?: boolean;
41
44
  failOnRisk?: boolean;
42
45
  failOnError?: boolean;
@@ -47,6 +50,8 @@ export interface ResolvedOptions {
47
50
  thresholds: Thresholds;
48
51
  profileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>>;
49
52
  maxFindings: number;
53
+ /** Number of largest files by code LOC to list; 0 disables the section. */
54
+ largestFiles: number;
50
55
  includeTests: boolean;
51
56
  failOnRisk: boolean;
52
57
  failOnError: boolean;
@@ -71,12 +76,14 @@ export interface CliOptions {
71
76
  fanOutThreshold?: number;
72
77
  parameterThreshold?: number;
73
78
  duplicateBlockThreshold?: number;
79
+ duplicationRatioPercentThreshold?: number;
74
80
  transitiveDependencyThreshold?: number;
75
81
  structuralBreadthThreshold?: number;
76
82
  structuralCoordinationThreshold?: number;
77
83
  stateMutationThreshold?: number;
78
84
  duplicateSymbolGroupThreshold?: number;
79
85
  maxFindings?: number;
86
+ largestFiles?: number;
80
87
  includeTests?: boolean;
81
88
  failOnRisk?: boolean;
82
89
  failOnError?: boolean;
package/dist/cliConfig.js CHANGED
@@ -1,2 +1,2 @@
1
- import{readFile as o,stat as r}from"node:fs/promises";import t from"node:path";const n={fileLoc:500,functionLoc:120,componentLoc:350,cognitive:25,cyclomatic:20,call:50,import:25,fanOut:10,parameter:8,duplicateBlock:2,transitiveDependency:25,structuralBreadth:8,structuralCoordination:300,stateMutation:50,duplicateSymbolGroup:5},e=20,i="code-gauge.config.json",s=["javascript","jsx","typescript","tsx","python","go","rust","react"],a={python:{stateMutation:90,structuralCoordination:350},react:{import:30}};function c(o,r,t){let n=o.thresholds;const e=o.profileThresholds[r];return e&&(n={...n,...e}),t&&o.profileThresholds.react&&(n={...n,...o.profileThresholds.react}),n}const l={fileLoc:"fileLocThreshold",functionLoc:"functionLocThreshold",componentLoc:"componentLocThreshold",cognitive:"cognitiveThreshold",cyclomatic:"cyclomaticThreshold",call:"callThreshold",import:"importThreshold",fanOut:"fanOutThreshold",parameter:"parameterThreshold",duplicateBlock:"duplicateBlockThreshold",transitiveDependency:"transitiveDependencyThreshold",structuralBreadth:"structuralBreadthThreshold",structuralCoordination:"structuralCoordinationThreshold",stateMutation:"stateMutationThreshold",duplicateSymbolGroup:"duplicateSymbolGroupThreshold"};function f(o,r){const t={...n};for(const n of Object.keys(t))t[n]=o[l[n]]??r.thresholds?.[n]??t[n];return{thresholds:t,profileThresholds:h(a,r.languageThresholds),maxFindings:o.maxFindings??r.maxFindings??20,includeTests:o.includeTests??r.includeTests??!1,failOnRisk:o.failOnRisk??r.failOnRisk??!1,failOnError:o.failOnError??r.failOnError??!1,json:o.json??!1,tsconfig:o.tsconfig??r.tsconfig}}function h(o,r){const t={};for(const n of s){const e={...o[n],...r?.[n]};Object.keys(e).length>0&&(t[n]=e)}return t}async function u(r,n){const e=r??await async function(o){let r=o;for(;;){const o=t.join(r,i);if(await d(o))return o;const n=t.dirname(r);if(n===r)return;r=n}}(n);if(!e)return{};let a,c;try{a=await o(e,"utf8")}catch(o){if(r)throw new Error(`Cannot read config file "${e}": ${y(o)}`);return{}}try{c=JSON.parse(a)}catch(o){throw new Error(`Invalid JSON in config file "${e}": ${y(o)}`)}return function(o,r){if("object"!=typeof o||null===o||Array.isArray(o))throw new Error(`Config file "${r}" must contain a JSON object.`);const t=o,n={};void 0!==t.thresholds&&(n.thresholds=g(t.thresholds,"thresholds",r));if(void 0!==t.languageThresholds){if("object"!=typeof t.languageThresholds||null===t.languageThresholds||Array.isArray(t.languageThresholds))throw new Error(`Config file "${r}": "languageThresholds" must be an object.`);const o={};for(const[n,e]of Object.entries(t.languageThresholds)){if(!s.includes(n))throw new Error(`Config file "${r}": unknown language profile "${n}" (expected one of ${s.join(", ")}).`);o[n]=g(e,`languageThresholds.${n}`,r)}n.languageThresholds=o}void 0!==t.maxFindings&&(n.maxFindings=p(t.maxFindings,"maxFindings",r));for(const o of["includeTests","failOnRisk","failOnError"])void 0!==t[o]&&(n[o]=m(t[o],o,r));if(void 0!==t.tsconfig){if("string"!=typeof t.tsconfig)throw new TypeError(`Config file "${r}": "tsconfig" must be a string.`);n.tsconfig=t.tsconfig}return n}(c,e)}async function d(o){try{return(await r(o)).isFile()}catch{return!1}}function g(o,r,t){if("object"!=typeof o||null===o||Array.isArray(o))throw new Error(`Config file "${t}": "${r}" must be an object.`);const e={};for(const[i,s]of Object.entries(o)){if(!(i in n))throw new Error(`Config file "${t}": unknown threshold "${i}" in "${r}".`);e[i]=p(s,`${r}.${i}`,t)}return e}function p(o,r,t){if("number"!=typeof o||!Number.isSafeInteger(o)||o<1)throw new Error(`Config file "${t}": "${r}" must be a positive integer.`);return o}function m(o,r,t){if("boolean"!=typeof o)throw new TypeError(`Config file "${t}": "${r}" must be a boolean.`);return o}function y(o){return o instanceof Error?o.message:String(o)}export{i as configFileName,e as defaultMaxFindings,a as defaultProfileThresholds,n as defaultThresholds,u as loadConfig,s as profileKeys,f as resolveOptions,c as resolveThresholds};
2
- //# sourceMappingURL=cliConfig.js.map
1
+ import{readFile as e,stat as t}from"node:fs/promises";import n from"node:path";const r={fileLoc:500,functionLoc:120,componentLoc:350,cognitive:25,cyclomatic:20,call:50,import:25,fanOut:10,parameter:8,duplicateBlock:2,duplicationRatioPercent:30,transitiveDependency:25,structuralBreadth:8,structuralCoordination:300,stateMutation:50,duplicateSymbolGroup:5},i=`code-gauge.config.json`,a=[`javascript`,`jsx`,`typescript`,`tsx`,`python`,`go`,`rust`,`java`,`ruby`,`c`,`cpp`,`react`],o={python:{stateMutation:90,structuralCoordination:350},ruby:{stateMutation:90,structuralCoordination:350},react:{import:30}};function s(e,t,n){let r=e.thresholds,i=e.profileThresholds[t];return i&&(r={...r,...i}),n&&e.profileThresholds.react&&(r={...r,...e.profileThresholds.react}),r}const c={fileLoc:`fileLocThreshold`,functionLoc:`functionLocThreshold`,componentLoc:`componentLocThreshold`,cognitive:`cognitiveThreshold`,cyclomatic:`cyclomaticThreshold`,call:`callThreshold`,import:`importThreshold`,fanOut:`fanOutThreshold`,parameter:`parameterThreshold`,duplicateBlock:`duplicateBlockThreshold`,duplicationRatioPercent:`duplicationRatioPercentThreshold`,transitiveDependency:`transitiveDependencyThreshold`,structuralBreadth:`structuralBreadthThreshold`,structuralCoordination:`structuralCoordinationThreshold`,stateMutation:`stateMutationThreshold`,duplicateSymbolGroup:`duplicateSymbolGroupThreshold`};function l(e,t){let n={...r};for(let r of Object.keys(n))n[r]=e[c[r]]??t.thresholds?.[r]??n[r];return{thresholds:n,profileThresholds:u(o,t.languageThresholds),maxFindings:e.maxFindings??t.maxFindings??20,largestFiles:e.largestFiles??t.largestFiles??0,includeTests:e.includeTests??t.includeTests??!1,failOnRisk:e.failOnRisk??t.failOnRisk??!1,failOnError:e.failOnError??t.failOnError??!1,json:e.json??!1,tsconfig:e.tsconfig??t.tsconfig}}function u(e,t){let n={};for(let r of a){let i={...e[r],...t?.[r]};Object.keys(i).length>0&&(n[r]=i)}return n}async function d(t,n){let r=t??await f(n);if(!r)return{};let i;try{i=await e(r,`utf8`)}catch(e){if(t)throw Error(`Cannot read config file "${r}": ${v(e)}`);return{}}let a;try{a=JSON.parse(i)}catch(e){throw Error(`Invalid JSON in config file "${r}": ${v(e)}`)}return m(a,r)}async function f(e){let t=e;for(;;){let e=n.join(t,i);if(await p(e))return e;let r=n.dirname(t);if(r===t)return;t=r}}async function p(e){try{return(await t(e)).isFile()}catch{return!1}}function m(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r={};if(n.thresholds!==void 0&&(r.thresholds=h(n.thresholds,`thresholds`,t)),n.languageThresholds!==void 0){if(typeof n.languageThresholds!=`object`||n.languageThresholds===null||Array.isArray(n.languageThresholds))throw Error(`Config file "${t}": "languageThresholds" must be an object.`);let e={};for(let[r,i]of Object.entries(n.languageThresholds)){if(!a.includes(r))throw Error(`Config file "${t}": unknown language profile "${r}" (expected one of ${a.join(`, `)}).`);e[r]=h(i,`languageThresholds.${r}`,t)}r.languageThresholds=e}n.maxFindings!==void 0&&(r.maxFindings=g(n.maxFindings,`maxFindings`,t)),n.largestFiles!==void 0&&(r.largestFiles=g(n.largestFiles,`largestFiles`,t));for(let e of[`includeTests`,`failOnRisk`,`failOnError`])n[e]!==void 0&&(r[e]=_(n[e],e,t));if(n.tsconfig!==void 0){if(typeof n.tsconfig!=`string`)throw TypeError(`Config file "${t}": "tsconfig" must be a string.`);r.tsconfig=n.tsconfig}return r}function h(e,t,n){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${n}": "${t}" must be an object.`);let i={};for(let[a,o]of Object.entries(e)){if(!(a in r))throw Error(`Config file "${n}": unknown threshold "${a}" in "${t}".`);let e=g(o,`${t}.${a}`,n);if(a===`duplicationRatioPercent`&&e>100)throw Error(`Config file "${n}": "${t}.${a}" must be between 1 and 100.`);i[a]=e}return i}function g(e,t,n){if(typeof e!=`number`||!Number.isSafeInteger(e)||e<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return e}function _(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function v(e){return e instanceof Error?e.message:String(e)}export{i as configFileName,d as loadConfig,l as resolveOptions,s as resolveThresholds};
2
+ //# sourceMappingURL=cliConfig.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cliConfig.js","sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Risk thresholds; a finding is reported when the measured value is greater than or equal to the threshold. */\nexport interface Thresholds {\n fileLoc: number;\n functionLoc: number;\n componentLoc: number;\n cognitive: number;\n cyclomatic: number;\n call: number;\n import: number;\n fanOut: number;\n parameter: number;\n duplicateBlock: number;\n transitiveDependency: number;\n structuralBreadth: number;\n structuralCoordination: number;\n stateMutation: number;\n duplicateSymbolGroup: number;\n}\n\n// Defaults tuned against blind human labels across five representative WillBooster/WillBoosterLab\n// repositories to maximize F1 (precision without sacrificing recall); see PR for the evaluation.\nexport const defaultThresholds: Thresholds = {\n fileLoc: 500,\n functionLoc: 120,\n componentLoc: 350,\n cognitive: 25,\n cyclomatic: 20,\n call: 50,\n import: 25,\n fanOut: 10,\n parameter: 8,\n duplicateBlock: 2,\n transitiveDependency: 25,\n structuralBreadth: 8,\n structuralCoordination: 300,\n stateMutation: 50,\n duplicateSymbolGroup: 5,\n};\n\nexport const defaultMaxFindings = 20;\nexport const configFileName = 'code-gauge.config.json';\n\n/**\n * Profile keys for per-language and React-specific threshold overrides. A file resolves its\n * thresholds as base → its language profile → the `react` profile (when it contains a component).\n */\nexport const profileKeys = ['javascript', 'jsx', 'typescript', 'tsx', 'python', 'go', 'rust', 'react'] as const;\nexport type ProfileKey = (typeof profileKeys)[number];\n\n/**\n * Built-in per-profile overrides, calibrated because some metric distributions differ sharply by\n * language/type: Python treats every binding as an assignment (so `stateMutation` runs ~10x higher)\n * and coordinates more per file, while React files import roughly twice as many sources as pure TS.\n */\nexport const defaultProfileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {\n python: { stateMutation: 90, structuralCoordination: 350 },\n react: { import: 30 },\n};\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n thresholds?: Partial<Thresholds>;\n /** Per-profile overrides keyed by language name or `react`; merged over `thresholds` for matching files. */\n languageThresholds?: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n tsconfig?: string;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n thresholds: Thresholds;\n profileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings: number;\n includeTests: boolean;\n failOnRisk: boolean;\n failOnError: boolean;\n json: boolean;\n tsconfig?: string;\n}\n\n/**\n * Resolves the thresholds for a single file: the base thresholds overlaid with its language profile\n * and then, when the file contains a React component, the `react` profile.\n */\nexport function resolveThresholds(options: ResolvedOptions, language: string, isReact: boolean): Thresholds {\n let thresholds = options.thresholds;\n const languageOverride = options.profileThresholds[language as ProfileKey];\n if (languageOverride) {\n thresholds = { ...thresholds, ...languageOverride };\n }\n if (isReact && options.profileThresholds.react) {\n thresholds = { ...thresholds, ...options.profileThresholds.react };\n }\n return thresholds;\n}\n\n/** Raw command-line options; every threshold is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n fileLocThreshold?: number;\n functionLocThreshold?: number;\n componentLocThreshold?: number;\n cognitiveThreshold?: number;\n cyclomaticThreshold?: number;\n callThreshold?: number;\n importThreshold?: number;\n fanOutThreshold?: number;\n parameterThreshold?: number;\n duplicateBlockThreshold?: number;\n transitiveDependencyThreshold?: number;\n structuralBreadthThreshold?: number;\n structuralCoordinationThreshold?: number;\n stateMutationThreshold?: number;\n duplicateSymbolGroupThreshold?: number;\n maxFindings?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n json?: boolean;\n tsconfig?: string;\n}\n\n/** Maps each threshold to the matching command-line flag; the config key equals the flag without the `-threshold` suffix. */\nconst thresholdCliKeys: Record<keyof Thresholds, keyof CliOptions> = {\n fileLoc: 'fileLocThreshold',\n functionLoc: 'functionLocThreshold',\n componentLoc: 'componentLocThreshold',\n cognitive: 'cognitiveThreshold',\n cyclomatic: 'cyclomaticThreshold',\n call: 'callThreshold',\n import: 'importThreshold',\n fanOut: 'fanOutThreshold',\n parameter: 'parameterThreshold',\n duplicateBlock: 'duplicateBlockThreshold',\n transitiveDependency: 'transitiveDependencyThreshold',\n structuralBreadth: 'structuralBreadthThreshold',\n structuralCoordination: 'structuralCoordinationThreshold',\n stateMutation: 'stateMutationThreshold',\n duplicateSymbolGroup: 'duplicateSymbolGroupThreshold',\n};\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n const thresholds = { ...defaultThresholds };\n for (const key of Object.keys(thresholds) as (keyof Thresholds)[]) {\n thresholds[key] = (cli[thresholdCliKeys[key]] as number | undefined) ?? config.thresholds?.[key] ?? thresholds[key];\n }\n\n return {\n thresholds,\n profileThresholds: mergeProfileThresholds(defaultProfileThresholds, config.languageThresholds),\n maxFindings: cli.maxFindings ?? config.maxFindings ?? defaultMaxFindings,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnRisk: cli.failOnRisk ?? config.failOnRisk ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n tsconfig: cli.tsconfig ?? config.tsconfig,\n };\n}\n\n/** Merges user-supplied per-profile overrides on top of the built-in ones, per profile. */\nfunction mergeProfileThresholds(\n defaults: Partial<Record<ProfileKey, Partial<Thresholds>>>,\n overrides: Partial<Record<ProfileKey, Partial<Thresholds>>> | undefined\n): Partial<Record<ProfileKey, Partial<Thresholds>>> {\n const merged: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const key of profileKeys) {\n const combined = { ...defaults[key], ...overrides?.[key] };\n if (Object.keys(combined).length > 0) {\n merged[key] = combined;\n }\n }\n return merged;\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const config: CodeGaugeConfig = {};\n\n if (raw.thresholds !== undefined) {\n config.thresholds = validateThresholdObject(raw.thresholds, 'thresholds', configFile);\n }\n\n if (raw.languageThresholds !== undefined) {\n if (\n typeof raw.languageThresholds !== 'object' ||\n raw.languageThresholds === null ||\n Array.isArray(raw.languageThresholds)\n ) {\n throw new Error(`Config file \"${configFile}\": \"languageThresholds\" must be an object.`);\n }\n const languageThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const [profile, thresholds] of Object.entries(raw.languageThresholds as Record<string, unknown>)) {\n if (!(profileKeys as readonly string[]).includes(profile)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language profile \"${profile}\" (expected one of ${profileKeys.join(', ')}).`\n );\n }\n languageThresholds[profile as ProfileKey] = validateThresholdObject(\n thresholds,\n `languageThresholds.${profile}`,\n configFile\n );\n }\n config.languageThresholds = languageThresholds;\n }\n\n if (raw.maxFindings !== undefined) {\n config.maxFindings = requirePositiveInteger(raw.maxFindings, 'maxFindings', configFile);\n }\n for (const key of ['includeTests', 'failOnRisk', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n if (raw.tsconfig !== undefined) {\n if (typeof raw.tsconfig !== 'string') {\n throw new TypeError(`Config file \"${configFile}\": \"tsconfig\" must be a string.`);\n }\n config.tsconfig = raw.tsconfig;\n }\n\n return config;\n}\n\nfunction validateThresholdObject(value: unknown, label: string, configFile: string): Partial<Thresholds> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${label}\" must be an object.`);\n }\n const thresholds: Partial<Thresholds> = {};\n for (const [key, threshold] of Object.entries(value as Record<string, unknown>)) {\n if (!(key in defaultThresholds)) {\n throw new Error(`Config file \"${configFile}\": unknown threshold \"${key}\" in \"${label}\".`);\n }\n thresholds[key as keyof Thresholds] = requirePositiveInteger(threshold, `${label}.${key}`, configFile);\n }\n return thresholds;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["defaultThresholds","fileLoc","functionLoc","componentLoc","cognitive","cyclomatic","call","import","fanOut","parameter","duplicateBlock","transitiveDependency","structuralBreadth","structuralCoordination","stateMutation","duplicateSymbolGroup","defaultMaxFindings","configFileName","profileKeys","defaultProfileThresholds","python","react","resolveThresholds","options","language","isReact","thresholds","languageOverride","profileThresholds","thresholdCliKeys","resolveOptions","cli","config","key","Object","keys","mergeProfileThresholds","languageThresholds","maxFindings","includeTests","failOnRisk","failOnError","json","tsconfig","defaults","overrides","merged","combined","length","async","loadConfig","explicitPath","targetDirectory","configFile","currentDirectory","path","join","fileExists","parentDirectory","dirname","findNearestConfig","content","parsed","readFile","error","Error","formatError","JSON","parse","value","Array","isArray","raw","undefined","validateThresholdObject","profile","entries","includes","requirePositiveInteger","requireBoolean","TypeError","validateConfig","file","stat","isFile","label","threshold","Number","isSafeInteger","message","String"],"mappings":"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"}
1
+ {"version":3,"file":"cliConfig.js","names":[],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Risk thresholds; a finding is reported when the measured value is greater than or equal to the threshold. */\nexport interface Thresholds {\n fileLoc: number;\n functionLoc: number;\n componentLoc: number;\n cognitive: number;\n cyclomatic: number;\n call: number;\n import: number;\n fanOut: number;\n parameter: number;\n duplicateBlock: number;\n /** Percentage (1-100) of a file's code lines (comments/blanks excluded) covered by duplicates. */\n duplicationRatioPercent: number;\n transitiveDependency: number;\n structuralBreadth: number;\n structuralCoordination: number;\n stateMutation: number;\n duplicateSymbolGroup: number;\n}\n\n// Defaults tuned against blind human labels across five representative WillBooster/WillBoosterLab\n// repositories to maximize F1 (precision without sacrificing recall); see PR for the evaluation.\nexport const defaultThresholds: Thresholds = {\n fileLoc: 500,\n functionLoc: 120,\n componentLoc: 350,\n cognitive: 25,\n cyclomatic: 20,\n call: 50,\n import: 25,\n fanOut: 10,\n parameter: 8,\n duplicateBlock: 2,\n duplicationRatioPercent: 30,\n transitiveDependency: 25,\n structuralBreadth: 8,\n structuralCoordination: 300,\n stateMutation: 50,\n duplicateSymbolGroup: 5,\n};\n\nexport const defaultMaxFindings = 20;\nexport const configFileName = 'code-gauge.config.json';\n\n/**\n * Profile keys for per-language and React-specific threshold overrides. A file resolves its\n * thresholds as base → its language profile → the `react` profile (when it contains a component).\n */\nexport const profileKeys = [\n 'javascript',\n 'jsx',\n 'typescript',\n 'tsx',\n 'python',\n 'go',\n 'rust',\n 'java',\n 'ruby',\n 'c',\n 'cpp',\n 'react',\n] as const;\nexport type ProfileKey = (typeof profileKeys)[number];\n\n/**\n * Built-in per-profile overrides, calibrated because some metric distributions differ sharply by\n * language/type: Python treats every binding as an assignment (so `stateMutation` runs ~10x higher)\n * and coordinates more per file, while React files import roughly twice as many sources as pure TS.\n */\nexport const defaultProfileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {\n python: { stateMutation: 90, structuralCoordination: 350 },\n // Ruby scores state mutation via assignments like Python (bindings are assignments).\n ruby: { stateMutation: 90, structuralCoordination: 350 },\n react: { import: 30 },\n};\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n thresholds?: Partial<Thresholds>;\n /** Per-profile overrides keyed by language name or `react`; merged over `thresholds` for matching files. */\n languageThresholds?: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings?: number;\n largestFiles?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n tsconfig?: string;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n thresholds: Thresholds;\n profileThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>>;\n maxFindings: number;\n /** Number of largest files by code LOC to list; 0 disables the section. */\n largestFiles: number;\n includeTests: boolean;\n failOnRisk: boolean;\n failOnError: boolean;\n json: boolean;\n tsconfig?: string;\n}\n\n/**\n * Resolves the thresholds for a single file: the base thresholds overlaid with its language profile\n * and then, when the file contains a React component, the `react` profile.\n */\nexport function resolveThresholds(options: ResolvedOptions, language: string, isReact: boolean): Thresholds {\n let thresholds = options.thresholds;\n const languageOverride = options.profileThresholds[language as ProfileKey];\n if (languageOverride) {\n thresholds = { ...thresholds, ...languageOverride };\n }\n if (isReact && options.profileThresholds.react) {\n thresholds = { ...thresholds, ...options.profileThresholds.react };\n }\n return thresholds;\n}\n\n/** Raw command-line options; every threshold is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n fileLocThreshold?: number;\n functionLocThreshold?: number;\n componentLocThreshold?: number;\n cognitiveThreshold?: number;\n cyclomaticThreshold?: number;\n callThreshold?: number;\n importThreshold?: number;\n fanOutThreshold?: number;\n parameterThreshold?: number;\n duplicateBlockThreshold?: number;\n duplicationRatioPercentThreshold?: number;\n transitiveDependencyThreshold?: number;\n structuralBreadthThreshold?: number;\n structuralCoordinationThreshold?: number;\n stateMutationThreshold?: number;\n duplicateSymbolGroupThreshold?: number;\n maxFindings?: number;\n largestFiles?: number;\n includeTests?: boolean;\n failOnRisk?: boolean;\n failOnError?: boolean;\n json?: boolean;\n tsconfig?: string;\n}\n\n/** Maps each threshold to the matching command-line flag; the config key equals the flag without the `-threshold` suffix. */\nconst thresholdCliKeys: Record<keyof Thresholds, keyof CliOptions> = {\n fileLoc: 'fileLocThreshold',\n functionLoc: 'functionLocThreshold',\n componentLoc: 'componentLocThreshold',\n cognitive: 'cognitiveThreshold',\n cyclomatic: 'cyclomaticThreshold',\n call: 'callThreshold',\n import: 'importThreshold',\n fanOut: 'fanOutThreshold',\n parameter: 'parameterThreshold',\n duplicateBlock: 'duplicateBlockThreshold',\n duplicationRatioPercent: 'duplicationRatioPercentThreshold',\n transitiveDependency: 'transitiveDependencyThreshold',\n structuralBreadth: 'structuralBreadthThreshold',\n structuralCoordination: 'structuralCoordinationThreshold',\n stateMutation: 'stateMutationThreshold',\n duplicateSymbolGroup: 'duplicateSymbolGroupThreshold',\n};\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n const thresholds = { ...defaultThresholds };\n for (const key of Object.keys(thresholds) as (keyof Thresholds)[]) {\n thresholds[key] = (cli[thresholdCliKeys[key]] as number | undefined) ?? config.thresholds?.[key] ?? thresholds[key];\n }\n\n return {\n thresholds,\n profileThresholds: mergeProfileThresholds(defaultProfileThresholds, config.languageThresholds),\n maxFindings: cli.maxFindings ?? config.maxFindings ?? defaultMaxFindings,\n largestFiles: cli.largestFiles ?? config.largestFiles ?? 0,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnRisk: cli.failOnRisk ?? config.failOnRisk ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n tsconfig: cli.tsconfig ?? config.tsconfig,\n };\n}\n\n/** Merges user-supplied per-profile overrides on top of the built-in ones, per profile. */\nfunction mergeProfileThresholds(\n defaults: Partial<Record<ProfileKey, Partial<Thresholds>>>,\n overrides: Partial<Record<ProfileKey, Partial<Thresholds>>> | undefined\n): Partial<Record<ProfileKey, Partial<Thresholds>>> {\n const merged: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const key of profileKeys) {\n const combined = { ...defaults[key], ...overrides?.[key] };\n if (Object.keys(combined).length > 0) {\n merged[key] = combined;\n }\n }\n return merged;\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const config: CodeGaugeConfig = {};\n\n if (raw.thresholds !== undefined) {\n config.thresholds = validateThresholdObject(raw.thresholds, 'thresholds', configFile);\n }\n\n if (raw.languageThresholds !== undefined) {\n if (\n typeof raw.languageThresholds !== 'object' ||\n raw.languageThresholds === null ||\n Array.isArray(raw.languageThresholds)\n ) {\n throw new Error(`Config file \"${configFile}\": \"languageThresholds\" must be an object.`);\n }\n const languageThresholds: Partial<Record<ProfileKey, Partial<Thresholds>>> = {};\n for (const [profile, thresholds] of Object.entries(raw.languageThresholds as Record<string, unknown>)) {\n if (!(profileKeys as readonly string[]).includes(profile)) {\n throw new Error(\n `Config file \"${configFile}\": unknown language profile \"${profile}\" (expected one of ${profileKeys.join(', ')}).`\n );\n }\n languageThresholds[profile as ProfileKey] = validateThresholdObject(\n thresholds,\n `languageThresholds.${profile}`,\n configFile\n );\n }\n config.languageThresholds = languageThresholds;\n }\n\n if (raw.maxFindings !== undefined) {\n config.maxFindings = requirePositiveInteger(raw.maxFindings, 'maxFindings', configFile);\n }\n if (raw.largestFiles !== undefined) {\n config.largestFiles = requirePositiveInteger(raw.largestFiles, 'largestFiles', configFile);\n }\n for (const key of ['includeTests', 'failOnRisk', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n if (raw.tsconfig !== undefined) {\n if (typeof raw.tsconfig !== 'string') {\n throw new TypeError(`Config file \"${configFile}\": \"tsconfig\" must be a string.`);\n }\n config.tsconfig = raw.tsconfig;\n }\n\n return config;\n}\n\nfunction validateThresholdObject(value: unknown, label: string, configFile: string): Partial<Thresholds> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${label}\" must be an object.`);\n }\n const thresholds: Partial<Thresholds> = {};\n for (const [key, threshold] of Object.entries(value as Record<string, unknown>)) {\n if (!(key in defaultThresholds)) {\n throw new Error(`Config file \"${configFile}\": unknown threshold \"${key}\" in \"${label}\".`);\n }\n const parsed = requirePositiveInteger(threshold, `${label}.${key}`, configFile);\n if (key === 'duplicationRatioPercent' && parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"${label}.${key}\" must be between 1 and 100.`);\n }\n thresholds[key as keyof Thresholds] = parsed;\n }\n return thresholds;\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),t=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),n=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),r=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),i=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),a=new Set([`comment`,`line_comment`,`block_comment`]),o=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),s=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]);function c(e,t){let n=[],r=[],i=[];return l(e,n,r,i),w(x([...p(n,r),...m(n,i)]),t,n)}function l(n,r,i,o){function s(n){let c=r.length,l=n.childCount===0?void 0:u(n);if(n.childCount===0)d(n,r);else if(l!==void 0)r.push({kind:`text`,text:l,startRow:n.startPosition.row,endRow:n.endPosition.row});else if(!a.has(n.type)){let e=[],r=n.isNamed&&t.has(n.type);for(let t of n.children){let n=s(t);r&&t.isNamed&&!a.has(t.type)&&e.push(n)}r&&e.length>=2&&o.push(e)}let f={startTokenIndex:c,endTokenIndex:r.length,node:n};return n.isNamed&&e.has(n.type)&&i.push(f),f}s(n)}function u(e){let t=e.isNamed?i.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>o.has(e.type))?t:void 0}function d(e,t){if(a.has(e.type))return;let o=e.startPosition.row,s=e.endPosition.row;if(e.isNamed&&r.has(e.type)){t.push({kind:`text`,text:e.text,startRow:o,endRow:s},{kind:`text`,text:`:`,startRow:o,endRow:s},{kind:`id`,text:e.text,startRow:o,endRow:s});return}if(e.isNamed&&n.has(e.type)&&!f(e)){t.push({kind:`id`,text:e.text,startRow:o,endRow:s});return}let c=e.isNamed?i.get(e.type):void 0;t.push({kind:`text`,text:c??e.text,startRow:o,endRow:s})}function f(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=s.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function p(e,t){let n=[];for(let r of t)r.endTokenIndex-r.startTokenIndex<40||n.push(_(`b:${v(e,r.startTokenIndex,r.endTokenIndex)}`,r.startTokenIndex,r.endTokenIndex,r.node,r.node));return n}function m(e,t){let n=[],r=new Map,i=t.map(t=>g(e,t));for(let[e,t]of i.entries())for(let[n,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=r.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.minStart=Math.min(i.minStart,n),i.maxStart=Math.max(i.maxStart,n)):r.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let a=(e,t)=>{if(e===void 0)return!1;let n=r.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},o=e=>{let t=i[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},s=[];for(let[e,t]of i.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,c]of r.entries()){if(!a(c,i)||!o({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],l=t.windowKeysByStart[n-1]?.[i+1];a(r,i+1)||a(l,i+1)||s.push({containerIndex:e,start:n,length:i})}let c=new Set(s.map(h)),l=s;for(;l.length>0;){let r=[];for(let i of l){let a=t[i.containerIndex],o=a?.[i.start],s=a?.[i.start+i.length-1];if(!o||!s)continue;let c=`s:${v(e,o.startTokenIndex,s.endTokenIndex)}`;n.push(_(c,o.startTokenIndex,s.endTokenIndex,o.node,s.node)),r.push(i)}l=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=i[e.containerIndex]?.windowKeysByStart[t]?.[n.length];c.has(h(n))||!a(r,n.length)||!o(n)||(c.add(h(n)),l.push(n))}}return n}function h(e){return`${e.containerIndex}:${e.start}:${e.length}`}function g(e,t){let n=t.map(t=>y(v(e,t.startTokenIndex,t.endTokenIndex))),r=[];for(let e=0;e<t.length;e+=1){let i=[],a=5381,o=0,s=Math.min(t.length,e+100);for(let r=e;r<s;r+=1){let s=t[r],c=n[r];if(!s||c===void 0)break;a=b(a,c),o+=s.endTokenIndex-s.startTokenIndex;let l=r-e+1;i[l]=l>=2&&o>=40?b(a,l):void 0}r.push(i)}return{windowKeysByStart:r,statementHashes:n}}function _(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}function v(e,t,n){let r=new Map,i=[];for(let a=t;a<n;a+=1){let t=e[a];if(t)if(t.kind===`id`){let e=r.get(t.text);e===void 0&&(e=r.size,r.set(t.text,e)),i.push(`$${e}`)}else i.push(t.text)}return i.join(` `)}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e,t){return Math.imul(e,31)+t}function x(e){let t=new Map;for(let n of e){let e=t.get(n.fingerprint)??[];e.push(n),t.set(n.fingerprint,e)}let n=[...t.values()].map(S).filter(e=>e.length>=2),r=new Map(n.map(e=>[e[0]?.fingerprint??``,e.length])),i=e=>e.tokenCount*(r.get(e.fingerprint)??1),a=n.flat();a.sort((e,t)=>i(t)-i(e));for(let e=0;;e+=1){let t=[],n=new Map;for(let e of a){if(t.some(t=>C(t,e)))continue;t.push(e);let r=n.get(e.fingerprint)??[];r.push(e),n.set(e.fingerprint,r)}let r,i=-1;for(let[e,t]of n){let n=t[0]?.tokenCount??0;t.length<2&&n>i&&(r=e,i=n)}if(r===void 0)return n;if(e>=20){for(let[e,t]of n)t.length<2&&n.delete(e);return n}a=a.filter(e=>e.fingerprint!==r)}}function S(e){let t=new Map;for(let n of e){let e=`${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}function C(e,t){return e.startIndex<t.endIndex&&t.startIndex<e.endIndex}function w(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e.values()){r+=s.length-1;for(let e of s){i=Math.max(i,e.tokenCount);for(let r=e.startTokenIndex;r<e.endTokenIndex;r+=1){let e=n[r];for(let n=e?.startRow??0;n<=(e?.endRow??-1);n+=1)t.has(n+1)&&o.add(n+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.size,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}exports.measureDuplication=c;
2
+ //# sourceMappingURL=duplication.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplication.cjs","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import type Parser from 'tree-sitter';\nimport type { DuplicationMetrics } from './types.js';\n\n/**\n * Block-like nodes considered as whole-subtree duplicate candidates. Detection itself is\n * token-based, so this set only decides which subtrees are compared; Ruby's keyword-like node\n * types (`if`, `case`, ...) are safe here because only named nodes become candidates.\n */\nconst duplicateBlockTypes = new Set([\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'do_block',\n 'if_statement',\n 'for_statement',\n 'for_in_statement',\n 'enhanced_for_statement',\n 'for_range_loop',\n 'while_statement',\n 'do_statement',\n 'try_statement',\n 'try_with_resources_statement',\n 'with_statement',\n 'switch_statement',\n 'switch_expression',\n 'switch_case',\n 'switch_block_statement_group',\n 'switch_rule',\n 'case_clause',\n 'case_statement',\n 'match_statement',\n 'match_arm',\n 'except_clause',\n 'catch_clause',\n 'finally_clause',\n 'elif_clause',\n 'ensure',\n 'expression_statement',\n 'return_statement',\n 'return_expression',\n 'if_expression',\n 'for_expression',\n 'while_expression',\n 'loop_expression',\n 'match_expression',\n 'jsx_element',\n 'jsx_self_closing_element',\n // Ruby\n 'if',\n 'unless',\n 'case',\n 'case_match',\n 'while',\n 'until',\n 'for',\n 'begin',\n 'when',\n]);\n\n/** Nodes whose direct named children form statement sequences scanned for copy-pasted runs. */\nconst statementContainerTypes = new Set([\n 'program',\n 'source_file',\n 'translation_unit',\n 'module',\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'class_body',\n 'block_body',\n 'do_block',\n // Ruby loop bodies are a named `do` node, and `ensure` holds statements directly.\n 'do',\n 'ensure',\n 'then',\n 'else',\n // Case-like nodes hold their statements directly, without an inner block.\n 'case_statement',\n 'switch_block_statement_group',\n 'switch_rule',\n 'expression_case',\n 'type_case',\n 'communication_case',\n 'default_case',\n]);\n\n/**\n * Identifier leaves anonymized by occurrence order so consistently renamed copies still match.\n * Member/type names (`property_identifier`, `field_identifier`, `type_identifier`, ...) are kept\n * verbatim instead: calling a different API is a semantic difference, not a rename.\n */\nconst anonymizedIdentifierTypes = new Set([\n 'identifier',\n 'constant',\n 'instance_variable',\n 'class_variable',\n 'global_variable',\n]);\n\n/**\n * JS shorthand properties (`{ alpha }`) both emit the property name (semantic output shape) and\n * reference the binding, so they tokenize as the desugared `name: binding` — one verbatim text\n * token plus one anonymized id token — matching how the explicit form is tokenized.\n */\nconst shorthandPropertyTypes = new Set(['shorthand_property_identifier', 'shorthand_property_identifier_pattern']);\n\n/** Literal leaves normalized to a kind tag so copies differing only in literal values still match. */\nconst literalKindByType = new Map([\n ['number', '#num'],\n ['number_literal', '#num'],\n ['integer', '#num'],\n ['float', '#num'],\n ['integer_literal', '#num'],\n ['float_literal', '#num'],\n ['int_literal', '#num'],\n ['rune_literal', '#char'],\n ['imaginary_literal', '#num'],\n ['decimal_integer_literal', '#num'],\n ['hex_integer_literal', '#num'],\n ['octal_integer_literal', '#num'],\n ['binary_integer_literal', '#num'],\n ['decimal_floating_point_literal', '#num'],\n ['hex_floating_point_literal', '#num'],\n ['string_fragment', '#str'],\n ['multiline_string_fragment', '#str'],\n ['string_content', '#str'],\n ['raw_string_content', '#str'],\n ['heredoc_content', '#str'],\n // Heredoc marker names (`<<~SQL` vs `<<~QUERY`) have no string-value significance.\n ['heredoc_beginning', '#heredoc'],\n ['heredoc_end', '#heredoc'],\n // Strings are leaves in some grammars (Go/Rust) and fragment containers in others.\n ['string', '#str'],\n ['template_string', '#str'],\n ['string_literal', '#str'],\n ['interpreted_string_literal', '#str'],\n ['raw_string_literal', '#str'],\n ['raw_string', '#str'],\n ['escape_sequence', '#str'],\n ['char_literal', '#char'],\n ['character_literal', '#char'],\n ['character', '#char'],\n ['regex_pattern', '#regex'],\n]);\n\nconst commentTypes = new Set(['comment', 'line_comment', 'block_comment']);\n\n/** Children of a string node that carry only literal content; anything else is interpolation. */\nconst stringFragmentTypes = new Set([\n 'string_fragment',\n 'multiline_string_fragment',\n 'string_content',\n 'raw_string_content',\n 'escape_sequence',\n 'heredoc_content',\n // Python string delimiters are named children; they never carry interpolation.\n 'string_start',\n 'string_end',\n]);\n\n/**\n * Where a grammar names callees/members with a plain `identifier` (Java `method_invocation.name`,\n * Ruby `call.method`, Python `attribute.attribute`, plain calls elsewhere), the leaf in that field\n * must stay verbatim like `property_identifier` does: calling a different API is a semantic\n * difference, not a rename.\n */\nconst semanticNameFieldByParentType = new Map([\n ['call_expression', 'function'],\n ['method_invocation', 'name'],\n ['call', 'method'],\n ['attribute', 'attribute'],\n ['macro_invocation', 'macro'],\n // Java names accessed fields with a plain identifier in the `field` field.\n ['field_access', 'field'],\n // JS/TS `new Foo(...)` names the constructed API in the `constructor` field.\n ['new_expression', 'constructor'],\n // Python `f(timeout=...)` and Java `@Anno(key=...)` name parameters of the callee's API.\n ['keyword_argument', 'name'],\n ['element_value_pair', 'key'],\n // Rust turbofish and C++ template callees (`compute::<u32>(...)`); type arguments stay\n // anonymized via their own node types.\n ['generic_function', 'function'],\n ['template_function', 'name'],\n]);\n\n/** Minimum normalized token count for a region to be considered for duplication, to skip trivial repeats. */\nconst minDuplicateTokenCount = 40;\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n/** Caps how often the maximal-region selection reruns after shedding failed duplicate groups. */\nconst maxSelectionRerunCount = 20;\n\ninterface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\ninterface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n node: Parser.SyntaxNode;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * Detects copy-pasted regions within a file. Regions are compared by their normalized token\n * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches\n * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all\n * keywords/operators are kept verbatim. Candidates are whole block-like subtrees plus runs of\n * consecutive sibling statements, so a copy pasted into the middle of a longer block is still\n * found. Only maximal, non-overlapping regions are counted.\n */\nexport function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>): DuplicationMetrics {\n const tokens: Token[] = [];\n const blockRanges: TokenRange[] = [];\n const containerStatementRanges: TokenRange[][] = [];\n collectTokens(root, tokens, blockRanges, containerStatementRanges);\n\n const candidates = [\n ...collectBlockCandidates(tokens, blockRanges),\n ...collectSequenceCandidates(tokens, containerStatementRanges),\n ];\n const counted = selectMaximalDuplicates(candidates);\n return summarizeDuplicates(counted, codeLineNumbers, tokens);\n}\n\nfunction collectTokens(\n root: Parser.SyntaxNode,\n tokens: Token[],\n blockRanges: TokenRange[],\n containerStatementRanges: TokenRange[][]\n): void {\n function visit(node: Parser.SyntaxNode): TokenRange {\n const startTokenIndex = tokens.length;\n const atomicKind = node.childCount === 0 ? undefined : atomicLiteralKind(node);\n if (node.childCount === 0) {\n appendLeafToken(node, tokens);\n } else if (atomicKind !== undefined) {\n // Interpolation-free strings collapse to their kind tag so copies differing only in quote\n // style or content still match; delimiter tokens would otherwise break the equivalence.\n tokens.push({ kind: 'text', text: atomicKind, startRow: node.startPosition.row, endRow: node.endPosition.row });\n } else if (!commentTypes.has(node.type)) {\n const statementRanges: TokenRange[] = [];\n const isContainer = node.isNamed && statementContainerTypes.has(node.type);\n for (const child of node.children) {\n const childRange = visit(child);\n if (isContainer && child.isNamed && !commentTypes.has(child.type)) {\n statementRanges.push(childRange);\n }\n }\n if (isContainer && statementRanges.length >= minSequenceStatementCount) {\n containerStatementRanges.push(statementRanges);\n }\n }\n\n const range = { startTokenIndex, endTokenIndex: tokens.length, node };\n if (node.isNamed && duplicateBlockTypes.has(node.type)) {\n blockRanges.push(range);\n }\n return range;\n }\n\n visit(root);\n}\n\n/** The kind tag of a string-like node with no interpolation, or undefined to descend normally. */\nfunction atomicLiteralKind(node: Parser.SyntaxNode): string | undefined {\n const kind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n if (kind === undefined) {\n return undefined;\n }\n return node.namedChildren.every((child) => stringFragmentTypes.has(child.type)) ? kind : undefined;\n}\n\nfunction appendLeafToken(node: Parser.SyntaxNode, tokens: Token[]): void {\n if (commentTypes.has(node.type)) {\n return;\n }\n\n const startRow = node.startPosition.row;\n const endRow = node.endPosition.row;\n if (node.isNamed && shorthandPropertyTypes.has(node.type)) {\n tokens.push(\n { kind: 'text', text: node.text, startRow, endRow },\n { kind: 'text', text: ':', startRow, endRow },\n { kind: 'id', text: node.text, startRow, endRow }\n );\n return;\n }\n\n if (node.isNamed && anonymizedIdentifierTypes.has(node.type) && !isSemanticNameLeaf(node)) {\n tokens.push({ kind: 'id', text: node.text, startRow, endRow });\n return;\n }\n\n // Anything else keeps its text: keywords, operators, punctuation, and semantic names such as\n // `property_identifier`/`type_identifier`, which must distinguish otherwise-identical structures.\n const literalKind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n tokens.push({ kind: 'text', text: literalKind ?? node.text, startRow, endRow });\n}\n\nfunction isSemanticNameLeaf(node: Parser.SyntaxNode): boolean {\n const parent = node.parent;\n if (!parent) {\n return false;\n }\n\n // Java method references (`Foo::bar`) name their identifiers without grammar fields; both the\n // type/object and the referenced method are semantic.\n if (parent.type === 'method_reference') {\n return true;\n }\n\n // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.\n if (parent.type === 'call' && parent.childForFieldName('function')?.id === node.id) {\n return true;\n }\n\n // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API; constants used as\n // plain values stay anonymized so renamed clones referencing constants still match.\n if (node.type === 'constant' && parent.type === 'call' && parent.childForFieldName('receiver')?.id === node.id) {\n return true;\n }\n\n // Java static receivers (`Alpha.run(...)`) name the invoked type. The tokenizer has no symbol\n // table, so PascalCase — Java's universal type-naming convention — is the discriminator;\n // camelCase instance receivers stay anonymized for rename tolerance.\n if (\n parent.type === 'method_invocation' &&\n parent.childForFieldName('object')?.id === node.id &&\n /^\\p{Lu}/u.test(node.text)\n ) {\n return true;\n }\n\n // Qualified callees (Rust `crate::alpha::make(...)`, C++ `detail::make(...)`) and generic\n // callees (`compute::<u32>(...)`, `compute<int>(...)`) wrap their identifiers arbitrarily deep;\n // every path/name segment is semantic there — but only in call position, so renamed clones that\n // merely reference scoped constants or `use` paths still match.\n if (\n (parent.type === 'scoped_identifier' || parent.type === 'qualified_identifier') &&\n (parent.childForFieldName('name')?.id === node.id || parent.childForFieldName('path')?.id === node.id)\n ) {\n let outer = parent;\n while (\n outer.parent &&\n (outer.parent.type === 'scoped_identifier' ||\n outer.parent.type === 'qualified_identifier' ||\n outer.parent.type === 'generic_function' ||\n outer.parent.type === 'template_function')\n ) {\n outer = outer.parent;\n }\n if (outer.parent?.type === 'call_expression' && outer.parent.childForFieldName('function')?.id === outer.id) {\n return true;\n }\n }\n\n // Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar: the key\n // is the keyed_element's first named child, a literal_element wrapping the identifier.\n if (\n parent.type === 'literal_element' &&\n parent.parent?.type === 'keyed_element' &&\n parent.parent.namedChild(0)?.id === parent.id\n ) {\n return true;\n }\n\n const field = semanticNameFieldByParentType.get(parent.type);\n return field !== undefined && parent.childForFieldName(field)?.id === node.id;\n}\n\nfunction collectBlockCandidates(tokens: Token[], blockRanges: TokenRange[]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n for (const range of blockRanges) {\n const tokenCount = range.endTokenIndex - range.startTokenIndex;\n if (tokenCount < minDuplicateTokenCount) {\n continue;\n }\n candidates.push(\n toCandidate(\n `b:${fingerprintTokens(tokens, range.startTokenIndex, range.endTokenIndex)}`,\n range.startTokenIndex,\n range.endTokenIndex,\n range.node,\n range.node\n )\n );\n }\n return candidates;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements. Every container statement participates; only\n * the window length is capped, so enumeration stays linear in the statement count. Windows are\n * grouped by a cheap rolling hash of per-statement fingerprints, and only locally maximal repeated\n * windows — those whose one-statement extensions stop repeating — become candidates with an exact\n * (window-consistent) fingerprint. Without the maximality filter a degenerate file of\n * near-identical statements would fingerprint every sub-window of every repeated region.\n */\nfunction collectSequenceCandidates(tokens: Token[], containers: TokenRange[][]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements) => enumerateContainerWindows(tokens, statements));\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, { count: 1, containerIndex, minStart: start, maxStart: start });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n return (\n occurrences !== undefined &&\n occurrences.count >= 2 &&\n (occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length)\n );\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n if (!first || !last) {\n continue;\n }\n const fingerprint = `s:${fingerprintTokens(tokens, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push(toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first.node, last.node));\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[]): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n hashText(fingerprintTokens(tokens, statement.startTokenIndex, statement.endTokenIndex))\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minDuplicateTokenCount\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n firstNode: Parser.SyntaxNode,\n lastNode: Parser.SyntaxNode\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: firstNode.startIndex,\n endIndex: lastNode.endIndex,\n startLine: firstNode.startPosition.row + 1,\n endLine: lastNode.endPosition.row + 1,\n };\n}\n\n/** Serializes a token range with identifiers anonymized consistently by first-occurrence order. */\nfunction fingerprintTokens(tokens: Token[], startTokenIndex: number, endTokenIndex: number): string {\n const indexByIdentifier = new Map<string, number>();\n const parts: string[] = [];\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n parts.push(`$${identifierIndex}`);\n } else {\n parts.push(token.text);\n }\n }\n return parts.join(' ');\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Keeps only maximal, non-overlapping duplicates: candidates are grouped by fingerprint, larger\n * regions win over regions overlapping them, and groups reduced below two survivors are dropped.\n */\nfunction selectMaximalDuplicates(candidates: DuplicateCandidate[]): Map<string, DuplicateCandidate[]> {\n const byFingerprint = new Map<string, DuplicateCandidate[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter((group) => group.length >= 2);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: DuplicateCandidate): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left));\n\n // Greedy selection can keep a candidate whose group ends up below two survivors; such an\n // uncounted region must not block smaller groups, so the largest failed group is removed and the\n // selection reruns. One group at a time: freeing a failed group's regions can rescue another.\n // The rerun cap bounds degenerate inputs; past it the remaining failed groups are dropped,\n // trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegions: { startIndex: number; endIndex: number }[] = [];\n const counted = new Map<string, DuplicateCandidate[]>();\n for (const candidate of duplicates) {\n if (keptRegions.some((region) => overlaps(region, candidate))) {\n continue;\n }\n keptRegions.push(candidate);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n\n let failedFingerprint: string | undefined;\n let failedTokenCount = -1;\n for (const [fingerprint, group] of counted) {\n const tokenCount = group[0]?.tokenCount ?? 0;\n if (group.length < 2 && tokenCount > failedTokenCount) {\n failedFingerprint = fingerprint;\n failedTokenCount = tokenCount;\n }\n }\n // No failed fingerprint means every counted group kept at least two survivors.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= maxSelectionRerunCount) {\n for (const [fingerprint, group] of counted) {\n if (group.length < 2) {\n counted.delete(fingerprint);\n }\n }\n return counted;\n }\n\n duplicates = duplicates.filter((candidate) => candidate.fingerprint !== failedFingerprint);\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nfunction dedupeByRegion(group: DuplicateCandidate[]): DuplicateCandidate[] {\n const byRegion = new Map<string, DuplicateCandidate>();\n for (const candidate of group) {\n const key = `${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n\nfunction overlaps(\n left: { startIndex: number; endIndex: number },\n right: { startIndex: number; endIndex: number }\n): boolean {\n return left.startIndex < right.endIndex && right.startIndex < left.endIndex;\n}\n\nfunction summarizeDuplicates(\n counted: Map<string, DuplicateCandidate[]>,\n codeLineNumbers: Set<number>,\n tokens: Token[]\n): DuplicationMetrics {\n let duplicateBlockCount = 0;\n let maxDuplicateBlockSize = 0;\n const duplicateBlockGroups: { startLine: number; endLine: number }[][] = [];\n const duplicatedLines = new Set<number>();\n for (const group of counted.values()) {\n duplicateBlockCount += group.length - 1;\n for (const candidate of group) {\n maxDuplicateBlockSize = Math.max(maxDuplicateBlockSize, candidate.tokenCount);\n // Only CODE lines carrying matched tokens count: comments and blank gaps inside a\n // candidate's bounding range — and blank rows inside a multi-row token (heredocs, template\n // literals) — are not duplicated content and would push the ratio past 1.\n for (let index = candidate.startTokenIndex; index < candidate.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n }\n duplicateBlockGroups.push(\n group\n .map(({ startLine, endLine }) => ({ startLine, endLine }))\n .toSorted((left, right) => left.startLine - right.startLine)\n );\n }\n duplicateBlockGroups.sort((left, right) => (left[0]?.startLine ?? 0) - (right[0]?.startLine ?? 0));\n\n return {\n duplicateBlockCount,\n duplicateBlockGroupCount: counted.size,\n duplicateBlockGroups,\n duplicateLineCount: duplicatedLines.size,\n duplicationRatio: codeLineNumbers.size === 0 ? 0 : duplicatedLines.size / codeLineNumbers.size,\n maxDuplicateBlockSize,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type Parser from 'tree-sitter';
2
+ import type { DuplicationMetrics } from './types.js';
3
+ /**
4
+ * Detects copy-pasted regions within a file. Regions are compared by their normalized token
5
+ * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches
6
+ * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all
7
+ * keywords/operators are kept verbatim. Candidates are whole block-like subtrees plus runs of
8
+ * consecutive sibling statements, so a copy pasted into the middle of a longer block is still
9
+ * found. Only maximal, non-overlapping regions are counted.
10
+ */
11
+ export declare function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>): DuplicationMetrics;
@@ -0,0 +1,2 @@
1
+ const e=new Set(`statement_block.block.compound_statement.body_statement.constructor_body.do_block.if_statement.for_statement.for_in_statement.enhanced_for_statement.for_range_loop.while_statement.do_statement.try_statement.try_with_resources_statement.with_statement.switch_statement.switch_expression.switch_case.switch_block_statement_group.switch_rule.case_clause.case_statement.match_statement.match_arm.except_clause.catch_clause.finally_clause.elif_clause.ensure.expression_statement.return_statement.return_expression.if_expression.for_expression.while_expression.loop_expression.match_expression.jsx_element.jsx_self_closing_element.if.unless.case.case_match.while.until.for.begin.when`.split(`.`)),t=new Set([`program`,`source_file`,`translation_unit`,`module`,`statement_block`,`block`,`compound_statement`,`body_statement`,`constructor_body`,`class_body`,`block_body`,`do_block`,`do`,`ensure`,`then`,`else`,`case_statement`,`switch_block_statement_group`,`switch_rule`,`expression_case`,`type_case`,`communication_case`,`default_case`]),n=new Set([`identifier`,`constant`,`instance_variable`,`class_variable`,`global_variable`]),r=new Set([`shorthand_property_identifier`,`shorthand_property_identifier_pattern`]),i=new Map([[`number`,`#num`],[`number_literal`,`#num`],[`integer`,`#num`],[`float`,`#num`],[`integer_literal`,`#num`],[`float_literal`,`#num`],[`int_literal`,`#num`],[`rune_literal`,`#char`],[`imaginary_literal`,`#num`],[`decimal_integer_literal`,`#num`],[`hex_integer_literal`,`#num`],[`octal_integer_literal`,`#num`],[`binary_integer_literal`,`#num`],[`decimal_floating_point_literal`,`#num`],[`hex_floating_point_literal`,`#num`],[`string_fragment`,`#str`],[`multiline_string_fragment`,`#str`],[`string_content`,`#str`],[`raw_string_content`,`#str`],[`heredoc_content`,`#str`],[`heredoc_beginning`,`#heredoc`],[`heredoc_end`,`#heredoc`],[`string`,`#str`],[`template_string`,`#str`],[`string_literal`,`#str`],[`interpreted_string_literal`,`#str`],[`raw_string_literal`,`#str`],[`raw_string`,`#str`],[`escape_sequence`,`#str`],[`char_literal`,`#char`],[`character_literal`,`#char`],[`character`,`#char`],[`regex_pattern`,`#regex`]]),a=new Set([`comment`,`line_comment`,`block_comment`]),o=new Set([`string_fragment`,`multiline_string_fragment`,`string_content`,`raw_string_content`,`escape_sequence`,`heredoc_content`,`string_start`,`string_end`]),s=new Map([[`call_expression`,`function`],[`method_invocation`,`name`],[`call`,`method`],[`attribute`,`attribute`],[`macro_invocation`,`macro`],[`field_access`,`field`],[`new_expression`,`constructor`],[`keyword_argument`,`name`],[`element_value_pair`,`key`],[`generic_function`,`function`],[`template_function`,`name`]]);function c(e,t){let n=[],r=[],i=[];return l(e,n,r,i),w(x([...p(n,r),...m(n,i)]),t,n)}function l(n,r,i,o){function s(n){let c=r.length,l=n.childCount===0?void 0:u(n);if(n.childCount===0)d(n,r);else if(l!==void 0)r.push({kind:`text`,text:l,startRow:n.startPosition.row,endRow:n.endPosition.row});else if(!a.has(n.type)){let e=[],r=n.isNamed&&t.has(n.type);for(let t of n.children){let n=s(t);r&&t.isNamed&&!a.has(t.type)&&e.push(n)}r&&e.length>=2&&o.push(e)}let f={startTokenIndex:c,endTokenIndex:r.length,node:n};return n.isNamed&&e.has(n.type)&&i.push(f),f}s(n)}function u(e){let t=e.isNamed?i.get(e.type):void 0;if(t!==void 0)return e.namedChildren.every(e=>o.has(e.type))?t:void 0}function d(e,t){if(a.has(e.type))return;let o=e.startPosition.row,s=e.endPosition.row;if(e.isNamed&&r.has(e.type)){t.push({kind:`text`,text:e.text,startRow:o,endRow:s},{kind:`text`,text:`:`,startRow:o,endRow:s},{kind:`id`,text:e.text,startRow:o,endRow:s});return}if(e.isNamed&&n.has(e.type)&&!f(e)){t.push({kind:`id`,text:e.text,startRow:o,endRow:s});return}let c=e.isNamed?i.get(e.type):void 0;t.push({kind:`text`,text:c??e.text,startRow:o,endRow:s})}function f(e){let t=e.parent;if(!t)return!1;if(t.type===`method_reference`||t.type===`call`&&t.childForFieldName(`function`)?.id===e.id||e.type===`constant`&&t.type===`call`&&t.childForFieldName(`receiver`)?.id===e.id||t.type===`method_invocation`&&t.childForFieldName(`object`)?.id===e.id&&/^\p{Lu}/u.test(e.text))return!0;if((t.type===`scoped_identifier`||t.type===`qualified_identifier`)&&(t.childForFieldName(`name`)?.id===e.id||t.childForFieldName(`path`)?.id===e.id)){let e=t;for(;e.parent&&(e.parent.type===`scoped_identifier`||e.parent.type===`qualified_identifier`||e.parent.type===`generic_function`||e.parent.type===`template_function`);)e=e.parent;if(e.parent?.type===`call_expression`&&e.parent.childForFieldName(`function`)?.id===e.id)return!0}if(t.type===`literal_element`&&t.parent?.type===`keyed_element`&&t.parent.namedChild(0)?.id===t.id)return!0;let n=s.get(t.type);return n!==void 0&&t.childForFieldName(n)?.id===e.id}function p(e,t){let n=[];for(let r of t)r.endTokenIndex-r.startTokenIndex<40||n.push(_(`b:${v(e,r.startTokenIndex,r.endTokenIndex)}`,r.startTokenIndex,r.endTokenIndex,r.node,r.node));return n}function m(e,t){let n=[],r=new Map,i=t.map(t=>g(e,t));for(let[e,t]of i.entries())for(let[n,i]of t.windowKeysByStart.entries())for(let t of i){if(t===void 0)continue;let i=r.get(t);i?(i.count+=1,i.containerIndex!==e&&(i.containerIndex=-1),i.minStart=Math.min(i.minStart,n),i.maxStart=Math.max(i.maxStart,n)):r.set(t,{count:1,containerIndex:e,minStart:n,maxStart:n})}let a=(e,t)=>{if(e===void 0)return!1;let n=r.get(e);return n!==void 0&&n.count>=2&&(n.containerIndex===-1||n.maxStart-n.minStart>=t)},o=e=>{let t=i[e.containerIndex]?.statementHashes??[],n=t[e.start];for(let r=e.start+1;r<e.start+e.length;r+=1)if(t[r]!==n)return!0;return!1},s=[];for(let[e,t]of i.entries())for(let[n,r]of t.windowKeysByStart.entries())for(let[i,c]of r.entries()){if(!a(c,i)||!o({containerIndex:e,start:n,length:i}))continue;let r=t.windowKeysByStart[n]?.[i+1],l=t.windowKeysByStart[n-1]?.[i+1];a(r,i+1)||a(l,i+1)||s.push({containerIndex:e,start:n,length:i})}let c=new Set(s.map(h)),l=s;for(;l.length>0;){let r=[];for(let i of l){let a=t[i.containerIndex],o=a?.[i.start],s=a?.[i.start+i.length-1];if(!o||!s)continue;let c=`s:${v(e,o.startTokenIndex,s.endTokenIndex)}`;n.push(_(c,o.startTokenIndex,s.endTokenIndex,o.node,s.node)),r.push(i)}l=[];for(let e of r)for(let t of[e.start,e.start+1]){let n={containerIndex:e.containerIndex,start:t,length:e.length-1},r=i[e.containerIndex]?.windowKeysByStart[t]?.[n.length];c.has(h(n))||!a(r,n.length)||!o(n)||(c.add(h(n)),l.push(n))}}return n}function h(e){return`${e.containerIndex}:${e.start}:${e.length}`}function g(e,t){let n=t.map(t=>y(v(e,t.startTokenIndex,t.endTokenIndex))),r=[];for(let e=0;e<t.length;e+=1){let i=[],a=5381,o=0,s=Math.min(t.length,e+100);for(let r=e;r<s;r+=1){let s=t[r],c=n[r];if(!s||c===void 0)break;a=b(a,c),o+=s.endTokenIndex-s.startTokenIndex;let l=r-e+1;i[l]=l>=2&&o>=40?b(a,l):void 0}r.push(i)}return{windowKeysByStart:r,statementHashes:n}}function _(e,t,n,r,i){return{fingerprint:e,tokenCount:n-t,startTokenIndex:t,endTokenIndex:n,startIndex:r.startIndex,endIndex:i.endIndex,startLine:r.startPosition.row+1,endLine:i.endPosition.row+1}}function v(e,t,n){let r=new Map,i=[];for(let a=t;a<n;a+=1){let t=e[a];if(t)if(t.kind===`id`){let e=r.get(t.text);e===void 0&&(e=r.size,r.set(t.text,e)),i.push(`$${e}`)}else i.push(t.text)}return i.join(` `)}function y(e){let t=5381;for(let n=0;n<e.length;n+=1)t=Math.imul(t,33)^e.charCodeAt(n);return t}function b(e,t){return Math.imul(e,31)+t}function x(e){let t=new Map;for(let n of e){let e=t.get(n.fingerprint)??[];e.push(n),t.set(n.fingerprint,e)}let n=[...t.values()].map(S).filter(e=>e.length>=2),r=new Map(n.map(e=>[e[0]?.fingerprint??``,e.length])),i=e=>e.tokenCount*(r.get(e.fingerprint)??1),a=n.flat();a.sort((e,t)=>i(t)-i(e));for(let e=0;;e+=1){let t=[],n=new Map;for(let e of a){if(t.some(t=>C(t,e)))continue;t.push(e);let r=n.get(e.fingerprint)??[];r.push(e),n.set(e.fingerprint,r)}let r,i=-1;for(let[e,t]of n){let n=t[0]?.tokenCount??0;t.length<2&&n>i&&(r=e,i=n)}if(r===void 0)return n;if(e>=20){for(let[e,t]of n)t.length<2&&n.delete(e);return n}a=a.filter(e=>e.fingerprint!==r)}}function S(e){let t=new Map;for(let n of e){let e=`${n.startIndex}:${n.endIndex}`,r=t.get(e);(!r||n.tokenCount>r.tokenCount)&&t.set(e,n)}return[...t.values()]}function C(e,t){return e.startIndex<t.endIndex&&t.startIndex<e.endIndex}function w(e,t,n){let r=0,i=0,a=[],o=new Set;for(let s of e.values()){r+=s.length-1;for(let e of s){i=Math.max(i,e.tokenCount);for(let r=e.startTokenIndex;r<e.endTokenIndex;r+=1){let e=n[r];for(let n=e?.startRow??0;n<=(e?.endRow??-1);n+=1)t.has(n+1)&&o.add(n+1)}}a.push(s.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t})).toSorted((e,t)=>e.startLine-t.startLine))}return a.sort((e,t)=>(e[0]?.startLine??0)-(t[0]?.startLine??0)),{duplicateBlockCount:r,duplicateBlockGroupCount:e.size,duplicateBlockGroups:a,duplicateLineCount:o.size,duplicationRatio:t.size===0?0:o.size/t.size,maxDuplicateBlockSize:i}}export{c as measureDuplication};
2
+ //# sourceMappingURL=duplication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplication.js","names":[],"sources":["../src/duplication.ts"],"sourcesContent":["import type Parser from 'tree-sitter';\nimport type { DuplicationMetrics } from './types.js';\n\n/**\n * Block-like nodes considered as whole-subtree duplicate candidates. Detection itself is\n * token-based, so this set only decides which subtrees are compared; Ruby's keyword-like node\n * types (`if`, `case`, ...) are safe here because only named nodes become candidates.\n */\nconst duplicateBlockTypes = new Set([\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'do_block',\n 'if_statement',\n 'for_statement',\n 'for_in_statement',\n 'enhanced_for_statement',\n 'for_range_loop',\n 'while_statement',\n 'do_statement',\n 'try_statement',\n 'try_with_resources_statement',\n 'with_statement',\n 'switch_statement',\n 'switch_expression',\n 'switch_case',\n 'switch_block_statement_group',\n 'switch_rule',\n 'case_clause',\n 'case_statement',\n 'match_statement',\n 'match_arm',\n 'except_clause',\n 'catch_clause',\n 'finally_clause',\n 'elif_clause',\n 'ensure',\n 'expression_statement',\n 'return_statement',\n 'return_expression',\n 'if_expression',\n 'for_expression',\n 'while_expression',\n 'loop_expression',\n 'match_expression',\n 'jsx_element',\n 'jsx_self_closing_element',\n // Ruby\n 'if',\n 'unless',\n 'case',\n 'case_match',\n 'while',\n 'until',\n 'for',\n 'begin',\n 'when',\n]);\n\n/** Nodes whose direct named children form statement sequences scanned for copy-pasted runs. */\nconst statementContainerTypes = new Set([\n 'program',\n 'source_file',\n 'translation_unit',\n 'module',\n 'statement_block',\n 'block',\n 'compound_statement',\n 'body_statement',\n 'constructor_body',\n 'class_body',\n 'block_body',\n 'do_block',\n // Ruby loop bodies are a named `do` node, and `ensure` holds statements directly.\n 'do',\n 'ensure',\n 'then',\n 'else',\n // Case-like nodes hold their statements directly, without an inner block.\n 'case_statement',\n 'switch_block_statement_group',\n 'switch_rule',\n 'expression_case',\n 'type_case',\n 'communication_case',\n 'default_case',\n]);\n\n/**\n * Identifier leaves anonymized by occurrence order so consistently renamed copies still match.\n * Member/type names (`property_identifier`, `field_identifier`, `type_identifier`, ...) are kept\n * verbatim instead: calling a different API is a semantic difference, not a rename.\n */\nconst anonymizedIdentifierTypes = new Set([\n 'identifier',\n 'constant',\n 'instance_variable',\n 'class_variable',\n 'global_variable',\n]);\n\n/**\n * JS shorthand properties (`{ alpha }`) both emit the property name (semantic output shape) and\n * reference the binding, so they tokenize as the desugared `name: binding` — one verbatim text\n * token plus one anonymized id token — matching how the explicit form is tokenized.\n */\nconst shorthandPropertyTypes = new Set(['shorthand_property_identifier', 'shorthand_property_identifier_pattern']);\n\n/** Literal leaves normalized to a kind tag so copies differing only in literal values still match. */\nconst literalKindByType = new Map([\n ['number', '#num'],\n ['number_literal', '#num'],\n ['integer', '#num'],\n ['float', '#num'],\n ['integer_literal', '#num'],\n ['float_literal', '#num'],\n ['int_literal', '#num'],\n ['rune_literal', '#char'],\n ['imaginary_literal', '#num'],\n ['decimal_integer_literal', '#num'],\n ['hex_integer_literal', '#num'],\n ['octal_integer_literal', '#num'],\n ['binary_integer_literal', '#num'],\n ['decimal_floating_point_literal', '#num'],\n ['hex_floating_point_literal', '#num'],\n ['string_fragment', '#str'],\n ['multiline_string_fragment', '#str'],\n ['string_content', '#str'],\n ['raw_string_content', '#str'],\n ['heredoc_content', '#str'],\n // Heredoc marker names (`<<~SQL` vs `<<~QUERY`) have no string-value significance.\n ['heredoc_beginning', '#heredoc'],\n ['heredoc_end', '#heredoc'],\n // Strings are leaves in some grammars (Go/Rust) and fragment containers in others.\n ['string', '#str'],\n ['template_string', '#str'],\n ['string_literal', '#str'],\n ['interpreted_string_literal', '#str'],\n ['raw_string_literal', '#str'],\n ['raw_string', '#str'],\n ['escape_sequence', '#str'],\n ['char_literal', '#char'],\n ['character_literal', '#char'],\n ['character', '#char'],\n ['regex_pattern', '#regex'],\n]);\n\nconst commentTypes = new Set(['comment', 'line_comment', 'block_comment']);\n\n/** Children of a string node that carry only literal content; anything else is interpolation. */\nconst stringFragmentTypes = new Set([\n 'string_fragment',\n 'multiline_string_fragment',\n 'string_content',\n 'raw_string_content',\n 'escape_sequence',\n 'heredoc_content',\n // Python string delimiters are named children; they never carry interpolation.\n 'string_start',\n 'string_end',\n]);\n\n/**\n * Where a grammar names callees/members with a plain `identifier` (Java `method_invocation.name`,\n * Ruby `call.method`, Python `attribute.attribute`, plain calls elsewhere), the leaf in that field\n * must stay verbatim like `property_identifier` does: calling a different API is a semantic\n * difference, not a rename.\n */\nconst semanticNameFieldByParentType = new Map([\n ['call_expression', 'function'],\n ['method_invocation', 'name'],\n ['call', 'method'],\n ['attribute', 'attribute'],\n ['macro_invocation', 'macro'],\n // Java names accessed fields with a plain identifier in the `field` field.\n ['field_access', 'field'],\n // JS/TS `new Foo(...)` names the constructed API in the `constructor` field.\n ['new_expression', 'constructor'],\n // Python `f(timeout=...)` and Java `@Anno(key=...)` name parameters of the callee's API.\n ['keyword_argument', 'name'],\n ['element_value_pair', 'key'],\n // Rust turbofish and C++ template callees (`compute::<u32>(...)`); type arguments stay\n // anonymized via their own node types.\n ['generic_function', 'function'],\n ['template_function', 'name'],\n]);\n\n/** Minimum normalized token count for a region to be considered for duplication, to skip trivial repeats. */\nconst minDuplicateTokenCount = 40;\n/** Minimum consecutive statements for a statement-sequence duplicate candidate. */\nconst minSequenceStatementCount = 2;\n/**\n * Caps the window length so statement-sequence enumeration stays linear in the statement count.\n * Heterogeneous clones longer than the cap are reported as capped windows (a deliberate\n * conservative undercount trading completeness for bounded discovery cost).\n */\nconst maxSequenceStatementCount = 100;\n/** Caps how often the maximal-region selection reruns after shedding failed duplicate groups. */\nconst maxSelectionRerunCount = 20;\n\ninterface Token {\n /** Normalization target: identifiers to anonymize, literal kind tags, or the raw token text. */\n kind: 'id' | 'text';\n text: string;\n /** 0-based source rows the token occupies, so line coverage counts only matched-token lines. */\n startRow: number;\n endRow: number;\n}\n\ninterface TokenRange {\n startTokenIndex: number;\n endTokenIndex: number;\n node: Parser.SyntaxNode;\n}\n\ninterface DuplicateCandidate {\n fingerprint: string;\n tokenCount: number;\n startTokenIndex: number;\n endTokenIndex: number;\n startIndex: number;\n endIndex: number;\n startLine: number;\n endLine: number;\n}\n\n/**\n * Detects copy-pasted regions within a file. Regions are compared by their normalized token\n * sequence: identifiers are anonymized consistently by first-occurrence order (`a.f(a, b)` matches\n * `x.f(x, y)` but not `x.f(y, z)`), literals are normalized by kind, and member/type names and all\n * keywords/operators are kept verbatim. Candidates are whole block-like subtrees plus runs of\n * consecutive sibling statements, so a copy pasted into the middle of a longer block is still\n * found. Only maximal, non-overlapping regions are counted.\n */\nexport function measureDuplication(root: Parser.SyntaxNode, codeLineNumbers: Set<number>): DuplicationMetrics {\n const tokens: Token[] = [];\n const blockRanges: TokenRange[] = [];\n const containerStatementRanges: TokenRange[][] = [];\n collectTokens(root, tokens, blockRanges, containerStatementRanges);\n\n const candidates = [\n ...collectBlockCandidates(tokens, blockRanges),\n ...collectSequenceCandidates(tokens, containerStatementRanges),\n ];\n const counted = selectMaximalDuplicates(candidates);\n return summarizeDuplicates(counted, codeLineNumbers, tokens);\n}\n\nfunction collectTokens(\n root: Parser.SyntaxNode,\n tokens: Token[],\n blockRanges: TokenRange[],\n containerStatementRanges: TokenRange[][]\n): void {\n function visit(node: Parser.SyntaxNode): TokenRange {\n const startTokenIndex = tokens.length;\n const atomicKind = node.childCount === 0 ? undefined : atomicLiteralKind(node);\n if (node.childCount === 0) {\n appendLeafToken(node, tokens);\n } else if (atomicKind !== undefined) {\n // Interpolation-free strings collapse to their kind tag so copies differing only in quote\n // style or content still match; delimiter tokens would otherwise break the equivalence.\n tokens.push({ kind: 'text', text: atomicKind, startRow: node.startPosition.row, endRow: node.endPosition.row });\n } else if (!commentTypes.has(node.type)) {\n const statementRanges: TokenRange[] = [];\n const isContainer = node.isNamed && statementContainerTypes.has(node.type);\n for (const child of node.children) {\n const childRange = visit(child);\n if (isContainer && child.isNamed && !commentTypes.has(child.type)) {\n statementRanges.push(childRange);\n }\n }\n if (isContainer && statementRanges.length >= minSequenceStatementCount) {\n containerStatementRanges.push(statementRanges);\n }\n }\n\n const range = { startTokenIndex, endTokenIndex: tokens.length, node };\n if (node.isNamed && duplicateBlockTypes.has(node.type)) {\n blockRanges.push(range);\n }\n return range;\n }\n\n visit(root);\n}\n\n/** The kind tag of a string-like node with no interpolation, or undefined to descend normally. */\nfunction atomicLiteralKind(node: Parser.SyntaxNode): string | undefined {\n const kind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n if (kind === undefined) {\n return undefined;\n }\n return node.namedChildren.every((child) => stringFragmentTypes.has(child.type)) ? kind : undefined;\n}\n\nfunction appendLeafToken(node: Parser.SyntaxNode, tokens: Token[]): void {\n if (commentTypes.has(node.type)) {\n return;\n }\n\n const startRow = node.startPosition.row;\n const endRow = node.endPosition.row;\n if (node.isNamed && shorthandPropertyTypes.has(node.type)) {\n tokens.push(\n { kind: 'text', text: node.text, startRow, endRow },\n { kind: 'text', text: ':', startRow, endRow },\n { kind: 'id', text: node.text, startRow, endRow }\n );\n return;\n }\n\n if (node.isNamed && anonymizedIdentifierTypes.has(node.type) && !isSemanticNameLeaf(node)) {\n tokens.push({ kind: 'id', text: node.text, startRow, endRow });\n return;\n }\n\n // Anything else keeps its text: keywords, operators, punctuation, and semantic names such as\n // `property_identifier`/`type_identifier`, which must distinguish otherwise-identical structures.\n const literalKind = node.isNamed ? literalKindByType.get(node.type) : undefined;\n tokens.push({ kind: 'text', text: literalKind ?? node.text, startRow, endRow });\n}\n\nfunction isSemanticNameLeaf(node: Parser.SyntaxNode): boolean {\n const parent = node.parent;\n if (!parent) {\n return false;\n }\n\n // Java method references (`Foo::bar`) name their identifiers without grammar fields; both the\n // type/object and the referenced method are semantic.\n if (parent.type === 'method_reference') {\n return true;\n }\n\n // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.\n if (parent.type === 'call' && parent.childForFieldName('function')?.id === node.id) {\n return true;\n }\n\n // A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API; constants used as\n // plain values stay anonymized so renamed clones referencing constants still match.\n if (node.type === 'constant' && parent.type === 'call' && parent.childForFieldName('receiver')?.id === node.id) {\n return true;\n }\n\n // Java static receivers (`Alpha.run(...)`) name the invoked type. The tokenizer has no symbol\n // table, so PascalCase — Java's universal type-naming convention — is the discriminator;\n // camelCase instance receivers stay anonymized for rename tolerance.\n if (\n parent.type === 'method_invocation' &&\n parent.childForFieldName('object')?.id === node.id &&\n /^\\p{Lu}/u.test(node.text)\n ) {\n return true;\n }\n\n // Qualified callees (Rust `crate::alpha::make(...)`, C++ `detail::make(...)`) and generic\n // callees (`compute::<u32>(...)`, `compute<int>(...)`) wrap their identifiers arbitrarily deep;\n // every path/name segment is semantic there — but only in call position, so renamed clones that\n // merely reference scoped constants or `use` paths still match.\n if (\n (parent.type === 'scoped_identifier' || parent.type === 'qualified_identifier') &&\n (parent.childForFieldName('name')?.id === node.id || parent.childForFieldName('path')?.id === node.id)\n ) {\n let outer = parent;\n while (\n outer.parent &&\n (outer.parent.type === 'scoped_identifier' ||\n outer.parent.type === 'qualified_identifier' ||\n outer.parent.type === 'generic_function' ||\n outer.parent.type === 'template_function')\n ) {\n outer = outer.parent;\n }\n if (outer.parent?.type === 'call_expression' && outer.parent.childForFieldName('function')?.id === outer.id) {\n return true;\n }\n }\n\n // Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar: the key\n // is the keyed_element's first named child, a literal_element wrapping the identifier.\n if (\n parent.type === 'literal_element' &&\n parent.parent?.type === 'keyed_element' &&\n parent.parent.namedChild(0)?.id === parent.id\n ) {\n return true;\n }\n\n const field = semanticNameFieldByParentType.get(parent.type);\n return field !== undefined && parent.childForFieldName(field)?.id === node.id;\n}\n\nfunction collectBlockCandidates(tokens: Token[], blockRanges: TokenRange[]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n for (const range of blockRanges) {\n const tokenCount = range.endTokenIndex - range.startTokenIndex;\n if (tokenCount < minDuplicateTokenCount) {\n continue;\n }\n candidates.push(\n toCandidate(\n `b:${fingerprintTokens(tokens, range.startTokenIndex, range.endTokenIndex)}`,\n range.startTokenIndex,\n range.endTokenIndex,\n range.node,\n range.node\n )\n );\n }\n return candidates;\n}\n\ninterface WindowOccurrences {\n count: number;\n /** -1 once occurrences span more than one container. */\n containerIndex: number;\n minStart: number;\n maxStart: number;\n}\n\ninterface SequenceWindow {\n containerIndex: number;\n start: number;\n length: number;\n}\n\n/**\n * Enumerates runs of consecutive sibling statements. Every container statement participates; only\n * the window length is capped, so enumeration stays linear in the statement count. Windows are\n * grouped by a cheap rolling hash of per-statement fingerprints, and only locally maximal repeated\n * windows — those whose one-statement extensions stop repeating — become candidates with an exact\n * (window-consistent) fingerprint. Without the maximality filter a degenerate file of\n * near-identical statements would fingerprint every sub-window of every repeated region.\n */\nfunction collectSequenceCandidates(tokens: Token[], containers: TokenRange[][]): DuplicateCandidate[] {\n const candidates: DuplicateCandidate[] = [];\n const occurrencesByWindowKey = new Map<number, WindowOccurrences>();\n const containerWindows = containers.map((statements) => enumerateContainerWindows(tokens, statements));\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const windowKey of row) {\n if (windowKey === undefined) {\n continue;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n if (occurrences) {\n occurrences.count += 1;\n if (occurrences.containerIndex !== containerIndex) {\n occurrences.containerIndex = -1;\n }\n occurrences.minStart = Math.min(occurrences.minStart, start);\n occurrences.maxStart = Math.max(occurrences.maxStart, start);\n } else {\n occurrencesByWindowKey.set(windowKey, { count: 1, containerIndex, minStart: start, maxStart: start });\n }\n }\n }\n }\n\n // A window only \"repeats\" when two of its occurrences can coexist without overlapping: sliding\n // matches inside a homogeneous run (start spread smaller than the window length) can never both\n // be counted and must neither qualify a window nor dominate its sub-windows.\n const repeats = (windowKey: number | undefined, length: number): boolean => {\n if (windowKey === undefined) {\n return false;\n }\n const occurrences = occurrencesByWindowKey.get(windowKey);\n return (\n occurrences !== undefined &&\n occurrences.count >= 2 &&\n (occurrences.containerIndex === -1 || occurrences.maxStart - occurrences.minStart >= length)\n );\n };\n\n // A window whose statements all share one normalized shape (sixteen `let x = 0;` declarations,\n // a constant table) is a homogeneous preamble, not a copy-paste: requiring two distinct\n // per-statement shapes keeps such runs out of duplicate groups and the duplication ratio.\n const hasDistinctStatements = (window: SequenceWindow): boolean => {\n const hashes = containerWindows[window.containerIndex]?.statementHashes ?? [];\n const firstHash = hashes[window.start];\n for (let index = window.start + 1; index < window.start + window.length; index += 1) {\n if (hashes[index] !== firstHash) {\n return true;\n }\n }\n return false;\n };\n\n const maximalWindows: SequenceWindow[] = [];\n for (const [containerIndex, windows] of containerWindows.entries()) {\n for (const [start, row] of windows.windowKeysByStart.entries()) {\n for (const [length, windowKey] of row.entries()) {\n if (!repeats(windowKey, length) || !hasDistinctStatements({ containerIndex, start, length })) {\n continue;\n }\n // Dominated windows are skipped: the one-statement extension also repeats, so a larger\n // candidate covering this window exists.\n const extendedRight = windows.windowKeysByStart[start]?.[length + 1];\n const extendedLeft = windows.windowKeysByStart[start - 1]?.[length + 1];\n if (repeats(extendedRight, length + 1) || repeats(extendedLeft, length + 1)) {\n continue;\n }\n maximalWindows.push({ containerIndex, start, length });\n }\n }\n }\n\n // The rolling hash anonymizes identifiers per statement, so a window can look repeated coarsely\n // while its exact (window-consistent) fingerprints differ, and a longer window's match can\n // dominate sub-windows that other copies still need (three copies where only two extend one\n // statement further). Every emitted window therefore exposes its repeating, unvisited\n // sub-windows; `visited` bounds the worklist and lengths strictly decrease, so it terminates.\n const visited = new Set(maximalWindows.map(windowId));\n let frontier = maximalWindows;\n while (frontier.length > 0) {\n const emitted: SequenceWindow[] = [];\n for (const window of frontier) {\n const statements = containers[window.containerIndex];\n const first = statements?.[window.start];\n const last = statements?.[window.start + window.length - 1];\n if (!first || !last) {\n continue;\n }\n const fingerprint = `s:${fingerprintTokens(tokens, first.startTokenIndex, last.endTokenIndex)}`;\n candidates.push(toCandidate(fingerprint, first.startTokenIndex, last.endTokenIndex, first.node, last.node));\n emitted.push(window);\n }\n frontier = [];\n for (const window of emitted) {\n for (const start of [window.start, window.start + 1]) {\n const subWindow = { containerIndex: window.containerIndex, start, length: window.length - 1 };\n const subWindowKey = containerWindows[window.containerIndex]?.windowKeysByStart[start]?.[subWindow.length];\n if (\n visited.has(windowId(subWindow)) ||\n !repeats(subWindowKey, subWindow.length) ||\n !hasDistinctStatements(subWindow)\n ) {\n continue;\n }\n visited.add(windowId(subWindow));\n frontier.push(subWindow);\n }\n }\n }\n return candidates;\n}\n\nfunction windowId(window: SequenceWindow): string {\n return `${window.containerIndex}:${window.start}:${window.length}`;\n}\n\ninterface ContainerWindows {\n /** windowKeysByStart[start][length] is the rolling-hash key of the window, or undefined if it is below the size thresholds. */\n windowKeysByStart: (number | undefined)[][];\n /** Per-statement fingerprint hashes, for the distinct-shape requirement on windows. */\n statementHashes: number[];\n}\n\nfunction enumerateContainerWindows(tokens: Token[], statements: TokenRange[]): ContainerWindows {\n const statementHashes = statements.map((statement) =>\n hashText(fingerprintTokens(tokens, statement.startTokenIndex, statement.endTokenIndex))\n );\n const windowKeysByStart: (number | undefined)[][] = [];\n for (let start = 0; start < statements.length; start += 1) {\n const row: (number | undefined)[] = [];\n let hash = 5381;\n let tokenCount = 0;\n const maxEnd = Math.min(statements.length, start + maxSequenceStatementCount);\n for (let end = start; end < maxEnd; end += 1) {\n const statement = statements[end];\n const statementHash = statementHashes[end];\n if (!statement || statementHash === undefined) {\n break;\n }\n hash = combineHashes(hash, statementHash);\n tokenCount += statement.endTokenIndex - statement.startTokenIndex;\n const statementCount = end - start + 1;\n row[statementCount] =\n statementCount >= minSequenceStatementCount && tokenCount >= minDuplicateTokenCount\n ? combineHashes(hash, statementCount)\n : undefined;\n }\n windowKeysByStart.push(row);\n }\n return { windowKeysByStart, statementHashes };\n}\n\nfunction toCandidate(\n fingerprint: string,\n startTokenIndex: number,\n endTokenIndex: number,\n firstNode: Parser.SyntaxNode,\n lastNode: Parser.SyntaxNode\n): DuplicateCandidate {\n return {\n fingerprint,\n tokenCount: endTokenIndex - startTokenIndex,\n startTokenIndex,\n endTokenIndex,\n startIndex: firstNode.startIndex,\n endIndex: lastNode.endIndex,\n startLine: firstNode.startPosition.row + 1,\n endLine: lastNode.endPosition.row + 1,\n };\n}\n\n/** Serializes a token range with identifiers anonymized consistently by first-occurrence order. */\nfunction fingerprintTokens(tokens: Token[], startTokenIndex: number, endTokenIndex: number): string {\n const indexByIdentifier = new Map<string, number>();\n const parts: string[] = [];\n for (let index = startTokenIndex; index < endTokenIndex; index += 1) {\n const token = tokens[index];\n if (!token) {\n continue;\n }\n if (token.kind === 'id') {\n let identifierIndex = indexByIdentifier.get(token.text);\n if (identifierIndex === undefined) {\n identifierIndex = indexByIdentifier.size;\n indexByIdentifier.set(token.text, identifierIndex);\n }\n parts.push(`$${identifierIndex}`);\n } else {\n parts.push(token.text);\n }\n }\n return parts.join(' ');\n}\n\n/** djb2-style hash; XOR keeps the value in signed 32-bit range, which is fine for a grouping key. */\nfunction hashText(text: string): number {\n let hash = 5381;\n for (let index = 0; index < text.length; index += 1) {\n // oxlint-disable-next-line unicorn/prefer-code-point -- djb2 hashes UTF-16 code units; codePointAt would hash surrogate pairs twice (full code point, then the lone low surrogate).\n hash = Math.imul(hash, 33) ^ text.charCodeAt(index);\n }\n return hash;\n}\n\nfunction combineHashes(hash: number, value: number): number {\n return Math.imul(hash, 31) + value;\n}\n\n/**\n * Keeps only maximal, non-overlapping duplicates: candidates are grouped by fingerprint, larger\n * regions win over regions overlapping them, and groups reduced below two survivors are dropped.\n */\nfunction selectMaximalDuplicates(candidates: DuplicateCandidate[]): Map<string, DuplicateCandidate[]> {\n const byFingerprint = new Map<string, DuplicateCandidate[]>();\n for (const candidate of candidates) {\n const group = byFingerprint.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n byFingerprint.set(candidate.fingerprint, group);\n }\n\n const groups = [...byFingerprint.values()].map(dedupeByRegion).filter((group) => group.length >= 2);\n // Greedy order ranks by total coverage (region size × copies): a 3×3-statement group must beat\n // a 2×4-statement group overlapping two of its copies, or the third copy is silently dropped\n // and the reported duplication shrinks as more copies are added.\n const groupSizeByFingerprint = new Map(groups.map((group) => [group[0]?.fingerprint ?? '', group.length]));\n const coverage = (candidate: DuplicateCandidate): number =>\n candidate.tokenCount * (groupSizeByFingerprint.get(candidate.fingerprint) ?? 1);\n let duplicates = groups.flat();\n duplicates.sort((left, right) => coverage(right) - coverage(left));\n\n // Greedy selection can keep a candidate whose group ends up below two survivors; such an\n // uncounted region must not block smaller groups, so the largest failed group is removed and the\n // selection reruns. One group at a time: freeing a failed group's regions can rescue another.\n // The rerun cap bounds degenerate inputs; past it the remaining failed groups are dropped,\n // trading a sliver of recall on such files for bounded runtime.\n for (let rerun = 0; ; rerun += 1) {\n const keptRegions: { startIndex: number; endIndex: number }[] = [];\n const counted = new Map<string, DuplicateCandidate[]>();\n for (const candidate of duplicates) {\n if (keptRegions.some((region) => overlaps(region, candidate))) {\n continue;\n }\n keptRegions.push(candidate);\n const group = counted.get(candidate.fingerprint) ?? [];\n group.push(candidate);\n counted.set(candidate.fingerprint, group);\n }\n\n let failedFingerprint: string | undefined;\n let failedTokenCount = -1;\n for (const [fingerprint, group] of counted) {\n const tokenCount = group[0]?.tokenCount ?? 0;\n if (group.length < 2 && tokenCount > failedTokenCount) {\n failedFingerprint = fingerprint;\n failedTokenCount = tokenCount;\n }\n }\n // No failed fingerprint means every counted group kept at least two survivors.\n if (failedFingerprint === undefined) {\n return counted;\n }\n if (rerun >= maxSelectionRerunCount) {\n for (const [fingerprint, group] of counted) {\n if (group.length < 2) {\n counted.delete(fingerprint);\n }\n }\n return counted;\n }\n\n duplicates = duplicates.filter((candidate) => candidate.fingerprint !== failedFingerprint);\n }\n}\n\n/** Drops candidates covering the same source region (a block and the statement run spanning it). */\nfunction dedupeByRegion(group: DuplicateCandidate[]): DuplicateCandidate[] {\n const byRegion = new Map<string, DuplicateCandidate>();\n for (const candidate of group) {\n const key = `${candidate.startIndex}:${candidate.endIndex}`;\n const existing = byRegion.get(key);\n if (!existing || candidate.tokenCount > existing.tokenCount) {\n byRegion.set(key, candidate);\n }\n }\n return [...byRegion.values()];\n}\n\nfunction overlaps(\n left: { startIndex: number; endIndex: number },\n right: { startIndex: number; endIndex: number }\n): boolean {\n return left.startIndex < right.endIndex && right.startIndex < left.endIndex;\n}\n\nfunction summarizeDuplicates(\n counted: Map<string, DuplicateCandidate[]>,\n codeLineNumbers: Set<number>,\n tokens: Token[]\n): DuplicationMetrics {\n let duplicateBlockCount = 0;\n let maxDuplicateBlockSize = 0;\n const duplicateBlockGroups: { startLine: number; endLine: number }[][] = [];\n const duplicatedLines = new Set<number>();\n for (const group of counted.values()) {\n duplicateBlockCount += group.length - 1;\n for (const candidate of group) {\n maxDuplicateBlockSize = Math.max(maxDuplicateBlockSize, candidate.tokenCount);\n // Only CODE lines carrying matched tokens count: comments and blank gaps inside a\n // candidate's bounding range — and blank rows inside a multi-row token (heredocs, template\n // literals) — are not duplicated content and would push the ratio past 1.\n for (let index = candidate.startTokenIndex; index < candidate.endTokenIndex; index += 1) {\n const token = tokens[index];\n for (let row = token?.startRow ?? 0; row <= (token?.endRow ?? -1); row += 1) {\n if (codeLineNumbers.has(row + 1)) {\n duplicatedLines.add(row + 1);\n }\n }\n }\n }\n duplicateBlockGroups.push(\n group\n .map(({ startLine, endLine }) => ({ startLine, endLine }))\n .toSorted((left, right) => left.startLine - right.startLine)\n );\n }\n duplicateBlockGroups.sort((left, right) => (left[0]?.startLine ?? 0) - (right[0]?.startLine ?? 0));\n\n return {\n duplicateBlockCount,\n duplicateBlockGroupCount: counted.size,\n duplicateBlockGroups,\n duplicateLineCount: duplicatedLines.size,\n duplicationRatio: codeLineNumbers.size === 0 ? 0 : duplicatedLines.size / codeLineNumbers.size,\n maxDuplicateBlockSize,\n };\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1,2 +1 @@
1
- "use strict";var e=require("./languages.cjs"),r=require("./metrics.cjs");exports.defaultLanguages=e.defaultLanguages,exports.supportedLanguages=e.supportedLanguages,exports.TreeMeasurer=r.TreeMeasurer,exports.defaultMeasurer=r.defaultMeasurer,exports.measureCode=r.measureCode;
2
- //# sourceMappingURL=index.cjs.map
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./languages.cjs"),t=require("./metrics.cjs");exports.TreeMeasurer=t.TreeMeasurer,exports.defaultLanguages=e.defaultLanguages,exports.defaultMeasurer=t.defaultMeasurer,exports.measureCode=t.measureCode,exports.supportedLanguages=e.supportedLanguages;
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
- export{defaultLanguages,supportedLanguages}from"./languages.js";export{TreeMeasurer,defaultMeasurer,measureCode}from"./metrics.js";
2
- //# sourceMappingURL=index.js.map
1
+ import{defaultLanguages as e,supportedLanguages as t}from"./languages.js";import{TreeMeasurer as n,defaultMeasurer as r,measureCode as i}from"./metrics.js";export{n as TreeMeasurer,e as defaultLanguages,r as defaultMeasurer,i as measureCode,t as supportedLanguages};