code-gauge 4.3.1 → 4.5.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.
Files changed (45) hide show
  1. package/README.md +9 -4
  2. package/dist/crossFileDuplication.cjs +1 -1
  3. package/dist/crossFileDuplication.cjs.map +1 -1
  4. package/dist/crossFileDuplication.d.ts +11 -4
  5. package/dist/crossFileDuplication.js +1 -1
  6. package/dist/crossFileDuplication.js.map +1 -1
  7. package/dist/crossFileNearMiss.cjs +2 -0
  8. package/dist/crossFileNearMiss.cjs.map +1 -0
  9. package/dist/crossFileNearMiss.d.ts +27 -0
  10. package/dist/crossFileNearMiss.js +2 -0
  11. package/dist/crossFileNearMiss.js.map +1 -0
  12. package/dist/diffCommand.cjs +1 -1
  13. package/dist/diffCommand.cjs.map +1 -1
  14. package/dist/diffCommand.js +3 -3
  15. package/dist/diffCommand.js.map +1 -1
  16. package/dist/duplication.cjs +1 -1
  17. package/dist/duplication.cjs.map +1 -1
  18. package/dist/duplication.d.ts +11 -0
  19. package/dist/duplication.js +1 -1
  20. package/dist/duplication.js.map +1 -1
  21. package/dist/metrics.cjs +1 -1
  22. package/dist/metrics.cjs.map +1 -1
  23. package/dist/metrics.d.ts +10 -0
  24. package/dist/metrics.js +1 -1
  25. package/dist/metrics.js.map +1 -1
  26. package/dist/nativeMetrics.cjs +2 -2
  27. package/dist/nativeMetrics.cjs.map +1 -1
  28. package/dist/nativeMetrics.d.ts +7 -2
  29. package/dist/nativeMetrics.js +2 -2
  30. package/dist/nativeMetrics.js.map +1 -1
  31. package/dist/scan.cjs +1 -1
  32. package/dist/scan.cjs.map +1 -1
  33. package/dist/scan.d.ts +12 -1
  34. package/dist/scan.js +1 -1
  35. package/dist/scan.js.map +1 -1
  36. package/dist/types.d.ts +11 -1
  37. package/native/src/complexity.rs +23 -7
  38. package/native/src/dep_degree.rs +2 -3
  39. package/native/src/duplication.rs +170 -115
  40. package/native/src/functions.rs +1 -1
  41. package/native/src/languages.rs +17 -0
  42. package/native/src/lib.rs +6 -2
  43. package/native/src/measure.rs +118 -13
  44. package/native/src/types.rs +6 -0
  45. package/package.json +8 -8
package/dist/metrics.d.ts CHANGED
@@ -9,6 +9,11 @@ export declare class TreeMeasurer {
9
9
  private readonly registry;
10
10
  getSupportedLanguages(): LanguageName[];
11
11
  measure(code: string, options: MeasureOptions): CodeMetrics;
12
+ /** Measures a file and collects its cross-file clone-detection contribution from one parse. */
13
+ measureWithCrossFileData(code: string, options: MeasureOptions): {
14
+ metrics: CodeMetrics;
15
+ crossFileData: CrossFileDuplicationFileData;
16
+ };
12
17
  /** Collects one file's duplicate candidates for cross-file clone detection. */
13
18
  collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[];
14
19
  /**
@@ -28,6 +33,11 @@ export declare class TreeMeasurer {
28
33
  export declare const defaultMeasurer: TreeMeasurer;
29
34
  export declare function measureCode(code: string, options: MeasureOptions): CodeMetrics;
30
35
  /** Standalone helper mirroring measureCode for the default measurer. */
36
+ export declare function measureCodeWithCrossFileData(code: string, options: MeasureOptions): {
37
+ metrics: CodeMetrics;
38
+ crossFileData: CrossFileDuplicationFileData;
39
+ };
40
+ /** Standalone helper mirroring measureCode for the default measurer. */
31
41
  export declare function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[];
32
42
  /** Standalone helper mirroring measureCode for the default measurer. */
33
43
  export declare function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[];
package/dist/metrics.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r}from"./nativeMetrics.js";var i=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return a(r(e,n.name,i,t.duplication),i)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language),i=t(e,r.name,n.duplication?.minTokens);return{candidates:i.candidates,tokens:i.tokens,containerStatements:i.containerStatements,codeLineNumbers:new Set(i.codeLineNumbers)}}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function a(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:o(e.halsteadCounts),depDegree:e.depDegree})),cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:o(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function o(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const s=new i;function c(e,t){return s.measure(e,t)}function l(e,t){return s.collectDuplicationCandidates(e,t)}function u(e,t){return s.collectFunctionTokenSequences(e,t)}function d(e,t){return s.collectCrossFileDuplicationFileData(e,t)}export{i as TreeMeasurer,d as collectCrossFileDuplicationFileData,l as collectDuplicationCandidates,u as collectFunctionTokenSequences,s as defaultMeasurer,c as measureCode};
1
+ import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r}from"./nativeMetrics.js";var i=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return o(r(e,n.name,i,t.duplication),i)}measureWithCrossFileData(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1,s=r(e,n.name,i,t.duplication,!0);if(!s.crossFileData)throw Error(`The native addon returned no cross-file duplication data`);return{metrics:o(s,i),crossFileData:a(s.crossFileData)}}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language);return a(t(e,r.name,n.duplication?.minTokens))}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function a(e){return{candidates:e.candidates,tokens:e.tokens,containerStatements:e.containerStatements,nearMissBlocks:e.nearMissBlocks,codeLineNumbers:new Set(e.codeLineNumbers)}}function o(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:s(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:s(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function s(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,effort:(n===0?0:t/2*(i/n))*s}}const c=new i;function l(e,t){return c.measure(e,t)}function u(e,t){return c.measureWithCrossFileData(e,t)}function d(e,t){return c.collectDuplicationCandidates(e,t)}function f(e,t){return c.collectFunctionTokenSequences(e,t)}function p(e,t){return c.collectCrossFileDuplicationFileData(e,t)}export{i as TreeMeasurer,p as collectCrossFileDuplicationFileData,d as collectDuplicationCandidates,f as collectFunctionTokenSequences,c as defaultMeasurer,l as measureCode,u as measureCodeWithCrossFileData};
2
2
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n const payload = collectCrossFileDataNative(code, language.name, options.duplication?.minTokens);\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"wLAgBA,IAAa,EAAb,KAA0B,CACxB,SAA4B,EAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADS,EAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAU,EAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,EAC9F,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAE7B,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAQA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAO,EAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAInE,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,QAXiB,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,IAC/D,CAW5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n type NativeCrossFileDataPayload,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Measures a file and collects its cross-file clone-detection contribution from one parse. */\n measureWithCrossFileData(\n code: string,\n options: MeasureOptions\n ): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication, true);\n if (!payload.crossFileData) {\n throw new Error('The native addon returned no cross-file duplication data');\n }\n return {\n metrics: assembleNativeMetrics(payload, includeSyntaxTree),\n crossFileData: toCrossFileDuplicationFileData(payload.crossFileData),\n };\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n return toCrossFileDuplicationFileData(\n collectCrossFileDataNative(code, language.name, options.duplication?.minTokens)\n );\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim, so the regression gate can re-match renamed or\n * moved functions across two revisions by token-LCS similarity.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\nfunction toCrossFileDuplicationFileData(payload: NativeCrossFileDataPayload): CrossFileDuplicationFileData {\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n nearMissBlocks: payload.nearMissBlocks,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function measureCodeWithCrossFileData(\n code: string,\n options: MeasureOptions\n): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n return defaultMeasurer.measureWithCrossFileData(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- "use strict";let e=require("node:module");function t(e,t,n,r){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent)))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=(0,e.createRequire)(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
- `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==5){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 5; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
1
+ "use strict";let e=require("node:module");function t(e,t,n,r,o=!1){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent),o))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=(0,e.createRequire)(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
+ `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==7){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 7; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
3
3
  `)}`),c}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=o,exports.collectCrossFileDataNative=n,exports.collectFunctionTokenSequencesNative=r,exports.measureCodeNative=t;
4
4
  //# sourceMappingURL=nativeMetrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.cjs","names":["createRequire"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 5;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"0CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,GAAA,EAAgBA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.cjs","names":["createRequire"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"0CAgEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,GAAA,EAAgBA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -19,17 +19,22 @@ export interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'fu
19
19
  functions: NativeFunctionMetricsPayload[];
20
20
  halsteadCounts: NativeHalsteadCounts;
21
21
  syntaxTree?: string;
22
+ crossFileData?: NativeCrossFileDataPayload;
22
23
  }
23
24
  /** One file's cross-file clone-detection contribution as serialized by the native addon. */
24
25
  export interface NativeCrossFileDataPayload {
25
26
  candidates: CrossFileDuplicateCandidate[];
26
27
  tokens: Token[];
27
28
  containerStatements: TokenRange[][];
29
+ nearMissBlocks: TokenRange[];
28
30
  /** 1-based lines that are neither blank nor comment-only, sorted ascending. */
29
31
  codeLineNumbers: number[];
30
32
  }
31
- /** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */
32
- export declare function measureCodeNative(code: string, language: string, includeSyntaxTree: boolean, duplication?: DuplicationOptions): NativeMetricsPayload;
33
+ /**
34
+ * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;
35
+ * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.
36
+ */
37
+ export declare function measureCodeNative(code: string, language: string, includeSyntaxTree: boolean, duplication?: DuplicationOptions, includeCrossFileData?: boolean): NativeMetricsPayload;
33
38
  /** Collects one file's cross-file clone-detection contribution via the native addon. */
34
39
  export declare function collectCrossFileDataNative(code: string, language: string, minTokens?: number): NativeCrossFileDataPayload;
35
40
  /** Collects normalized token hash sequences of every function via the native addon. */
@@ -1,4 +1,4 @@
1
- import{createRequire as e}from"node:module";function t(e,t,n,r){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent)))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=e(import.meta.url),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
- `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==5){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 5; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
1
+ import{createRequire as e}from"node:module";function t(e,t,n,r,o=!1){return JSON.parse(l().measureCodeNative(i(e),t,n,a(r?.minTokens),a(r?.maxGapTokens),a(r?.minSimilarityPercent),o))}function n(e,t,n){return JSON.parse(l().collectCrossFileDataNative(i(e),t,a(n)))}function r(e,t){return JSON.parse(l().collectFunctionTokenSequencesNative(i(e),t)).map(e=>Int32Array.from(e))}function i(e){return e.isWellFormed()?e:e.toWellFormed()}function a(e){return e===void 0||Number.isNaN(e)?void 0:Math.min(Math.max(Math.trunc(e),0),4294967295)}var o=class extends Error{};let s,c;function l(){if(s)return s;if(c)throw c;let t=e(import.meta.url),n=[`code-gauge-${u()}`,`../native/code-gauge.node`],r=[];for(let e of n){let n;try{n=t(e)}catch(t){r.push(` ${e}: ${t instanceof Error?t.message.split(`
2
+ `)[0]:String(t)}`);continue}let i=n.payloadVersion?.();if(i!==7){r.push(` ${e}: payload version ${i??`unknown`} does not match the expected 7; rebuild the addon with \`bun run build-native\``);continue}return s=n,n}throw c=new o(`The code-gauge native addon is not available for ${u()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain); when installing with npm, also allow install scripts for code-gauge so its postinstall build can run.\n${r.join(`
3
3
  `)}`),c}function u(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{o as NativeAddonError,n as collectCrossFileDataNative,r as collectFunctionTokenSequencesNative,t as measureCodeNative};
4
4
  //# sourceMappingURL=nativeMetrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 5;\n\n/** Measures one file via the native addon, returning the raw payload for assembly in metrics.ts. */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent)\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"4CA0DA,SAAgB,EACd,EACA,EACA,EACA,EACsB,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,CAC9C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,EAAgB,EAAc,YAAY,GAAG,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.js","names":[],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ninterface NativeBinding {\n measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens?: number,\n maxGapTokens?: number,\n minSimilarityPercent?: number,\n includeCrossFileData?: boolean\n ): string;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields instead of failing with a clear rebuild message.\n */\nconst expectedPayloadVersion = 7;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData\n )\n ) as NativeMetricsPayload;\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value)\n ? undefined\n : Math.min(Math.max(Math.trunc(value), 0), 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n const requireNative = createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${error instanceof Error ? error.message.split('\\n')[0] : String(error)}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n 'toolchain); when installing with npm, also allow install scripts for code-gauge so its ' +\n `postinstall build can run.\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"4CAgEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAC5C,IAAA,GACA,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,CAAK,EAAG,CAAC,EAAG,UAAa,CAC5D,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAEJ,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAGR,IAAM,EAAgB,EAAc,YAAY,GAAG,EAC7C,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,IAAI,aAAiB,MAAQ,EAAM,QAAQ,MAAM;CAAI,CAAC,CAAC,GAAK,OAAO,CAAK,GAAG,EACxG,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAY,EAAwB,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAOA,KANA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,2NAGrC,EAAS,KAAK;CAAI,GACrD,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
package/dist/scan.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs/promises"),s=require("node:os");s=e.__toESM(s,1);const c=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),l=new Set([`__tests__`,`test`,`tests`,`spec`]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,d=/Tests?\.(?:java|kt|cs)$/u;function f(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?a.default.join(s.default.homedir(),e.slice(2)):a.default.resolve(e)}async function p(e){try{return(await(0,o.stat)(e)).isDirectory()?e:a.default.dirname(e)}catch{return a.default.dirname(e)}}async function m(e,t){let n=[],r=[],i=[],s=e;try{s=await(0,o.realpath)(e)}catch{}let c=a.default.dirname(s),l;try{l=await(0,o.stat)(s)}catch(e){let t=`${A(s,c)}: ${N(e)}`;return{displayRoot:c,files:n,errors:[t],warnings:i,fatalError:t}}if(l.isFile()){let e=a.default.dirname(s),o=k(s,t,!0);if(!o){let t=`${A(s,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=_(t,n,r,i,e);try{await C(s,o,`single-file`,c,s)}catch(t){return g(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await b(s,_(t,n,r,i,s))}catch(e){return g(e,s,n,r,i)}return{displayRoot:s,files:n,errors:r,warnings:i}}async function h(e,t,n){let r=[],i=[],s=[],c=_(n,r,i,s,e);for(let l of t){let t=O(l,n)?k(l,n):void 0;if(!t)continue;let u=a.default.join(e,l);if(!(await(0,o.lstat)(u).catch(()=>{}))?.isSymbolicLink())try{await C(u,t,`directory`,c)}catch(t){return g(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function g(e,t,n,i,a){if(!(e instanceof r.NativeAddonError))throw e;let o=N(e);return{displayRoot:t,files:n,errors:[...i,o],warnings:a,fatalError:o}}function _(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function v(e,t,n){try{return await e()}catch(e){n.errors.push(`${A(t,n.rootDirectory)}: ${N(e)}`);return}}async function y(e,t){let n=await v(()=>(0,o.realpath)(e),e,t);return n!==void 0&&D(n,t.rootDirectory)?n:void 0}async function b(e,t){let n=await y(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await v(()=>(0,o.readdir)(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=a.default.join(e,n.name);if(n.isSymbolicLink()){await x(n.name,r,t);continue}if(n.isDirectory()){if(E(n.name,t.options))continue;await b(r,t);continue}n.isFile()&&await S(r,t)}}async function x(e,t,n){let r=await y(t,n);if(r===void 0)return;let i=await v(()=>(0,o.stat)(t),t,n);if(i!==void 0){if(i.isDirectory()){if(E(e,n.options)||E(a.default.basename(r),n.options))return;await b(t,n);return}i.isFile()&&await S(t,n,r,r)}}async function S(e,t,n=e,r){let i=k(n,t.options);i&&await C(e,i,`directory`,t,r)}async function C(e,t,n,a,s){try{let r=s??await(0,o.realpath)(e);if(a.visitedFiles.has(r))return;a.visitedFiles.add(r);let c=await(0,o.readFile)(e,`utf8`),l={language:t,duplication:a.options.duplication},u={file:e,metrics:i.measureCode(c,l)};if(n===`directory`)try{u.duplicationCandidates=i.collectCrossFileDuplicationFileData(c,l)}catch(t){a.warnings.push(`${A(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${N(t)}`)}a.files.push(u)}catch(t){if(t instanceof r.NativeAddonError)throw t;a.errors.push(`${A(e,a.rootDirectory)}: ${N(t)}`)}}function w(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:A(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function T(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function E(e,t){return c.has(e)?!0:!t.includeTests&&l.has(e)}function D(e,t){let n=a.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${a.default.sep}`)&&!a.default.isAbsolute(n)}function O(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(c.has(e)||!t.includeTests&&l.has(e))return!1;return k(e,t)!==void 0}function k(e,t,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||t.includeTests||!u.test(a.default.basename(e))&&!d.test(a.default.basename(e))))return n.detectLanguage(e)}function A(e,t){return a.default.relative(t,e)||a.default.basename(e)}function j(e){process.stdout.write(e)}function M(e){process.stderr.write(e)}function N(e){return e instanceof Error?e.message:String(e)}exports.addCrossFileDuplication=w,exports.collectDuplicatedLineNumbers=T,exports.configSearchDirectory=p,exports.formatError=N,exports.formatPath=A,exports.getLanguage=k,exports.isScannedPath=O,exports.resolveTarget=f,exports.scanListedFiles=h,exports.scanTarget=m,exports.writeStderr=M,exports.writeStdout=j;
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:path");a=e.__toESM(a,1);let o=require("node:fs/promises"),s=require("node:os");s=e.__toESM(s,1);const c=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),l=new Set([`__tests__`,`test`,`tests`,`spec`]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,d=/Tests?\.(?:java|kt|cs)$/u;function f(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?a.default.join(s.default.homedir(),e.slice(2)):a.default.resolve(e)}async function p(e){try{return(await(0,o.stat)(e)).isDirectory()?e:a.default.dirname(e)}catch{return a.default.dirname(e)}}async function m(e,t){let n=[],r=[],i=[],s=e;try{s=await(0,o.realpath)(e)}catch{}let c=a.default.dirname(s),l;try{l=await(0,o.stat)(s)}catch(e){let t=`${j(s,c)}: ${P(e)}`;return{displayRoot:c,files:n,errors:[t],warnings:i,fatalError:t}}if(l.isFile()){let e=a.default.dirname(s),o=A(s,t,!0);if(!o){let t=`${j(s,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=_(t,n,r,i,e);try{await C(s,o,`single-file`,c,s)}catch(t){return g(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await b(s,_(t,n,r,i,s))}catch(e){return g(e,s,n,r,i)}return{displayRoot:s,files:n,errors:r,warnings:i}}async function h(e,t,n){let r=[],i=[],s=[],c=_(n,r,i,s,e);for(let l of t){let t=k(l,n)?A(l,n):void 0;if(!t)continue;let u=a.default.join(e,l);if(!(await(0,o.lstat)(u).catch(()=>{}))?.isSymbolicLink())try{await C(u,t,`directory`,c)}catch(t){return g(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function g(e,t,n,i,a){if(!(e instanceof r.NativeAddonError))throw e;let o=P(e);return{displayRoot:t,files:n,errors:[...i,o],warnings:a,fatalError:o}}function _(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function v(e,t,n){try{return await e()}catch(e){n.errors.push(`${j(t,n.rootDirectory)}: ${P(e)}`);return}}async function y(e,t){let n=await v(()=>(0,o.realpath)(e),e,t);return n!==void 0&&O(n,t.rootDirectory)?n:void 0}async function b(e,t){let n=await y(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await v(()=>(0,o.readdir)(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=a.default.join(e,n.name);if(n.isSymbolicLink()){await x(n.name,r,t);continue}if(n.isDirectory()){if(D(n.name,t.options))continue;await b(r,t);continue}n.isFile()&&await S(r,t)}}async function x(e,t,n){let r=await y(t,n);if(r===void 0)return;let i=await v(()=>(0,o.stat)(t),t,n);if(i!==void 0){if(i.isDirectory()){if(D(e,n.options)||D(a.default.basename(r),n.options))return;await b(t,n);return}i.isFile()&&await S(t,n,r,r)}}async function S(e,t,n=e,r){let i=A(n,t.options);i&&await C(e,i,`directory`,t,r)}async function C(e,t,n,a,s){try{let r=s??await(0,o.realpath)(e);if(a.visitedFiles.has(r))return;a.visitedFiles.add(r);let c=await(0,o.readFile)(e,`utf8`),l={language:t,duplication:a.options.duplication};if(n===`single-file`){a.files.push({file:e,metrics:i.measureCode(c,l)});return}let{metrics:u,crossFileData:d,crossFileError:f}=w(c,l);f!==void 0&&a.warnings.push(`${j(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${f}`),a.files.push({file:e,metrics:u,duplicationCandidates:d})}catch(t){if(t instanceof r.NativeAddonError)throw t;a.errors.push(`${j(e,a.rootDirectory)}: ${P(t)}`)}}function w(e,t){try{return i.measureCodeWithCrossFileData(e,t)}catch(n){if(n instanceof r.NativeAddonError)throw n;return{metrics:i.measureCode(e,t),crossFileError:P(n)}}}function T(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:j(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function E(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function D(e,t){return c.has(e)?!0:!t.includeTests&&l.has(e)}function O(e,t){let n=a.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${a.default.sep}`)&&!a.default.isAbsolute(n)}function k(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(c.has(e)||!t.includeTests&&l.has(e))return!1;return A(e,t)!==void 0}function A(e,t,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||t.includeTests||!u.test(a.default.basename(e))&&!d.test(a.default.basename(e))))return n.detectLanguage(e)}function j(e,t){return a.default.relative(t,e)||a.default.basename(e)}function M(e){process.stdout.write(e)}function N(e){process.stderr.write(e)}function P(e){return e instanceof Error?e.message:String(e)}exports.addCrossFileDuplication=T,exports.collectDuplicatedLineNumbers=E,exports.configSearchDirectory=p,exports.formatError=P,exports.formatPath=j,exports.getLanguage=A,exports.isScannedPath=k,exports.measureWithCrossFileData=w,exports.resolveTarget=f,exports.scanListedFiles=h,exports.scanTarget=m,exports.writeStderr=N,exports.writeStdout=M;
2
2
  //# sourceMappingURL=scan.cjs.map
package/dist/scan.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scan.cjs","names":["os","path","stat","realpath","lstat","NativeAddonError","readdir","readFile","measureCode","collectCrossFileDuplicationFileData","measureCrossFileDuplication","detectLanguage"],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection is an auxiliary pass: if it fails where measureCode succeeded (an\n // addon error specific to this pass), that must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scan.cjs","names":["os","path","stat","realpath","lstat","NativeAddonError","readdir","readFile","measureCode","measureCodeWithCrossFileData","measureCrossFileDuplication","detectLanguage"],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileData } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n context.files.push({ file, metrics: measureCode(code, measureOptions) });\n return;\n }\n const { metrics, crossFileData, crossFileError } = measureWithCrossFileData(code, measureOptions);\n if (crossFileError !== undefined) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`\n );\n }\n context.files.push({ file, metrics, duplicationCandidates: crossFileData });\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): { metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string } {\n try {\n return measureCodeWithCrossFileData(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
package/dist/scan.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type CrossFileDuplicationMetrics } from './crossFileDuplication.js';
2
2
  import type { CrossFileDuplicationFileData } from './duplication.js';
3
- import type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';
3
+ import type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';
4
4
  /** The scan settings shared by every command (a structural subset of each command's options). */
5
5
  export interface ScanOptions {
6
6
  duplication: Required<DuplicationOptions>;
@@ -32,6 +32,17 @@ export declare function scanTarget(target: string, options: ScanOptions): Promis
32
32
  * with the same rules as the walk.
33
33
  */
34
34
  export declare function scanListedFiles(rootDirectory: string, relativePaths: Iterable<string>, options: ScanOptions): Promise<ScanResult>;
35
+ /**
36
+ * Measures a file together with its cross-file contribution from one parse. The contribution is
37
+ * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to
38
+ * cross the addon boundary), the metrics are still returned with the failure message, which
39
+ * callers report as a warning rather than an error.
40
+ */
41
+ export declare function measureWithCrossFileData(code: string, measureOptions: MeasureOptions): {
42
+ metrics: CodeMetrics;
43
+ crossFileData?: CrossFileDuplicationFileData;
44
+ crossFileError?: string;
45
+ };
35
46
  /** Runs after the scan so every measured file's candidates participate. */
36
47
  export declare function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void;
37
48
  /**
package/dist/scan.js CHANGED
@@ -1,2 +1,2 @@
1
- import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{collectCrossFileDuplicationFileData as r,measureCode as i}from"./metrics.js";import a from"node:path";import{lstat as o,readFile as s,readdir as c,realpath as l,stat as u}from"node:fs/promises";import d from"node:os";const f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?a.join(d.homedir(),e.slice(2)):a.resolve(e)}async function _(e){try{return(await u(e)).isDirectory()?e:a.dirname(e)}catch{return a.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],o=e;try{o=await l(e)}catch{}let s=a.dirname(o),c;try{c=await u(o)}catch(e){let t=`${P(o,s)}: ${L(e)}`;return{displayRoot:s,files:n,errors:[t],warnings:i,fatalError:t}}if(c.isFile()){let e=a.dirname(o),s=N(o,t,!0);if(!s){let t=`${P(o,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=x(t,n,r,i,e);try{await D(o,s,`single-file`,c,o)}catch(t){return b(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await w(o,x(t,n,r,i,o))}catch(e){return b(e,o,n,r,i)}return{displayRoot:o,files:n,errors:r,warnings:i}}async function y(e,t,n){let r=[],i=[],s=[],c=x(n,r,i,s,e);for(let l of t){let t=M(l,n)?N(l,n):void 0;if(!t)continue;let u=a.join(e,l);if(!(await o(u).catch(()=>{}))?.isSymbolicLink())try{await D(u,t,`directory`,c)}catch(t){return b(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function b(e,t,r,i,a){if(!(e instanceof n))throw e;let o=L(e);return{displayRoot:t,files:r,errors:[...i,o],warnings:a,fatalError:o}}function x(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function S(e,t,n){try{return await e()}catch(e){n.errors.push(`${P(t,n.rootDirectory)}: ${L(e)}`);return}}async function C(e,t){let n=await S(()=>l(e),e,t);return n!==void 0&&j(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>c(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=a.join(e,n.name);if(n.isSymbolicLink()){await T(n.name,r,t);continue}if(n.isDirectory()){if(A(n.name,t.options))continue;await w(r,t);continue}n.isFile()&&await E(r,t)}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>u(t),t,n);if(i!==void 0){if(i.isDirectory()){if(A(e,n.options)||A(a.basename(r),n.options))return;await w(t,n);return}i.isFile()&&await E(t,n,r,r)}}async function E(e,t,n=e,r){let i=N(n,t.options);i&&await D(e,i,`directory`,t,r)}async function D(e,t,a,o,c){try{let n=c??await l(e);if(o.visitedFiles.has(n))return;o.visitedFiles.add(n);let u=await s(e,`utf8`),d={language:t,duplication:o.options.duplication},f={file:e,metrics:i(u,d)};if(a===`directory`)try{f.duplicationCandidates=r(u,d)}catch(t){o.warnings.push(`${P(e,o.rootDirectory)}: cross-file duplication candidates unavailable: ${L(t)}`)}o.files.push(f)}catch(t){if(t instanceof n)throw t;o.errors.push(`${P(e,o.rootDirectory)}: ${L(t)}`)}}function O(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:P(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function k(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function A(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function j(e,t){let n=a.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${a.sep}`)&&!a.isAbsolute(n)}function M(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return N(e,t)!==void 0}function N(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(a.basename(e))&&!h.test(a.basename(e))))return t(e)}function P(e,t){return a.relative(t,e)||a.basename(e)}function F(e){process.stdout.write(e)}function I(e){process.stderr.write(e)}function L(e){return e instanceof Error?e.message:String(e)}export{O as addCrossFileDuplication,k as collectDuplicatedLineNumbers,_ as configSearchDirectory,L as formatError,P as formatPath,N as getLanguage,M as isScannedPath,g as resolveTarget,y as scanListedFiles,v as scanTarget,I as writeStderr,F as writeStdout};
1
+ import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileData as i}from"./metrics.js";import a from"node:path";import{lstat as o,readFile as s,readdir as c,realpath as l,stat as u}from"node:fs/promises";import d from"node:os";const f=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?a.join(d.homedir(),e.slice(2)):a.resolve(e)}async function _(e){try{return(await u(e)).isDirectory()?e:a.dirname(e)}catch{return a.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],o=e;try{o=await l(e)}catch{}let s=a.dirname(o),c;try{c=await u(o)}catch(e){let t=`${F(o,s)}: ${R(e)}`;return{displayRoot:s,files:n,errors:[t],warnings:i,fatalError:t}}if(c.isFile()){let e=a.dirname(o),s=P(o,t,!0);if(!s){let t=`${F(o,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let c=x(t,n,r,i,e);try{await D(o,s,`single-file`,c,o)}catch(t){return b(t,e,n,r,i)}return{displayRoot:e,files:n,errors:r,warnings:i}}try{await w(o,x(t,n,r,i,o))}catch(e){return b(e,o,n,r,i)}return{displayRoot:o,files:n,errors:r,warnings:i}}async function y(e,t,n){let r=[],i=[],s=[],c=x(n,r,i,s,e);for(let l of t){let t=N(l,n)?P(l,n):void 0;if(!t)continue;let u=a.join(e,l);if(!(await o(u).catch(()=>{}))?.isSymbolicLink())try{await D(u,t,`directory`,c)}catch(t){return b(t,e,r,i,s)}}return{displayRoot:e,files:r,errors:i,warnings:s}}function b(e,t,r,i,a){if(!(e instanceof n))throw e;let o=R(e);return{displayRoot:t,files:r,errors:[...i,o],warnings:a,fatalError:o}}function x(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function S(e,t,n){try{return await e()}catch(e){n.errors.push(`${F(t,n.rootDirectory)}: ${R(e)}`);return}}async function C(e,t){let n=await S(()=>l(e),e,t);return n!==void 0&&M(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>c(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){let r=a.join(e,n.name);if(n.isSymbolicLink()){await T(n.name,r,t);continue}if(n.isDirectory()){if(j(n.name,t.options))continue;await w(r,t);continue}n.isFile()&&await E(r,t)}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>u(t),t,n);if(i!==void 0){if(i.isDirectory()){if(j(e,n.options)||j(a.basename(r),n.options))return;await w(t,n);return}i.isFile()&&await E(t,n,r,r)}}async function E(e,t,n=e,r){let i=P(n,t.options);i&&await D(e,i,`directory`,t,r)}async function D(e,t,i,a,o){try{let n=o??await l(e);if(a.visitedFiles.has(n))return;a.visitedFiles.add(n);let c=await s(e,`utf8`),u={language:t,duplication:a.options.duplication};if(i===`single-file`){a.files.push({file:e,metrics:r(c,u)});return}let{metrics:d,crossFileData:f,crossFileError:p}=O(c,u);p!==void 0&&a.warnings.push(`${F(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${p}`),a.files.push({file:e,metrics:d,duplicationCandidates:f})}catch(t){if(t instanceof n)throw t;a.errors.push(`${F(e,a.rootDirectory)}: ${R(t)}`)}}function O(e,t){try{return i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:R(i)}}}function k(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:F(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function A(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function j(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function M(e,t){let n=a.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${a.sep}`)&&!a.isAbsolute(n)}function N(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return P(e,t)!==void 0}function P(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(a.basename(e))&&!h.test(a.basename(e))))return t(e)}function F(e,t){return a.relative(t,e)||a.basename(e)}function I(e){process.stdout.write(e)}function L(e){process.stderr.write(e)}function R(e){return e instanceof Error?e.message:String(e)}export{k as addCrossFileDuplication,A as collectDuplicatedLineNumbers,_ as configSearchDirectory,R as formatError,F as formatPath,P as getLanguage,N as isScannedPath,O as measureWithCrossFileData,g as resolveTarget,y as scanListedFiles,v as scanTarget,L as writeStderr,I as writeStdout};
2
2
  //# sourceMappingURL=scan.js.map
package/dist/scan.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection is an auxiliary pass: if it fails where measureCode succeeded (an\n // addon error specific to this pass), that must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileData } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n try {\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n } catch (error) {\n return toFatalResult(error, displayRoot, files, errors, warnings);\n }\n return { displayRoot, files, errors, warnings };\n }\n\n try {\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n } catch (error) {\n return toFatalResult(error, canonicalTarget, files, errors, warnings);\n }\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n const context = makeScanContext(options, files, errors, warnings, rootDirectory);\n for (const relativePath of relativePaths) {\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n try {\n await measureFile(absolutePath, language, 'directory', context);\n } catch (error) {\n return toFatalResult(error, rootDirectory, files, errors, warnings);\n }\n }\n return { displayRoot: rootDirectory, files, errors, warnings };\n}\n\n/** A run-wide failure (a missing native addon) as a fatal result; anything else keeps throwing. */\nfunction toFatalResult(\n error: unknown,\n displayRoot: string,\n files: FileMetrics[],\n errors: string[],\n warnings: string[]\n): ScanResult {\n if (!(error instanceof NativeAddonError)) {\n throw error;\n }\n const fatalError = formatError(error);\n // Errors the walk accumulated before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n}\n\nfunction makeScanContext(\n options: ScanOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n context.errors.push(`${formatPath(target, context.rootDirectory)}: ${formatError(error)}`);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n context.files.push({ file, metrics: measureCode(code, measureOptions) });\n return;\n }\n const { metrics, crossFileData, crossFileError } = measureWithCrossFileData(code, measureOptions);\n if (crossFileError !== undefined) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`\n );\n }\n context.files.push({ file, metrics, duplicationCandidates: crossFileData });\n } catch (error) {\n // A missing native addon fails every file identically: propagate it once as a fatal scan\n // error instead of recording one \"skipped\" entry per file behind a successful exit code.\n if (error instanceof NativeAddonError) {\n throw error;\n }\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): { metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string } {\n try {\n return measureCodeWithCrossFileData(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface DuplicationOptions {
23
23
  * Minimum similarity percent (1-100) for near-miss (Type-3) clone blocks, measured as the
24
24
  * token-level longest common subsequence relative to the larger block (NiCad-style per-fragment
25
25
  * similarity). 100 disables near-miss detection and reports exact (Type-1/2) matches plus gapped
26
- * merges only (default 70). Applies to within-file detection only.
26
+ * merges only (default 70). Applies to within-file and cross-file detection.
27
27
  */
28
28
  minSimilarityPercent?: number;
29
29
  }
@@ -140,6 +140,16 @@ export interface CodeMetrics {
140
140
  bytes: number;
141
141
  lines: LineMetrics;
142
142
  functions: FunctionMetrics[];
143
+ /**
144
+ * The file's cyclomatic complexity as McCabe's v = e - n + 2p over its components: the sum of
145
+ * every function's value, plus 1 per initializer block (Java static/instance initializers, Kotlin
146
+ * `init`, JavaScript/TypeScript class `static` blocks), plus the decisions outside functions,
147
+ * plus 1 for the module body when
148
+ * the file runs top-level code (always for JavaScript, TypeScript, Python, and Ruby; for C# and
149
+ * Kotlin when the file has top-level statements, except a Kotlin file with parse errors, whose top
150
+ * level the grammar cannot be trusted to have read correctly).
151
+ */
152
+ cyclomaticComplexity: number;
143
153
  cognitiveComplexity: number;
144
154
  maxCognitiveComplexity: number;
145
155
  nestingDepth: number;