measure-code 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -13,6 +13,10 @@ A library for measuring code metrics with tree-sitter.
13
13
  - Cognitive complexity
14
14
  - Maximum per-function complexity
15
15
  - Nesting depth
16
+ - Intra-file call graph metrics, including call counts, fan-in/fan-out, recursion, and call depth
17
+ - File coupling metrics from imports and exports
18
+ - File cohesion metrics from shared function identifiers
19
+ - TypeScript type-shape metrics, including annotations, aliases, interfaces, generics, unions, intersections, assertions, and conditional types
16
20
  - Halstead metrics
17
21
  - Maintainability index
18
22
 
package/dist/cli.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var t=require("node:fs/promises"),e=require("node:os"),i=require("node:path"),n=require("commander"),o=require("./metrics.cjs");const r=new Map([[".cjs","javascript"],[".cts","typescript"],[".go","go"],[".js","javascript"],[".jsx","jsx"],[".mjs","javascript"],[".mts","typescript"],[".py","python"],[".ts","typescript"],[".tsx","tsx"]]),s=new Set([".git",".next",".tox",".tmp",".turbo",".venv",".yarn","__generated__","__pycache__","coverage","dist","generated","node_modules","vendor","venv"]),c=new Set(["__tests__","test","tests"]),a=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-]test)\.[^.]+$/iu;async function l(e,n,o,r,s,c,a){let f,d;try{f=await t.realpath(e)}catch(t){return void r.push(`${x(e,a)}: ${$(t)}`)}if(p(f,a)&&!s.has(f)){s.add(f);try{d=await t.readdir(e,{withFileTypes:!0})}catch(t){return void r.push(`${x(e,a)}: ${$(t)}`)}for(const t of d){const f=i.join(e,t.name);if(t.isSymbolicLink())await m(t.name,f,n,o,r,s,c,a);else if(t.isDirectory()){if(h(t.name,n))continue;await l(f,n,o,r,s,c,a)}else t.isFile()&&await u(f,n,o,r,c,a)}}}async function m(e,n,o,r,s,c,a,m){let f,d;try{f=await t.realpath(n)}catch(t){return void s.push(`${x(n,m)}: ${$(t)}`)}if(p(f,m)){try{d=await t.stat(n)}catch(t){return void s.push(`${x(n,m)}: ${$(t)}`)}if(d.isDirectory()){if(h(e,o)||h(i.basename(f),o))return;await l(n,o,r,s,c,a,m)}else d.isFile()&&await u(n,o,r,s,a,m,f,f)}}async function u(t,e,i,n,o,r,s=t,c){const a=y(s,e);a&&await f(t,a,i,n,o,r,c)}async function f(e,i,n,r,s,c,a){try{const r=a??await t.realpath(e);if(s.has(r))return;s.add(r);const c=await t.readFile(e,"utf8");n.push({file:e,metrics:o.measureCode(c,{language:i})})}catch(t){r.push(`${x(e,c)}: ${$(t)}`)}}function d(t){let e=0,i=0,n=0,o=0;for(const r of t)e+=r.metrics.functionCount,i+=r.metrics.lines.code,n=Math.max(n,r.metrics.maxCyclomaticComplexity),o=Math.max(o,r.metrics.maxCognitiveComplexity);return{fileCount:t.length,functionCount:e,linesOfCode:i,maxCyclomaticComplexity:n,maxCognitiveComplexity:o}}function h(t,e){return!!s.has(t)||!e.includeTests&&c.has(t)}function p(t,e){const n=i.relative(e,t);return""===n||".."!==n&&!n.startsWith(`..${i.sep}`)&&!i.isAbsolute(n)}function y(t,e,n=!1){const o=t.toLowerCase();if((n||!(o.endsWith(".d.ts")||o.endsWith(".d.mts")||o.endsWith(".d.cts")||o.endsWith(".min.js")))&&(n||e.includeTests||!a.test(i.basename(t))))return r.get(i.extname(o))}function g(t){if(!/^[1-9]\d*$/u.test(t))throw new n.InvalidArgumentError("Expected a positive integer.");const e=Number(t);if(!Number.isSafeInteger(e)||e<1)throw new n.InvalidArgumentError("Expected a positive integer.");return e}function x(t,e){return i.relative(e,t)||i.basename(t)}function v(t){process.stdout.write(t)}function C(t){process.stderr.write(t)}function $(t){return t instanceof Error?t.message:String(t)}(async function(){const o=(new n.Command).name("measure-code").description("Measure code metrics and list high-risk functions.").argument("[target]","file or directory to measure",".").option("--cyclomatic-threshold <number>","minimum cyclomatic complexity to report",g,10).option("--cognitive-threshold <number>","minimum cognitive complexity to report",g,15).option("--max-findings <number>","maximum number of risk findings to print",g,20).option("--include-tests","include test files and test directories").option("--json","print JSON output").option("--fail-on-error","exit with code 1 when files or directories cannot be scanned").option("--fail-on-risk","exit with code 1 when high-risk functions are found");o.action(async(n,o)=>{const r=function(t){if("~"===t)return e.homedir();if(t.startsWith("~/"))return i.join(e.homedir(),t.slice(2));return i.resolve(t)}(n),s=await async function(e,n){const o=[],r=[],s=new Set;let c=e;try{c=await t.realpath(e)}catch{}const a=i.dirname(c);let m;try{m=await t.stat(c)}catch(t){const e=`${x(c,a)}: ${$(t)}`;return{displayRoot:a,files:o,errors:[e],fatalError:e}}if(m.isFile()){const t=i.dirname(c),e=y(c,n,!0);if(!e){const e=`${x(c,t)}: unsupported file type`;return{displayRoot:t,files:o,errors:[e],fatalError:e}}return await f(c,e,o,r,s,t,c),{displayRoot:t,files:o,errors:r}}return await l(c,n,o,r,new Set,s,c),{displayRoot:c,files:o,errors:r}}(r,o),c=function(t,e,i){const n=t.flatMap(({file:t,metrics:n})=>n.functions.filter(t=>function(t,e){return t.cyclomaticComplexity>=e.cyclomaticThreshold||t.cognitiveComplexity>=e.cognitiveThreshold}(t,e)).map(o=>function(t,e,i,n,o){return{file:x(t,o),language:e,name:i.name??"<anonymous>",startLine:i.startLine,endLine:i.endLine,cyclomaticComplexity:i.cyclomaticComplexity,cognitiveComplexity:i.cognitiveComplexity,score:Math.max(i.cyclomaticComplexity/n.cyclomaticThreshold,i.cognitiveComplexity/n.cognitiveThreshold)}}(t,n.language,o,e,i)));return n.sort((t,e)=>e.score-t.score||e.cyclomaticComplexity-t.cyclomaticComplexity),n}(s.files,o,s.displayRoot);o.json?function(t,e,i){const n=d(t.files),o=e.slice(0,i.maxFindings);v(JSON.stringify({summary:n,thresholds:{cyclomaticComplexity:i.cyclomaticThreshold,cognitiveComplexity:i.cognitiveThreshold},totalRisks:e.length,truncated:o.length<e.length,risks:o,errors:t.errors},void 0,2)+"\n")}(s,c,o):function(t,e,i,n){if(e.fatalError)return void C(`Error: ${e.fatalError}\n`);const o=d(e.files);if(v(`Measured ${o.fileCount} files under ${t}\n`),v(`LOC ${o.linesOfCode}, functions ${o.functionCount}, max cyclomatic ${o.maxCyclomaticComplexity}, max cognitive ${o.maxCognitiveComplexity}\n`),v(`Risk thresholds: cyclomatic >= ${n.cyclomaticThreshold}, cognitive >= ${n.cognitiveThreshold}\n`),0===i.length)v("No high-risk functions found.\n");else{const t=i.slice(0,n.maxFindings),e=i.length>t.length?` of ${i.length}`:"";v(`\nHigh-risk functions (top ${t.length}${e}):\n`);for(const e of t)v(`${e.file}:${e.startLine}-${e.endLine} ${e.name} (cyclomatic ${e.cyclomaticComplexity}, cognitive ${e.cognitiveComplexity})\n`)}if(e.errors.length>0){C(`\nSkipped ${e.errors.length} files or directories:\n`);for(const t of e.errors.slice(0,10))C(`- ${t}\n`);e.errors.length>10&&C(`- ... ${e.errors.length-10} more\n`)}}(r,s,c,o),(s.fatalError||o.failOnError&&s.errors.length>0||o.failOnRisk&&c.length>0)&&(process.exitCode=1)}),await o.parseAsync()})().catch(t=>{C(`Error: ${$(t)}\n`),process.exitCode=1});
2
+ "use strict";var t=require("node:fs/promises"),e=require("node:os"),i=require("node:path"),n=require("commander"),o=require("./metrics.cjs");const r=new Map([[".cjs","javascript"],[".cts","typescript"],[".go","go"],[".js","javascript"],[".jsx","jsx"],[".mjs","javascript"],[".mts","typescript"],[".py","python"],[".ts","typescript"],[".tsx","tsx"]]),s=new Set([".git",".next",".tox",".tmp",".turbo",".venv",".yarn","__generated__","__pycache__","coverage","dist","generated","node_modules","vendor","venv"]),a=new Set(["__tests__","test","tests"]),c=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-]test)\.[^.]+$/iu;async function l(e,n,o,r,s,a,c){let p,f;try{p=await t.realpath(e)}catch(t){return void r.push(`${C(e,c)}: ${$(t)}`)}if(h(p,c)&&!s.has(p)){s.add(p);try{f=await t.readdir(e,{withFileTypes:!0})}catch(t){return void r.push(`${C(e,c)}: ${$(t)}`)}for(const t of f){const p=i.join(e,t.name);if(t.isSymbolicLink())await u(t.name,p,n,o,r,s,a,c);else if(t.isDirectory()){if(d(t.name,n))continue;await l(p,n,o,r,s,a,c)}else t.isFile()&&await m(p,n,o,r,a,c)}}}async function u(e,n,o,r,s,a,c,u){let p,f;try{p=await t.realpath(n)}catch(t){return void s.push(`${C(n,u)}: ${$(t)}`)}if(h(p,u)){try{f=await t.stat(n)}catch(t){return void s.push(`${C(n,u)}: ${$(t)}`)}if(f.isDirectory()){if(d(e,o)||d(i.basename(p),o))return;await l(n,o,r,s,a,c,u)}else f.isFile()&&await m(n,o,r,s,c,u,p,p)}}async function m(t,e,i,n,o,r,s=t,a){const c=y(s,e);c&&await p(t,c,i,n,o,r,a)}async function p(e,i,n,r,s,a,c){try{const r=c??await t.realpath(e);if(s.has(r))return;s.add(r);const a=await t.readFile(e,"utf8");n.push({file:e,metrics:o.measureCode(a,{language:i})})}catch(t){r.push(`${C(e,a)}: ${$(t)}`)}}function f(t){let e=0,i=0,n=0,o=0,r=0,s=0,a=0,c=0,l=0,u=0,m=0,p=0,f=0,d=0,h=0,y=0;for(const g of t)e+=g.metrics.functionCount,i+=g.metrics.lines.code,n=Math.max(n,g.metrics.maxCyclomaticComplexity),o=Math.max(o,g.metrics.maxCognitiveComplexity),r+=g.metrics.callGraph.callCount,s+=g.metrics.callGraph.internalCallCount,a=Math.max(a,g.metrics.callGraph.maxCallDepth),c+=g.metrics.coupling.importSourceCount,l+=g.metrics.coupling.relativeImportCount,u+=g.metrics.coupling.externalImportCount,m+=g.metrics.coupling.exportCount,p+=g.metrics.cohesion.averageFunctionIdentifierOverlap,f+=g.metrics.typeComplexity.typeAnnotationCount,d+=g.metrics.typeComplexity.typeAliasCount,h+=g.metrics.typeComplexity.interfaceCount,y+=g.metrics.typeComplexity.genericParameterCount;return{fileCount:t.length,functionCount:e,linesOfCode:i,maxCyclomaticComplexity:n,maxCognitiveComplexity:o,callCount:r,internalCallCount:s,maxCallDepth:a,importSourceCount:c,relativeImportCount:l,externalImportCount:u,exportCount:m,averageFunctionIdentifierOverlap:0===t.length?0:p/t.length,typeAnnotationCount:f,typeAliasCount:d,interfaceCount:h,genericParameterCount:y}}function d(t,e){return!!s.has(t)||!e.includeTests&&a.has(t)}function h(t,e){const n=i.relative(e,t);return""===n||".."!==n&&!n.startsWith(`..${i.sep}`)&&!i.isAbsolute(n)}function y(t,e,n=!1){const o=t.toLowerCase();if((n||!(o.endsWith(".d.ts")||o.endsWith(".d.mts")||o.endsWith(".d.cts")||o.endsWith(".min.js")))&&(n||e.includeTests||!c.test(i.basename(t))))return r.get(i.extname(o))}function g(t){if(!/^[1-9]\d*$/u.test(t))throw new n.InvalidArgumentError("Expected a positive integer.");const e=Number(t);if(!Number.isSafeInteger(e)||e<1)throw new n.InvalidArgumentError("Expected a positive integer.");return e}function C(t,e){return i.relative(e,t)||i.basename(t)}function x(t){process.stdout.write(t)}function v(t){process.stderr.write(t)}function $(t){return t instanceof Error?t.message:String(t)}(async function(){const o=(new n.Command).name("measure-code").description("Measure code metrics and list high-risk functions.").argument("[target]","file or directory to measure",".").option("--cyclomatic-threshold <number>","minimum cyclomatic complexity to report",g,10).option("--cognitive-threshold <number>","minimum cognitive complexity to report",g,15).option("--max-findings <number>","maximum number of risk findings to print",g,20).option("--include-tests","include test files and test directories").option("--json","print JSON output").option("--fail-on-error","exit with code 1 when files or directories cannot be scanned").option("--fail-on-risk","exit with code 1 when high-risk functions are found");o.action(async(n,o)=>{const r=function(t){if("~"===t)return e.homedir();if(t.startsWith("~/"))return i.join(e.homedir(),t.slice(2));return i.resolve(t)}(n),s=await async function(e,n){const o=[],r=[],s=new Set;let a=e;try{a=await t.realpath(e)}catch{}const c=i.dirname(a);let u;try{u=await t.stat(a)}catch(t){const e=`${C(a,c)}: ${$(t)}`;return{displayRoot:c,files:o,errors:[e],fatalError:e}}if(u.isFile()){const t=i.dirname(a),e=y(a,n,!0);if(!e){const e=`${C(a,t)}: unsupported file type`;return{displayRoot:t,files:o,errors:[e],fatalError:e}}return await p(a,e,o,r,s,t,a),{displayRoot:t,files:o,errors:r}}return await l(a,n,o,r,new Set,s,a),{displayRoot:a,files:o,errors:r}}(r,o),a=function(t,e,i){const n=t.flatMap(({file:t,metrics:n})=>n.functions.filter(t=>function(t,e){return t.cyclomaticComplexity>=e.cyclomaticThreshold||t.cognitiveComplexity>=e.cognitiveThreshold}(t,e)).map(o=>function(t,e,i,n,o){return{file:C(t,o),language:e,name:i.name??"<anonymous>",startLine:i.startLine,endLine:i.endLine,cyclomaticComplexity:i.cyclomaticComplexity,cognitiveComplexity:i.cognitiveComplexity,score:Math.max(i.cyclomaticComplexity/n.cyclomaticThreshold,i.cognitiveComplexity/n.cognitiveThreshold)}}(t,n.language,o,e,i)));return n.sort((t,e)=>e.score-t.score||e.cyclomaticComplexity-t.cyclomaticComplexity),n}(s.files,o,s.displayRoot);o.json?function(t,e,i){const n=f(t.files),o=e.slice(0,i.maxFindings);x(JSON.stringify({summary:n,thresholds:{cyclomaticComplexity:i.cyclomaticThreshold,cognitiveComplexity:i.cognitiveThreshold},totalRisks:e.length,truncated:o.length<e.length,risks:o,errors:t.errors},void 0,2)+"\n")}(s,a,o):function(t,e,i,n){if(e.fatalError)return void v(`Error: ${e.fatalError}\n`);const o=f(e.files);if(x(`Measured ${o.fileCount} files under ${t}\n`),x(`LOC ${o.linesOfCode}, functions ${o.functionCount}, max cyclomatic ${o.maxCyclomaticComplexity}, max cognitive ${o.maxCognitiveComplexity}\n`),x(`Calls ${o.callCount}, internal edges ${o.internalCallCount}, max call depth ${o.maxCallDepth}, imports ${o.importSourceCount}, exports ${o.exportCount}\n`),x(`Type annotations ${o.typeAnnotationCount}, type aliases ${o.typeAliasCount}, interfaces ${o.interfaceCount}, avg cohesion ${o.averageFunctionIdentifierOverlap.toFixed(2)}\n`),x(`Risk thresholds: cyclomatic >= ${n.cyclomaticThreshold}, cognitive >= ${n.cognitiveThreshold}\n`),0===i.length)x("No high-risk functions found.\n");else{const t=i.slice(0,n.maxFindings),e=i.length>t.length?` of ${i.length}`:"";x(`\nHigh-risk functions (top ${t.length}${e}):\n`);for(const e of t)x(`${e.file}:${e.startLine}-${e.endLine} ${e.name} (cyclomatic ${e.cyclomaticComplexity}, cognitive ${e.cognitiveComplexity})\n`)}if(e.errors.length>0){v(`\nSkipped ${e.errors.length} files or directories:\n`);for(const t of e.errors.slice(0,10))v(`- ${t}\n`);e.errors.length>10&&v(`- ... ${e.errors.length-10} more\n`)}}(r,s,a,o),(s.fatalError||o.failOnError&&s.errors.length>0||o.failOnRisk&&a.length>0)&&(process.exitCode=1)}),await o.parseAsync()})().catch(t=>{v(`Error: ${$(t)}\n`),process.exitCode=1});
3
3
  //# sourceMappingURL=cli.cjs.map
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface CliOptions {\n cognitiveThreshold: number;\n cyclomaticThreshold: number;\n failOnError?: boolean;\n failOnRisk?: boolean;\n includeTests?: boolean;\n json?: boolean;\n maxFindings: number;\n}\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine: number;\n file: string;\n language: LanguageName;\n name: string;\n score: number;\n startLine: number;\n}\n\ninterface ScanResult {\n displayRoot: string;\n errors: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.cjs', 'javascript'],\n ['.cts', 'typescript'],\n ['.go', 'go'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.git',\n '.next',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'generated',\n 'node_modules',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-]test)\\.[^.]+$/iu;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('measure-code')\n .description('Measure code metrics and list high-risk functions.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger, 10)\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger, 15)\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger, 20)\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk functions are found');\n\n program.action(async (target: string, options: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const result = await scanTarget(resolvedTarget, options);\n const risks = findRiskyFunctions(result.files, options, result.displayRoot);\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction 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\nasync function scanTarget(target: string, options: CliOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const visitedFiles = new Set<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], 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], fatalError };\n }\n\n await measureFile(canonicalTarget, language, files, errors, visitedFiles, displayRoot, canonicalTarget);\n return { displayRoot, files, errors };\n }\n\n await scanDirectory(canonicalTarget, options, files, errors, new Set(), visitedFiles, canonicalTarget);\n return { displayRoot: canonicalTarget, files, errors };\n}\n\nasync function scanDirectory(\n directory: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, rootDirectory)) {\n return;\n }\n\n if (visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\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(\n entry.name,\n entryPath,\n options,\n files,\n errors,\n visitedDirectories,\n visitedFiles,\n rootDirectory\n );\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, options)) {\n continue;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, options, files, errors, visitedFiles, rootDirectory);\n }\n }\n}\n\nasync function scanSymbolicLink(\n name: string,\n entryPath: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (shouldSkipDirectory(name, options) || shouldSkipDirectory(path.basename(resolvedPath), options)) {\n return;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(\n entryPath,\n options,\n files,\n errors,\n visitedFiles,\n rootDirectory,\n resolvedPath,\n resolvedPath\n );\n }\n}\n\nasync function measureScannableFile(\n file: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, options);\n if (language) {\n await measureFile(file, language, files, errors, visitedFiles, displayRoot, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (visitedFiles.has(resolvedFile)) {\n return;\n }\n visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n files.push({\n file,\n metrics: measureCode(code, { language }),\n });\n } catch (error) {\n errors.push(`${formatPath(file, displayRoot)}: ${formatError(error)}`);\n }\n}\n\nfunction findRiskyFunctions(files: FileMetrics[], options: CliOptions, displayRoot: string): RiskFinding[] {\n const findings = files.flatMap(({ file, metrics }) =>\n metrics.functions\n .filter((fn) => isRiskyFunction(fn, options))\n .map((fn) => createRiskFinding(file, metrics.language, fn, options, displayRoot))\n );\n\n findings.sort((left, right) => right.score - left.score || right.cyclomaticComplexity - left.cyclomaticComplexity);\n return findings;\n}\n\nfunction isRiskyFunction(fn: FunctionMetrics, options: CliOptions): boolean {\n return fn.cyclomaticComplexity >= options.cyclomaticThreshold || fn.cognitiveComplexity >= options.cognitiveThreshold;\n}\n\nfunction createRiskFinding(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n options: CliOptions,\n displayRoot: string\n): RiskFinding {\n return {\n file: formatPath(file, displayRoot),\n language,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n score: Math.max(\n fn.cyclomaticComplexity / options.cyclomaticThreshold,\n fn.cognitiveComplexity / options.cognitiveThreshold\n ),\n };\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: {\n cyclomaticComplexity: options.cyclomaticThreshold,\n cognitiveComplexity: options.cognitiveThreshold,\n },\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n risks: reportedRisks,\n errors: result.errors,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Risk thresholds: cyclomatic >= ${options.cyclomaticThreshold}, cognitive >= ${options.cognitiveThreshold}\\n`\n );\n\n if (risks.length === 0) {\n writeStdout('No high-risk functions found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk functions (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(\n `${risk.file}:${risk.startLine}-${risk.endLine} ${risk.name} ` +\n `(cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})\\n`\n );\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: CliOptions): 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\nfunction getLanguage(file: string, options: CliOptions, 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 ) {\n return undefined;\n }\n\n if (!explicitTarget && !options.includeTests && testFilePattern.test(path.basename(file))) {\n return undefined;\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["languageByExtension","Map","ignoredDirectoryNames","Set","testDirectoryNames","testFilePattern","async","scanDirectory","directory","options","files","errors","visitedDirectories","visitedFiles","rootDirectory","resolvedDirectory","entries","realpath","error","push","formatPath","formatError","isWithinDirectory","has","add","readdir","withFileTypes","entry","entryPath","path","join","name","isSymbolicLink","scanSymbolicLink","isDirectory","shouldSkipDirectory","isFile","measureScannableFile","resolvedPath","entryStat","stat","basename","file","displayRoot","languageFile","realFile","language","getLanguage","measureFile","resolvedFile","code","readFile","metrics","measureCode","summarize","functionCount","linesOfCode","maxCyclomaticComplexity","maxCognitiveComplexity","lines","Math","max","fileCount","length","includeTests","candidate","relative","startsWith","sep","isAbsolute","explicitTarget","lowerFile","toLowerCase","endsWith","test","get","extname","parsePositiveInteger","value","InvalidArgumentError","parsed","Number","isSafeInteger","base","writeStdout","message","process","stdout","write","writeStderr","stderr","Error","String","program","Command","description","argument","option","action","target","resolvedTarget","os","homedir","slice","resolve","resolveTarget","result","canonicalTarget","fallbackDisplayRoot","dirname","targetStat","fatalError","scanTarget","risks","findings","flatMap","functions","filter","fn","cyclomaticComplexity","cyclomaticThreshold","cognitiveComplexity","cognitiveThreshold","isRiskyFunction","map","startLine","endLine","score","createRiskFinding","sort","left","right","findRiskyFunctions","json","summary","reportedRisks","maxFindings","JSON","stringify","thresholds","totalRisks","truncated","undefined","printJson","totalSuffix","risk","printTextReport","failOnError","failOnRisk","exitCode","parseAsync","main","catch"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface CliOptions {\n cognitiveThreshold: number;\n cyclomaticThreshold: number;\n failOnError?: boolean;\n failOnRisk?: boolean;\n includeTests?: boolean;\n json?: boolean;\n maxFindings: number;\n}\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine: number;\n file: string;\n language: LanguageName;\n name: string;\n score: number;\n startLine: number;\n}\n\ninterface ScanResult {\n displayRoot: string;\n errors: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.cjs', 'javascript'],\n ['.cts', 'typescript'],\n ['.go', 'go'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.git',\n '.next',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'generated',\n 'node_modules',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-]test)\\.[^.]+$/iu;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('measure-code')\n .description('Measure code metrics and list high-risk functions.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger, 10)\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger, 15)\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger, 20)\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk functions are found');\n\n program.action(async (target: string, options: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const result = await scanTarget(resolvedTarget, options);\n const risks = findRiskyFunctions(result.files, options, result.displayRoot);\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction 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\nasync function scanTarget(target: string, options: CliOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const visitedFiles = new Set<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], 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], fatalError };\n }\n\n await measureFile(canonicalTarget, language, files, errors, visitedFiles, displayRoot, canonicalTarget);\n return { displayRoot, files, errors };\n }\n\n await scanDirectory(canonicalTarget, options, files, errors, new Set(), visitedFiles, canonicalTarget);\n return { displayRoot: canonicalTarget, files, errors };\n}\n\nasync function scanDirectory(\n directory: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, rootDirectory)) {\n return;\n }\n\n if (visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\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(\n entry.name,\n entryPath,\n options,\n files,\n errors,\n visitedDirectories,\n visitedFiles,\n rootDirectory\n );\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, options)) {\n continue;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, options, files, errors, visitedFiles, rootDirectory);\n }\n }\n}\n\nasync function scanSymbolicLink(\n name: string,\n entryPath: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (shouldSkipDirectory(name, options) || shouldSkipDirectory(path.basename(resolvedPath), options)) {\n return;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(\n entryPath,\n options,\n files,\n errors,\n visitedFiles,\n rootDirectory,\n resolvedPath,\n resolvedPath\n );\n }\n}\n\nasync function measureScannableFile(\n file: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, options);\n if (language) {\n await measureFile(file, language, files, errors, visitedFiles, displayRoot, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (visitedFiles.has(resolvedFile)) {\n return;\n }\n visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n files.push({\n file,\n metrics: measureCode(code, { language }),\n });\n } catch (error) {\n errors.push(`${formatPath(file, displayRoot)}: ${formatError(error)}`);\n }\n}\n\nfunction findRiskyFunctions(files: FileMetrics[], options: CliOptions, displayRoot: string): RiskFinding[] {\n const findings = files.flatMap(({ file, metrics }) =>\n metrics.functions\n .filter((fn) => isRiskyFunction(fn, options))\n .map((fn) => createRiskFinding(file, metrics.language, fn, options, displayRoot))\n );\n\n findings.sort((left, right) => right.score - left.score || right.cyclomaticComplexity - left.cyclomaticComplexity);\n return findings;\n}\n\nfunction isRiskyFunction(fn: FunctionMetrics, options: CliOptions): boolean {\n return fn.cyclomaticComplexity >= options.cyclomaticThreshold || fn.cognitiveComplexity >= options.cognitiveThreshold;\n}\n\nfunction createRiskFinding(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n options: CliOptions,\n displayRoot: string\n): RiskFinding {\n return {\n file: formatPath(file, displayRoot),\n language,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n score: Math.max(\n fn.cyclomaticComplexity / options.cyclomaticThreshold,\n fn.cognitiveComplexity / options.cognitiveThreshold\n ),\n };\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: {\n cyclomaticComplexity: options.cyclomaticThreshold,\n cognitiveComplexity: options.cognitiveThreshold,\n },\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n risks: reportedRisks,\n errors: result.errors,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Calls ${summary.callCount}, internal edges ${summary.internalCallCount}, max call depth ${summary.maxCallDepth}, imports ${summary.importSourceCount}, exports ${summary.exportCount}\\n`\n );\n writeStdout(\n `Type annotations ${summary.typeAnnotationCount}, type aliases ${summary.typeAliasCount}, interfaces ${summary.interfaceCount}, avg cohesion ${summary.averageFunctionIdentifierOverlap.toFixed(2)}\\n`\n );\n writeStdout(\n `Risk thresholds: cyclomatic >= ${options.cyclomaticThreshold}, cognitive >= ${options.cognitiveThreshold}\\n`\n );\n\n if (risks.length === 0) {\n writeStdout('No high-risk functions found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk functions (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(\n `${risk.file}:${risk.startLine}-${risk.endLine} ${risk.name} ` +\n `(cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})\\n`\n );\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n callCount: number;\n internalCallCount: number;\n maxCallDepth: number;\n importSourceCount: number;\n relativeImportCount: number;\n externalImportCount: number;\n exportCount: number;\n averageFunctionIdentifierOverlap: number;\n typeAnnotationCount: number;\n typeAliasCount: number;\n interfaceCount: number;\n genericParameterCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n let callCount = 0;\n let internalCallCount = 0;\n let maxCallDepth = 0;\n let importSourceCount = 0;\n let relativeImportCount = 0;\n let externalImportCount = 0;\n let exportCount = 0;\n let cohesionTotal = 0;\n let typeAnnotationCount = 0;\n let typeAliasCount = 0;\n let interfaceCount = 0;\n let genericParameterCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n callCount += file.metrics.callGraph.callCount;\n internalCallCount += file.metrics.callGraph.internalCallCount;\n maxCallDepth = Math.max(maxCallDepth, file.metrics.callGraph.maxCallDepth);\n importSourceCount += file.metrics.coupling.importSourceCount;\n relativeImportCount += file.metrics.coupling.relativeImportCount;\n externalImportCount += file.metrics.coupling.externalImportCount;\n exportCount += file.metrics.coupling.exportCount;\n cohesionTotal += file.metrics.cohesion.averageFunctionIdentifierOverlap;\n typeAnnotationCount += file.metrics.typeComplexity.typeAnnotationCount;\n typeAliasCount += file.metrics.typeComplexity.typeAliasCount;\n interfaceCount += file.metrics.typeComplexity.interfaceCount;\n genericParameterCount += file.metrics.typeComplexity.genericParameterCount;\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n callCount,\n internalCallCount,\n maxCallDepth,\n importSourceCount,\n relativeImportCount,\n externalImportCount,\n exportCount,\n averageFunctionIdentifierOverlap: files.length === 0 ? 0 : cohesionTotal / files.length,\n typeAnnotationCount,\n typeAliasCount,\n interfaceCount,\n genericParameterCount,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: CliOptions): 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\nfunction getLanguage(file: string, options: CliOptions, 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 ) {\n return undefined;\n }\n\n if (!explicitTarget && !options.includeTests && testFilePattern.test(path.basename(file))) {\n return undefined;\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["languageByExtension","Map","ignoredDirectoryNames","Set","testDirectoryNames","testFilePattern","async","scanDirectory","directory","options","files","errors","visitedDirectories","visitedFiles","rootDirectory","resolvedDirectory","entries","realpath","error","push","formatPath","formatError","isWithinDirectory","has","add","readdir","withFileTypes","entry","entryPath","path","join","name","isSymbolicLink","scanSymbolicLink","isDirectory","shouldSkipDirectory","isFile","measureScannableFile","resolvedPath","entryStat","stat","basename","file","displayRoot","languageFile","realFile","language","getLanguage","measureFile","resolvedFile","code","readFile","metrics","measureCode","summarize","functionCount","linesOfCode","maxCyclomaticComplexity","maxCognitiveComplexity","callCount","internalCallCount","maxCallDepth","importSourceCount","relativeImportCount","externalImportCount","exportCount","cohesionTotal","typeAnnotationCount","typeAliasCount","interfaceCount","genericParameterCount","lines","Math","max","callGraph","coupling","cohesion","averageFunctionIdentifierOverlap","typeComplexity","fileCount","length","includeTests","candidate","relative","startsWith","sep","isAbsolute","explicitTarget","lowerFile","toLowerCase","endsWith","test","get","extname","parsePositiveInteger","value","InvalidArgumentError","parsed","Number","isSafeInteger","base","writeStdout","message","process","stdout","write","writeStderr","stderr","Error","String","program","Command","description","argument","option","action","target","resolvedTarget","os","homedir","slice","resolve","resolveTarget","result","canonicalTarget","fallbackDisplayRoot","dirname","targetStat","fatalError","scanTarget","risks","findings","flatMap","functions","filter","fn","cyclomaticComplexity","cyclomaticThreshold","cognitiveComplexity","cognitiveThreshold","isRiskyFunction","map","startLine","endLine","score","createRiskFinding","sort","left","right","findRiskyFunctions","json","summary","reportedRisks","maxFindings","JSON","stringify","thresholds","totalRisks","truncated","undefined","printJson","toFixed","totalSuffix","risk","printTextReport","failOnError","failOnRisk","exitCode","parseAsync","main","catch"],"mappings":";6IA0CA,MAAMA,EAAsB,IAAIC,IAA0B,CACxD,CAAC,OAAQ,cACT,CAAC,OAAQ,cACT,CAAC,MAAO,MACR,CAAC,MAAO,cACR,CAAC,OAAQ,OACT,CAAC,OAAQ,cACT,CAAC,OAAQ,cACT,CAAC,MAAO,UACR,CAAC,MAAO,cACR,CAAC,OAAQ,SAGLC,EAAwB,IAAIC,IAAI,CACpC,OACA,QACA,OACA,OACA,SACA,QACA,QACA,gBACA,cACA,WACA,OACA,YACA,eACA,SACA,SAGIC,EAAqB,IAAID,IAAI,CAAC,YAAa,OAAQ,UACnDE,EAAkB,0DA6FxBC,eAAeC,EACbC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,GAEA,IAAIC,EAiBAC,EAhBJ,IACED,QAA0BE,EAAAA,SAAST,EACrC,CAAE,MAAOU,GAEP,YADAP,EAAOQ,KAAK,GAAGC,EAAWZ,EAAWM,OAAmBO,EAAYH,KAEtE,CAEA,GAAKI,EAAkBP,EAAmBD,KAItCF,EAAmBW,IAAIR,GAA3B,CAGAH,EAAmBY,IAAIT,GAGvB,IACEC,QAAgBS,EAAAA,QAAQjB,EAAW,CAAEkB,eAAe,GACtD,CAAE,MAAOR,GAEP,YADAP,EAAOQ,KAAK,GAAGC,EAAWZ,EAAWM,OAAmBO,EAAYH,KAEtE,CAEA,IAAK,MAAMS,KAASX,EAAS,CAC3B,MAAMY,EAAYC,EAAKC,KAAKtB,EAAWmB,EAAMI,MAC7C,GAAIJ,EAAMK,uBACFC,EACJN,EAAMI,KACNH,EACAnB,EACAC,EACAC,EACAC,EACAC,EACAC,QAKJ,GAAIa,EAAMO,cAAV,CACE,GAAIC,EAAoBR,EAAMI,KAAMtB,GAClC,eAEIF,EAAcqB,EAAWnB,EAASC,EAAOC,EAAQC,EAAoBC,EAAcC,EAE3F,MAEIa,EAAMS,gBACFC,EAAqBT,EAAWnB,EAASC,EAAOC,EAAQE,EAAcC,EAEhF,CAtCA,CAuCF,CAEAR,eAAe2B,EACbF,EACAH,EACAnB,EACAC,EACAC,EACAC,EACAC,EACAC,GAEA,IAAIwB,EAYAC,EAXJ,IACED,QAAqBrB,EAAAA,SAASW,EAChC,CAAE,MAAOV,GAEP,YADAP,EAAOQ,KAAK,GAAGC,EAAWQ,EAAWd,OAAmBO,EAAYH,KAEtE,CAEA,GAAKI,EAAkBgB,EAAcxB,GAArC,CAKA,IACEyB,QAAkBC,EAAAA,KAAKZ,EACzB,CAAE,MAAOV,GAEP,YADAP,EAAOQ,KAAK,GAAGC,EAAWQ,EAAWd,OAAmBO,EAAYH,KAEtE,CAEA,GAAIqB,EAAUL,cAAd,CACE,GAAIC,EAAoBJ,EAAMtB,IAAY0B,EAAoBN,EAAKY,SAASH,GAAe7B,GACzF,aAEIF,EAAcqB,EAAWnB,EAASC,EAAOC,EAAQC,EAAoBC,EAAcC,EAE3F,MAEIyB,EAAUH,gBACNC,EACJT,EACAnB,EACAC,EACAC,EACAE,EACAC,EACAwB,EACAA,EA3BJ,CA8BF,CAEAhC,eAAe+B,EACbK,EACAjC,EACAC,EACAC,EACAE,EACA8B,EACAC,EAAeF,EACfG,GAEA,MAAMC,EAAWC,EAAYH,EAAcnC,GACvCqC,SACIE,EAAYN,EAAMI,EAAUpC,EAAOC,EAAQE,EAAc8B,EAAaE,EAEhF,CAEAvC,eAAe0C,EACbN,EACAI,EACApC,EACAC,EACAE,EACA8B,EACAE,GAEA,IACE,MAAMI,EAAeJ,SAAmB5B,EAAAA,SAASyB,GACjD,GAAI7B,EAAaU,IAAI0B,GACnB,OAEFpC,EAAaW,IAAIyB,GAEjB,MAAMC,QAAaC,WAAST,EAAM,QAClChC,EAAMS,KAAK,CACTuB,OACAU,QAASC,EAAAA,YAAYH,EAAM,CAAEJ,cAEjC,CAAE,MAAO5B,GACPP,EAAOQ,KAAK,GAAGC,EAAWsB,EAAMC,OAAiBtB,EAAYH,KAC/D,CACF,CA2GA,SAASoC,EAAU5C,GAmBjB,IAAI6C,EAAgB,EAChBC,EAAc,EACdC,EAA0B,EAC1BC,EAAyB,EACzBC,EAAY,EACZC,EAAoB,EACpBC,EAAe,EACfC,EAAoB,EACpBC,EAAsB,EACtBC,EAAsB,EACtBC,EAAc,EACdC,EAAgB,EAChBC,EAAsB,EACtBC,EAAiB,EACjBC,EAAiB,EACjBC,EAAwB,EAE5B,IAAK,MAAM5B,KAAQhC,EACjB6C,GAAiBb,EAAKU,QAAQG,cAC9BC,GAAed,EAAKU,QAAQmB,MAAMrB,KAClCO,EAA0Be,KAAKC,IAAIhB,EAAyBf,EAAKU,QAAQK,yBACzEC,EAAyBc,KAAKC,IAAIf,EAAwBhB,EAAKU,QAAQM,wBACvEC,GAAajB,EAAKU,QAAQsB,UAAUf,UACpCC,GAAqBlB,EAAKU,QAAQsB,UAAUd,kBAC5CC,EAAeW,KAAKC,IAAIZ,EAAcnB,EAAKU,QAAQsB,UAAUb,cAC7DC,GAAqBpB,EAAKU,QAAQuB,SAASb,kBAC3CC,GAAuBrB,EAAKU,QAAQuB,SAASZ,oBAC7CC,GAAuBtB,EAAKU,QAAQuB,SAASX,oBAC7CC,GAAevB,EAAKU,QAAQuB,SAASV,YACrCC,GAAiBxB,EAAKU,QAAQwB,SAASC,iCACvCV,GAAuBzB,EAAKU,QAAQ0B,eAAeX,oBACnDC,GAAkB1B,EAAKU,QAAQ0B,eAAeV,eAC9CC,GAAkB3B,EAAKU,QAAQ0B,eAAeT,eAC9CC,GAAyB5B,EAAKU,QAAQ0B,eAAeR,sBAGvD,MAAO,CACLS,UAAWrE,EAAMsE,OACjBzB,gBACAC,cACAC,0BACAC,yBACAC,YACAC,oBACAC,eACAC,oBACAC,sBACAC,sBACAC,cACAY,iCAAmD,IAAjBnE,EAAMsE,OAAe,EAAId,EAAgBxD,EAAMsE,OACjFb,sBACAC,iBACAC,iBACAC,wBAEJ,CAEA,SAASnC,EAAoBJ,EAActB,GACzC,QAAIP,EAAsBqB,IAAIQ,KAI1BtB,EAAQwE,cAIL7E,EAAmBmB,IAAIQ,EAChC,CAEA,SAAST,EAAkB4D,EAAmB1E,GAC5C,MAAM2E,EAAWtD,EAAKsD,SAAS3E,EAAW0E,GAC1C,MAAoB,KAAbC,GAAiC,OAAbA,IAAsBA,EAASC,WAAW,KAAKvD,EAAKwD,SAAWxD,EAAKyD,WAAWH,EAC5G,CAEA,SAASpC,EAAYL,EAAcjC,EAAqB8E,GAAiB,GACvE,MAAMC,EAAY9C,EAAK+C,cACvB,IACGF,KACAC,EAAUE,SAAS,UAClBF,EAAUE,SAAS,WACnBF,EAAUE,SAAS,WACnBF,EAAUE,SAAS,eAKlBH,GAAmB9E,EAAQwE,eAAgB5E,EAAgBsF,KAAK9D,EAAKY,SAASC,KAInF,OAAO1C,EAAoB4F,IAAI/D,EAAKgE,QAAQL,GAC9C,CAEA,SAASM,EAAqBC,GAC5B,IAAK,cAAcJ,KAAKI,GACtB,MAAM,IAAIC,EAAAA,qBAAqB,gCAGjC,MAAMC,EAASC,OAAOH,GACtB,IAAKG,OAAOC,cAAcF,IAAWA,EAAS,EAC5C,MAAM,IAAID,EAAAA,qBAAqB,gCAEjC,OAAOC,CACT,CAEA,SAAS7E,EAAWsB,EAAc0D,GAChC,OAAOvE,EAAKsD,SAASiB,EAAM1D,IAASb,EAAKY,SAASC,EACpD,CAEA,SAAS2D,EAAYC,GACnBC,QAAQC,OAAOC,MAAMH,EACvB,CAEA,SAASI,EAAYJ,GACnBC,QAAQI,OAAOF,MAAMH,EACvB,CAEA,SAASjF,EAAYH,GACnB,OAAOA,aAAiB0F,MAAQ1F,EAAMoF,QAAUO,OAAO3F,EACzD,EAteAZ,iBACE,MAAMwG,GAAU,IAAIC,EAAAA,SACjBhF,KAAK,gBACLiF,YAAY,sDACZC,SAAS,WAAY,+BAAgC,KACrDC,OAAO,kCAAmC,0CAA2CpB,EAAsB,IAC3GoB,OAAO,iCAAkC,yCAA0CpB,EAAsB,IACzGoB,OAAO,0BAA2B,2CAA4CpB,EAAsB,IACpGoB,OAAO,kBAAmB,2CAC1BA,OAAO,SAAU,qBACjBA,OAAO,kBAAmB,gEAC1BA,OAAO,iBAAkB,uDAE5BJ,EAAQK,OAAO7G,MAAO8G,EAAgB3G,KACpC,MAAM4G,EAsBV,SAAuBD,GACrB,GAAe,MAAXA,EACF,OAAOE,EAAGC,UAGZ,GAAIH,EAAOhC,WAAW,MACpB,OAAOvD,EAAKC,KAAKwF,EAAGC,UAAWH,EAAOI,MAAM,IAG9C,OAAO3F,EAAK4F,QAAQL,EACtB,CAhC2BM,CAAcN,GAC/BO,QAiCVrH,eAA0B8G,EAAgB3G,GACxC,MAAMC,EAAuB,GACvBC,EAAmB,GACnBE,EAAe,IAAIV,IACzB,IAAIyH,EAAkBR,EACtB,IACEQ,QAAwB3G,EAAAA,SAASmG,EACnC,CAAE,MACA,CAGF,MAAMS,EAAsBhG,EAAKiG,QAAQF,GACzC,IAAIG,EAEJ,IACEA,QAAmBvF,EAAAA,KAAKoF,EAC1B,CAAE,MAAO1G,GACP,MAAM8G,EAAa,GAAG5G,EAAWwG,EAAiBC,OAAyBxG,EAAYH,KACvF,MAAO,CAAEyB,YAAakF,EAAqBnH,QAAOC,OAAQ,CAACqH,GAAaA,aAC1E,CAEA,GAAID,EAAW3F,SAAU,CACvB,MAAMO,EAAcd,EAAKiG,QAAQF,GAC3B9E,EAAWC,EAAY6E,EAAiBnH,GAAS,GACvD,IAAKqC,EAAU,CACb,MAAMkF,EAAa,GAAG5G,EAAWwG,EAAiBjF,4BAClD,MAAO,CAAEA,cAAajC,QAAOC,OAAQ,CAACqH,GAAaA,aACrD,CAGA,aADMhF,EAAY4E,EAAiB9E,EAAUpC,EAAOC,EAAQE,EAAc8B,EAAaiF,GAChF,CAAEjF,cAAajC,QAAOC,SAC/B,CAGA,aADMJ,EAAcqH,EAAiBnH,EAASC,EAAOC,EAAQ,IAAIR,IAAOU,EAAc+G,GAC/E,CAAEjF,YAAaiF,EAAiBlH,QAAOC,SAChD,CApEyBsH,CAAWZ,EAAgB5G,GAC1CyH,EAmOV,SAA4BxH,EAAsBD,EAAqBkC,GACrE,MAAMwF,EAAWzH,EAAM0H,QAAQ,EAAG1F,OAAMU,aACtCA,EAAQiF,UACLC,OAAQC,GAQf,SAAyBA,EAAqB9H,GAC5C,OAAO8H,EAAGC,sBAAwB/H,EAAQgI,qBAAuBF,EAAGG,qBAAuBjI,EAAQkI,kBACrG,CAVsBC,CAAgBL,EAAI9H,IACnCoI,IAAKN,GAWZ,SACE7F,EACAI,EACAyF,EACA9H,EACAkC,GAEA,MAAO,CACLD,KAAMtB,EAAWsB,EAAMC,GACvBG,WACAf,KAAMwG,EAAGxG,MAAQ,cACjB+G,UAAWP,EAAGO,UACdC,QAASR,EAAGQ,QACZP,qBAAsBD,EAAGC,qBACzBE,oBAAqBH,EAAGG,oBACxBM,MAAOxE,KAAKC,IACV8D,EAAGC,qBAAuB/H,EAAQgI,oBAClCF,EAAGG,oBAAsBjI,EAAQkI,oBAGvC,CA/BmBM,CAAkBvG,EAAMU,EAAQN,SAAUyF,EAAI9H,EAASkC,KAIxE,OADAwF,EAASe,KAAK,CAACC,EAAMC,IAAUA,EAAMJ,MAAQG,EAAKH,OAASI,EAAMZ,qBAAuBW,EAAKX,sBACtFL,CACT,CA5OkBkB,CAAmB1B,EAAOjH,MAAOD,EAASkH,EAAOhF,aAE3DlC,EAAQ6I,KAsQhB,SAAmB3B,EAAoBO,EAAsBzH,GAC3D,MAAM8I,EAAUjG,EAAUqE,EAAOjH,OAC3B8I,EAAgBtB,EAAMV,MAAM,EAAG/G,EAAQgJ,aAC7CpD,EACEqD,KAAKC,UACH,CACEJ,UACAK,WAAY,CACVpB,qBAAsB/H,EAAQgI,oBAC9BC,oBAAqBjI,EAAQkI,oBAE/BkB,WAAY3B,EAAMlD,OAClB8E,UAAWN,EAAcxE,OAASkD,EAAMlD,OACxCkD,MAAOsB,EACP7I,OAAQgH,EAAOhH,aAEjBoJ,EACA,GACE,KAER,CAzRMC,CAAUrC,EAAQO,EAAOzH,GA2R/B,SAAyB2G,EAAgBO,EAAoBO,EAAsBzH,GACjF,GAAIkH,EAAOK,WAET,YADAtB,EAAY,UAAUiB,EAAOK,gBAI/B,MAAMuB,EAAUjG,EAAUqE,EAAOjH,OAejC,GAdA2F,EAAY,YAAYkD,EAAQxE,yBAAyBqC,OACzDf,EACE,OAAOkD,EAAQ/F,0BAA0B+F,EAAQhG,iCAAiCgG,EAAQ9F,0CAA0C8F,EAAQ7F,4BAE9I2C,EACE,SAASkD,EAAQ5F,6BAA6B4F,EAAQ3F,qCAAqC2F,EAAQ1F,yBAAyB0F,EAAQzF,8BAA8ByF,EAAQtF,iBAE5KoC,EACE,oBAAoBkD,EAAQpF,qCAAqCoF,EAAQnF,8BAA8BmF,EAAQlF,gCAAgCkF,EAAQ1E,iCAAiCoF,QAAQ,QAElM5D,EACE,kCAAkC5F,EAAQgI,qCAAqChI,EAAQkI,wBAGpE,IAAjBT,EAAMlD,OACRqB,EAAY,uCACP,CACL,MAAMmD,EAAgBtB,EAAMV,MAAM,EAAG/G,EAAQgJ,aACvCS,EAAchC,EAAMlD,OAASwE,EAAcxE,OAAS,OAAOkD,EAAMlD,SAAW,GAClFqB,EAAY,8BAA8BmD,EAAcxE,SAASkF,SACjE,IAAK,MAAMC,KAAQX,EACjBnD,EACE,GAAG8D,EAAKzH,QAAQyH,EAAKrB,aAAaqB,EAAKpB,WAAWoB,EAAKpI,oBACtCoI,EAAK3B,mCAAmC2B,EAAKzB,yBAGpE,CAEA,GAAIf,EAAOhH,OAAOqE,OAAS,EAAG,CAC5B0B,EAAY,aAAaiB,EAAOhH,OAAOqE,kCACvC,IAAK,MAAM9D,KAASyG,EAAOhH,OAAO6G,MAAM,EAAG,IACzCd,EAAY,KAAKxF,OAEfyG,EAAOhH,OAAOqE,OAAS,IACzB0B,EAAY,SAASiB,EAAOhH,OAAOqE,OAAS,YAEhD,CACF,CArUMoF,CAAgB/C,EAAgBM,EAAQO,EAAOzH,IAI/CkH,EAAOK,YACNvH,EAAQ4J,aAAe1C,EAAOhH,OAAOqE,OAAS,GAC9CvE,EAAQ6J,YAAcpC,EAAMlD,OAAS,KAEtCuB,QAAQgE,SAAW,WAIjBzD,EAAQ0D,YAChB,EAvCKC,GAAOC,MAAOxJ,IACjBwF,EAAY,UAAUrF,EAAYH,QAClCqF,QAAQgE,SAAW"}
package/dist/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{realpath as t,stat as e,readdir as i,readFile as n}from"node:fs/promises";import o from"node:os";import r from"node:path";import{Command as s,InvalidArgumentError as c}from"commander";import{measureCode as a}from"./metrics.js";const l=new Map([[".cjs","javascript"],[".cts","typescript"],[".go","go"],[".js","javascript"],[".jsx","jsx"],[".mjs","javascript"],[".mts","typescript"],[".py","python"],[".ts","typescript"],[".tsx","tsx"]]),m=new Set([".git",".next",".tox",".tmp",".turbo",".venv",".yarn","__generated__","__pycache__","coverage","dist","generated","node_modules","vendor","venv"]),f=new Set(["__tests__","test","tests"]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-]test)\.[^.]+$/iu;async function d(e,n,o,s,c,a,l){let m,f;try{m=await t(e)}catch(t){return void s.push(`${w(e,l)}: ${b(t)}`)}if(v(m,l)&&!c.has(m)){c.add(m);try{f=await i(e,{withFileTypes:!0})}catch(t){return void s.push(`${w(e,l)}: ${b(t)}`)}for(const t of f){const i=r.join(e,t.name);if(t.isSymbolicLink())await p(t.name,i,n,o,s,c,a,l);else if(t.isDirectory()){if(x(t.name,n))continue;await d(i,n,o,s,c,a,l)}else t.isFile()&&await h(i,n,o,s,a,l)}}}async function p(i,n,o,s,c,a,l,m){let f,u;try{f=await t(n)}catch(t){return void c.push(`${w(n,m)}: ${b(t)}`)}if(v(f,m)){try{u=await e(n)}catch(t){return void c.push(`${w(n,m)}: ${b(t)}`)}if(u.isDirectory()){if(x(i,o)||x(r.basename(f),o))return;await d(n,o,s,c,a,l,m)}else u.isFile()&&await h(n,o,s,c,l,m,f,f)}}async function h(t,e,i,n,o,r,s=t,c){const a=$(s,e);a&&await y(t,a,i,n,o,r,c)}async function y(e,i,o,r,s,c,l){try{const r=l??await t(e);if(s.has(r))return;s.add(r);const c=await n(e,"utf8");o.push({file:e,metrics:a(c,{language:i})})}catch(t){r.push(`${w(e,c)}: ${b(t)}`)}}function g(t){let e=0,i=0,n=0,o=0;for(const r of t)e+=r.metrics.functionCount,i+=r.metrics.lines.code,n=Math.max(n,r.metrics.maxCyclomaticComplexity),o=Math.max(o,r.metrics.maxCognitiveComplexity);return{fileCount:t.length,functionCount:e,linesOfCode:i,maxCyclomaticComplexity:n,maxCognitiveComplexity:o}}function x(t,e){return!!m.has(t)||!e.includeTests&&f.has(t)}function v(t,e){const i=r.relative(e,t);return""===i||".."!==i&&!i.startsWith(`..${r.sep}`)&&!r.isAbsolute(i)}function $(t,e,i=!1){const n=t.toLowerCase();if((i||!(n.endsWith(".d.ts")||n.endsWith(".d.mts")||n.endsWith(".d.cts")||n.endsWith(".min.js")))&&(i||e.includeTests||!u.test(r.basename(t))))return l.get(r.extname(n))}function C(t){if(!/^[1-9]\d*$/u.test(t))throw new c("Expected a positive integer.");const e=Number(t);if(!Number.isSafeInteger(e)||e<1)throw new c("Expected a positive integer.");return e}function w(t,e){return r.relative(e,t)||r.basename(t)}function _(t){process.stdout.write(t)}function j(t){process.stderr.write(t)}function b(t){return t instanceof Error?t.message:String(t)}(async function(){const i=(new s).name("measure-code").description("Measure code metrics and list high-risk functions.").argument("[target]","file or directory to measure",".").option("--cyclomatic-threshold <number>","minimum cyclomatic complexity to report",C,10).option("--cognitive-threshold <number>","minimum cognitive complexity to report",C,15).option("--max-findings <number>","maximum number of risk findings to print",C,20).option("--include-tests","include test files and test directories").option("--json","print JSON output").option("--fail-on-error","exit with code 1 when files or directories cannot be scanned").option("--fail-on-risk","exit with code 1 when high-risk functions are found");i.action(async(i,n)=>{const s=function(t){if("~"===t)return o.homedir();if(t.startsWith("~/"))return r.join(o.homedir(),t.slice(2));return r.resolve(t)}(i),c=await async function(i,n){const o=[],s=[],c=new Set;let a=i;try{a=await t(i)}catch{}const l=r.dirname(a);let m;try{m=await e(a)}catch(t){const e=`${w(a,l)}: ${b(t)}`;return{displayRoot:l,files:o,errors:[e],fatalError:e}}if(m.isFile()){const t=r.dirname(a),e=$(a,n,!0);if(!e){const e=`${w(a,t)}: unsupported file type`;return{displayRoot:t,files:o,errors:[e],fatalError:e}}return await y(a,e,o,s,c,t,a),{displayRoot:t,files:o,errors:s}}return await d(a,n,o,s,new Set,c,a),{displayRoot:a,files:o,errors:s}}(s,n),a=function(t,e,i){const n=t.flatMap(({file:t,metrics:n})=>n.functions.filter(t=>function(t,e){return t.cyclomaticComplexity>=e.cyclomaticThreshold||t.cognitiveComplexity>=e.cognitiveThreshold}(t,e)).map(o=>function(t,e,i,n,o){return{file:w(t,o),language:e,name:i.name??"<anonymous>",startLine:i.startLine,endLine:i.endLine,cyclomaticComplexity:i.cyclomaticComplexity,cognitiveComplexity:i.cognitiveComplexity,score:Math.max(i.cyclomaticComplexity/n.cyclomaticThreshold,i.cognitiveComplexity/n.cognitiveThreshold)}}(t,n.language,o,e,i)));return n.sort((t,e)=>e.score-t.score||e.cyclomaticComplexity-t.cyclomaticComplexity),n}(c.files,n,c.displayRoot);n.json?function(t,e,i){const n=g(t.files),o=e.slice(0,i.maxFindings);_(JSON.stringify({summary:n,thresholds:{cyclomaticComplexity:i.cyclomaticThreshold,cognitiveComplexity:i.cognitiveThreshold},totalRisks:e.length,truncated:o.length<e.length,risks:o,errors:t.errors},void 0,2)+"\n")}(c,a,n):function(t,e,i,n){if(e.fatalError)return void j(`Error: ${e.fatalError}\n`);const o=g(e.files);if(_(`Measured ${o.fileCount} files under ${t}\n`),_(`LOC ${o.linesOfCode}, functions ${o.functionCount}, max cyclomatic ${o.maxCyclomaticComplexity}, max cognitive ${o.maxCognitiveComplexity}\n`),_(`Risk thresholds: cyclomatic >= ${n.cyclomaticThreshold}, cognitive >= ${n.cognitiveThreshold}\n`),0===i.length)_("No high-risk functions found.\n");else{const t=i.slice(0,n.maxFindings),e=i.length>t.length?` of ${i.length}`:"";_(`\nHigh-risk functions (top ${t.length}${e}):\n`);for(const e of t)_(`${e.file}:${e.startLine}-${e.endLine} ${e.name} (cyclomatic ${e.cyclomaticComplexity}, cognitive ${e.cognitiveComplexity})\n`)}if(e.errors.length>0){j(`\nSkipped ${e.errors.length} files or directories:\n`);for(const t of e.errors.slice(0,10))j(`- ${t}\n`);e.errors.length>10&&j(`- ... ${e.errors.length-10} more\n`)}}(s,c,a,n),(c.fatalError||n.failOnError&&c.errors.length>0||n.failOnRisk&&a.length>0)&&(process.exitCode=1)}),await i.parseAsync()})().catch(t=>{j(`Error: ${b(t)}\n`),process.exitCode=1});
2
+ import{realpath as t,stat as e,readdir as i,readFile as n}from"node:fs/promises";import o from"node:os";import r from"node:path";import{Command as s,InvalidArgumentError as c}from"commander";import{measureCode as a}from"./metrics.js";const l=new Map([[".cjs","javascript"],[".cts","typescript"],[".go","go"],[".js","javascript"],[".jsx","jsx"],[".mjs","javascript"],[".mts","typescript"],[".py","python"],[".ts","typescript"],[".tsx","tsx"]]),m=new Set([".git",".next",".tox",".tmp",".turbo",".venv",".yarn","__generated__","__pycache__","coverage","dist","generated","node_modules","vendor","venv"]),u=new Set(["__tests__","test","tests"]),p=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-]test)\.[^.]+$/iu;async function f(e,n,o,s,c,a,l){let m,u;try{m=await t(e)}catch(t){return void s.push(`${w(e,l)}: ${b(t)}`)}if(x(m,l)&&!c.has(m)){c.add(m);try{u=await i(e,{withFileTypes:!0})}catch(t){return void s.push(`${w(e,l)}: ${b(t)}`)}for(const t of u){const i=r.join(e,t.name);if(t.isSymbolicLink())await h(t.name,i,n,o,s,c,a,l);else if(t.isDirectory()){if(C(t.name,n))continue;await f(i,n,o,s,c,a,l)}else t.isFile()&&await d(i,n,o,s,a,l)}}}async function h(i,n,o,s,c,a,l,m){let u,p;try{u=await t(n)}catch(t){return void c.push(`${w(n,m)}: ${b(t)}`)}if(x(u,m)){try{p=await e(n)}catch(t){return void c.push(`${w(n,m)}: ${b(t)}`)}if(p.isDirectory()){if(C(i,o)||C(r.basename(u),o))return;await f(n,o,s,c,a,l,m)}else p.isFile()&&await d(n,o,s,c,l,m,u,u)}}async function d(t,e,i,n,o,r,s=t,c){const a=v(s,e);a&&await y(t,a,i,n,o,r,c)}async function y(e,i,o,r,s,c,l){try{const r=l??await t(e);if(s.has(r))return;s.add(r);const c=await n(e,"utf8");o.push({file:e,metrics:a(c,{language:i})})}catch(t){r.push(`${w(e,c)}: ${b(t)}`)}}function g(t){let e=0,i=0,n=0,o=0,r=0,s=0,c=0,a=0,l=0,m=0,u=0,p=0,f=0,h=0,d=0,y=0;for(const g of t)e+=g.metrics.functionCount,i+=g.metrics.lines.code,n=Math.max(n,g.metrics.maxCyclomaticComplexity),o=Math.max(o,g.metrics.maxCognitiveComplexity),r+=g.metrics.callGraph.callCount,s+=g.metrics.callGraph.internalCallCount,c=Math.max(c,g.metrics.callGraph.maxCallDepth),a+=g.metrics.coupling.importSourceCount,l+=g.metrics.coupling.relativeImportCount,m+=g.metrics.coupling.externalImportCount,u+=g.metrics.coupling.exportCount,p+=g.metrics.cohesion.averageFunctionIdentifierOverlap,f+=g.metrics.typeComplexity.typeAnnotationCount,h+=g.metrics.typeComplexity.typeAliasCount,d+=g.metrics.typeComplexity.interfaceCount,y+=g.metrics.typeComplexity.genericParameterCount;return{fileCount:t.length,functionCount:e,linesOfCode:i,maxCyclomaticComplexity:n,maxCognitiveComplexity:o,callCount:r,internalCallCount:s,maxCallDepth:c,importSourceCount:a,relativeImportCount:l,externalImportCount:m,exportCount:u,averageFunctionIdentifierOverlap:0===t.length?0:p/t.length,typeAnnotationCount:f,typeAliasCount:h,interfaceCount:d,genericParameterCount:y}}function C(t,e){return!!m.has(t)||!e.includeTests&&u.has(t)}function x(t,e){const i=r.relative(e,t);return""===i||".."!==i&&!i.startsWith(`..${r.sep}`)&&!r.isAbsolute(i)}function v(t,e,i=!1){const n=t.toLowerCase();if((i||!(n.endsWith(".d.ts")||n.endsWith(".d.mts")||n.endsWith(".d.cts")||n.endsWith(".min.js")))&&(i||e.includeTests||!p.test(r.basename(t))))return l.get(r.extname(n))}function $(t){if(!/^[1-9]\d*$/u.test(t))throw new c("Expected a positive integer.");const e=Number(t);if(!Number.isSafeInteger(e)||e<1)throw new c("Expected a positive integer.");return e}function w(t,e){return r.relative(e,t)||r.basename(t)}function _(t){process.stdout.write(t)}function j(t){process.stderr.write(t)}function b(t){return t instanceof Error?t.message:String(t)}(async function(){const i=(new s).name("measure-code").description("Measure code metrics and list high-risk functions.").argument("[target]","file or directory to measure",".").option("--cyclomatic-threshold <number>","minimum cyclomatic complexity to report",$,10).option("--cognitive-threshold <number>","minimum cognitive complexity to report",$,15).option("--max-findings <number>","maximum number of risk findings to print",$,20).option("--include-tests","include test files and test directories").option("--json","print JSON output").option("--fail-on-error","exit with code 1 when files or directories cannot be scanned").option("--fail-on-risk","exit with code 1 when high-risk functions are found");i.action(async(i,n)=>{const s=function(t){if("~"===t)return o.homedir();if(t.startsWith("~/"))return r.join(o.homedir(),t.slice(2));return r.resolve(t)}(i),c=await async function(i,n){const o=[],s=[],c=new Set;let a=i;try{a=await t(i)}catch{}const l=r.dirname(a);let m;try{m=await e(a)}catch(t){const e=`${w(a,l)}: ${b(t)}`;return{displayRoot:l,files:o,errors:[e],fatalError:e}}if(m.isFile()){const t=r.dirname(a),e=v(a,n,!0);if(!e){const e=`${w(a,t)}: unsupported file type`;return{displayRoot:t,files:o,errors:[e],fatalError:e}}return await y(a,e,o,s,c,t,a),{displayRoot:t,files:o,errors:s}}return await f(a,n,o,s,new Set,c,a),{displayRoot:a,files:o,errors:s}}(s,n),a=function(t,e,i){const n=t.flatMap(({file:t,metrics:n})=>n.functions.filter(t=>function(t,e){return t.cyclomaticComplexity>=e.cyclomaticThreshold||t.cognitiveComplexity>=e.cognitiveThreshold}(t,e)).map(o=>function(t,e,i,n,o){return{file:w(t,o),language:e,name:i.name??"<anonymous>",startLine:i.startLine,endLine:i.endLine,cyclomaticComplexity:i.cyclomaticComplexity,cognitiveComplexity:i.cognitiveComplexity,score:Math.max(i.cyclomaticComplexity/n.cyclomaticThreshold,i.cognitiveComplexity/n.cognitiveThreshold)}}(t,n.language,o,e,i)));return n.sort((t,e)=>e.score-t.score||e.cyclomaticComplexity-t.cyclomaticComplexity),n}(c.files,n,c.displayRoot);n.json?function(t,e,i){const n=g(t.files),o=e.slice(0,i.maxFindings);_(JSON.stringify({summary:n,thresholds:{cyclomaticComplexity:i.cyclomaticThreshold,cognitiveComplexity:i.cognitiveThreshold},totalRisks:e.length,truncated:o.length<e.length,risks:o,errors:t.errors},void 0,2)+"\n")}(c,a,n):function(t,e,i,n){if(e.fatalError)return void j(`Error: ${e.fatalError}\n`);const o=g(e.files);if(_(`Measured ${o.fileCount} files under ${t}\n`),_(`LOC ${o.linesOfCode}, functions ${o.functionCount}, max cyclomatic ${o.maxCyclomaticComplexity}, max cognitive ${o.maxCognitiveComplexity}\n`),_(`Calls ${o.callCount}, internal edges ${o.internalCallCount}, max call depth ${o.maxCallDepth}, imports ${o.importSourceCount}, exports ${o.exportCount}\n`),_(`Type annotations ${o.typeAnnotationCount}, type aliases ${o.typeAliasCount}, interfaces ${o.interfaceCount}, avg cohesion ${o.averageFunctionIdentifierOverlap.toFixed(2)}\n`),_(`Risk thresholds: cyclomatic >= ${n.cyclomaticThreshold}, cognitive >= ${n.cognitiveThreshold}\n`),0===i.length)_("No high-risk functions found.\n");else{const t=i.slice(0,n.maxFindings),e=i.length>t.length?` of ${i.length}`:"";_(`\nHigh-risk functions (top ${t.length}${e}):\n`);for(const e of t)_(`${e.file}:${e.startLine}-${e.endLine} ${e.name} (cyclomatic ${e.cyclomaticComplexity}, cognitive ${e.cognitiveComplexity})\n`)}if(e.errors.length>0){j(`\nSkipped ${e.errors.length} files or directories:\n`);for(const t of e.errors.slice(0,10))j(`- ${t}\n`);e.errors.length>10&&j(`- ... ${e.errors.length-10} more\n`)}}(s,c,a,n),(c.fatalError||n.failOnError&&c.errors.length>0||n.failOnRisk&&a.length>0)&&(process.exitCode=1)}),await i.parseAsync()})().catch(t=>{j(`Error: ${b(t)}\n`),process.exitCode=1});
3
3
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface CliOptions {\n cognitiveThreshold: number;\n cyclomaticThreshold: number;\n failOnError?: boolean;\n failOnRisk?: boolean;\n includeTests?: boolean;\n json?: boolean;\n maxFindings: number;\n}\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine: number;\n file: string;\n language: LanguageName;\n name: string;\n score: number;\n startLine: number;\n}\n\ninterface ScanResult {\n displayRoot: string;\n errors: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.cjs', 'javascript'],\n ['.cts', 'typescript'],\n ['.go', 'go'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.git',\n '.next',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'generated',\n 'node_modules',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-]test)\\.[^.]+$/iu;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('measure-code')\n .description('Measure code metrics and list high-risk functions.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger, 10)\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger, 15)\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger, 20)\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk functions are found');\n\n program.action(async (target: string, options: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const result = await scanTarget(resolvedTarget, options);\n const risks = findRiskyFunctions(result.files, options, result.displayRoot);\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction 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\nasync function scanTarget(target: string, options: CliOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const visitedFiles = new Set<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], 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], fatalError };\n }\n\n await measureFile(canonicalTarget, language, files, errors, visitedFiles, displayRoot, canonicalTarget);\n return { displayRoot, files, errors };\n }\n\n await scanDirectory(canonicalTarget, options, files, errors, new Set(), visitedFiles, canonicalTarget);\n return { displayRoot: canonicalTarget, files, errors };\n}\n\nasync function scanDirectory(\n directory: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, rootDirectory)) {\n return;\n }\n\n if (visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\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(\n entry.name,\n entryPath,\n options,\n files,\n errors,\n visitedDirectories,\n visitedFiles,\n rootDirectory\n );\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, options)) {\n continue;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, options, files, errors, visitedFiles, rootDirectory);\n }\n }\n}\n\nasync function scanSymbolicLink(\n name: string,\n entryPath: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (shouldSkipDirectory(name, options) || shouldSkipDirectory(path.basename(resolvedPath), options)) {\n return;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(\n entryPath,\n options,\n files,\n errors,\n visitedFiles,\n rootDirectory,\n resolvedPath,\n resolvedPath\n );\n }\n}\n\nasync function measureScannableFile(\n file: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, options);\n if (language) {\n await measureFile(file, language, files, errors, visitedFiles, displayRoot, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (visitedFiles.has(resolvedFile)) {\n return;\n }\n visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n files.push({\n file,\n metrics: measureCode(code, { language }),\n });\n } catch (error) {\n errors.push(`${formatPath(file, displayRoot)}: ${formatError(error)}`);\n }\n}\n\nfunction findRiskyFunctions(files: FileMetrics[], options: CliOptions, displayRoot: string): RiskFinding[] {\n const findings = files.flatMap(({ file, metrics }) =>\n metrics.functions\n .filter((fn) => isRiskyFunction(fn, options))\n .map((fn) => createRiskFinding(file, metrics.language, fn, options, displayRoot))\n );\n\n findings.sort((left, right) => right.score - left.score || right.cyclomaticComplexity - left.cyclomaticComplexity);\n return findings;\n}\n\nfunction isRiskyFunction(fn: FunctionMetrics, options: CliOptions): boolean {\n return fn.cyclomaticComplexity >= options.cyclomaticThreshold || fn.cognitiveComplexity >= options.cognitiveThreshold;\n}\n\nfunction createRiskFinding(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n options: CliOptions,\n displayRoot: string\n): RiskFinding {\n return {\n file: formatPath(file, displayRoot),\n language,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n score: Math.max(\n fn.cyclomaticComplexity / options.cyclomaticThreshold,\n fn.cognitiveComplexity / options.cognitiveThreshold\n ),\n };\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: {\n cyclomaticComplexity: options.cyclomaticThreshold,\n cognitiveComplexity: options.cognitiveThreshold,\n },\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n risks: reportedRisks,\n errors: result.errors,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Risk thresholds: cyclomatic >= ${options.cyclomaticThreshold}, cognitive >= ${options.cognitiveThreshold}\\n`\n );\n\n if (risks.length === 0) {\n writeStdout('No high-risk functions found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk functions (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(\n `${risk.file}:${risk.startLine}-${risk.endLine} ${risk.name} ` +\n `(cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})\\n`\n );\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: CliOptions): 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\nfunction getLanguage(file: string, options: CliOptions, 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 ) {\n return undefined;\n }\n\n if (!explicitTarget && !options.includeTests && testFilePattern.test(path.basename(file))) {\n return undefined;\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["languageByExtension","Map","ignoredDirectoryNames","Set","testDirectoryNames","testFilePattern","async","scanDirectory","directory","options","files","errors","visitedDirectories","visitedFiles","rootDirectory","resolvedDirectory","entries","realpath","error","push","formatPath","formatError","isWithinDirectory","has","add","readdir","withFileTypes","entry","entryPath","path","join","name","isSymbolicLink","scanSymbolicLink","isDirectory","shouldSkipDirectory","isFile","measureScannableFile","resolvedPath","entryStat","stat","basename","file","displayRoot","languageFile","realFile","language","getLanguage","measureFile","resolvedFile","code","readFile","metrics","measureCode","summarize","functionCount","linesOfCode","maxCyclomaticComplexity","maxCognitiveComplexity","lines","Math","max","fileCount","length","includeTests","candidate","relative","startsWith","sep","isAbsolute","explicitTarget","lowerFile","toLowerCase","endsWith","test","get","extname","parsePositiveInteger","value","InvalidArgumentError","parsed","Number","isSafeInteger","base","writeStdout","message","process","stdout","write","writeStderr","stderr","Error","String","program","Command","description","argument","option","action","target","resolvedTarget","os","homedir","slice","resolve","resolveTarget","result","canonicalTarget","fallbackDisplayRoot","dirname","targetStat","fatalError","scanTarget","risks","findings","flatMap","functions","filter","fn","cyclomaticComplexity","cyclomaticThreshold","cognitiveComplexity","cognitiveThreshold","isRiskyFunction","map","startLine","endLine","score","createRiskFinding","sort","left","right","findRiskyFunctions","json","summary","reportedRisks","maxFindings","JSON","stringify","thresholds","totalRisks","truncated","undefined","printJson","totalSuffix","risk","printTextReport","failOnError","failOnRisk","exitCode","parseAsync","main","catch"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface CliOptions {\n cognitiveThreshold: number;\n cyclomaticThreshold: number;\n failOnError?: boolean;\n failOnRisk?: boolean;\n includeTests?: boolean;\n json?: boolean;\n maxFindings: number;\n}\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine: number;\n file: string;\n language: LanguageName;\n name: string;\n score: number;\n startLine: number;\n}\n\ninterface ScanResult {\n displayRoot: string;\n errors: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.cjs', 'javascript'],\n ['.cts', 'typescript'],\n ['.go', 'go'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.git',\n '.next',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'generated',\n 'node_modules',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-]test)\\.[^.]+$/iu;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('measure-code')\n .description('Measure code metrics and list high-risk functions.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger, 10)\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger, 15)\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger, 20)\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk functions are found');\n\n program.action(async (target: string, options: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const result = await scanTarget(resolvedTarget, options);\n const risks = findRiskyFunctions(result.files, options, result.displayRoot);\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction 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\nasync function scanTarget(target: string, options: CliOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const visitedFiles = new Set<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], 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], fatalError };\n }\n\n await measureFile(canonicalTarget, language, files, errors, visitedFiles, displayRoot, canonicalTarget);\n return { displayRoot, files, errors };\n }\n\n await scanDirectory(canonicalTarget, options, files, errors, new Set(), visitedFiles, canonicalTarget);\n return { displayRoot: canonicalTarget, files, errors };\n}\n\nasync function scanDirectory(\n directory: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, rootDirectory)) {\n return;\n }\n\n if (visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n errors.push(`${formatPath(directory, rootDirectory)}: ${formatError(error)}`);\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(\n entry.name,\n entryPath,\n options,\n files,\n errors,\n visitedDirectories,\n visitedFiles,\n rootDirectory\n );\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, options)) {\n continue;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, options, files, errors, visitedFiles, rootDirectory);\n }\n }\n}\n\nasync function scanSymbolicLink(\n name: string,\n entryPath: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedDirectories: Set<string>,\n visitedFiles: Set<string>,\n rootDirectory: string\n): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n errors.push(`${formatPath(entryPath, rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (shouldSkipDirectory(name, options) || shouldSkipDirectory(path.basename(resolvedPath), options)) {\n return;\n }\n await scanDirectory(entryPath, options, files, errors, visitedDirectories, visitedFiles, rootDirectory);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(\n entryPath,\n options,\n files,\n errors,\n visitedFiles,\n rootDirectory,\n resolvedPath,\n resolvedPath\n );\n }\n}\n\nasync function measureScannableFile(\n file: string,\n options: CliOptions,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, options);\n if (language) {\n await measureFile(file, language, files, errors, visitedFiles, displayRoot, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n files: FileMetrics[],\n errors: string[],\n visitedFiles: Set<string>,\n displayRoot: string,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (visitedFiles.has(resolvedFile)) {\n return;\n }\n visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n files.push({\n file,\n metrics: measureCode(code, { language }),\n });\n } catch (error) {\n errors.push(`${formatPath(file, displayRoot)}: ${formatError(error)}`);\n }\n}\n\nfunction findRiskyFunctions(files: FileMetrics[], options: CliOptions, displayRoot: string): RiskFinding[] {\n const findings = files.flatMap(({ file, metrics }) =>\n metrics.functions\n .filter((fn) => isRiskyFunction(fn, options))\n .map((fn) => createRiskFinding(file, metrics.language, fn, options, displayRoot))\n );\n\n findings.sort((left, right) => right.score - left.score || right.cyclomaticComplexity - left.cyclomaticComplexity);\n return findings;\n}\n\nfunction isRiskyFunction(fn: FunctionMetrics, options: CliOptions): boolean {\n return fn.cyclomaticComplexity >= options.cyclomaticThreshold || fn.cognitiveComplexity >= options.cognitiveThreshold;\n}\n\nfunction createRiskFinding(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n options: CliOptions,\n displayRoot: string\n): RiskFinding {\n return {\n file: formatPath(file, displayRoot),\n language,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n score: Math.max(\n fn.cyclomaticComplexity / options.cyclomaticThreshold,\n fn.cognitiveComplexity / options.cognitiveThreshold\n ),\n };\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: {\n cyclomaticComplexity: options.cyclomaticThreshold,\n cognitiveComplexity: options.cognitiveThreshold,\n },\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n risks: reportedRisks,\n errors: result.errors,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: CliOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Calls ${summary.callCount}, internal edges ${summary.internalCallCount}, max call depth ${summary.maxCallDepth}, imports ${summary.importSourceCount}, exports ${summary.exportCount}\\n`\n );\n writeStdout(\n `Type annotations ${summary.typeAnnotationCount}, type aliases ${summary.typeAliasCount}, interfaces ${summary.interfaceCount}, avg cohesion ${summary.averageFunctionIdentifierOverlap.toFixed(2)}\\n`\n );\n writeStdout(\n `Risk thresholds: cyclomatic >= ${options.cyclomaticThreshold}, cognitive >= ${options.cognitiveThreshold}\\n`\n );\n\n if (risks.length === 0) {\n writeStdout('No high-risk functions found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk functions (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(\n `${risk.file}:${risk.startLine}-${risk.endLine} ${risk.name} ` +\n `(cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})\\n`\n );\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n callCount: number;\n internalCallCount: number;\n maxCallDepth: number;\n importSourceCount: number;\n relativeImportCount: number;\n externalImportCount: number;\n exportCount: number;\n averageFunctionIdentifierOverlap: number;\n typeAnnotationCount: number;\n typeAliasCount: number;\n interfaceCount: number;\n genericParameterCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n let callCount = 0;\n let internalCallCount = 0;\n let maxCallDepth = 0;\n let importSourceCount = 0;\n let relativeImportCount = 0;\n let externalImportCount = 0;\n let exportCount = 0;\n let cohesionTotal = 0;\n let typeAnnotationCount = 0;\n let typeAliasCount = 0;\n let interfaceCount = 0;\n let genericParameterCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n callCount += file.metrics.callGraph.callCount;\n internalCallCount += file.metrics.callGraph.internalCallCount;\n maxCallDepth = Math.max(maxCallDepth, file.metrics.callGraph.maxCallDepth);\n importSourceCount += file.metrics.coupling.importSourceCount;\n relativeImportCount += file.metrics.coupling.relativeImportCount;\n externalImportCount += file.metrics.coupling.externalImportCount;\n exportCount += file.metrics.coupling.exportCount;\n cohesionTotal += file.metrics.cohesion.averageFunctionIdentifierOverlap;\n typeAnnotationCount += file.metrics.typeComplexity.typeAnnotationCount;\n typeAliasCount += file.metrics.typeComplexity.typeAliasCount;\n interfaceCount += file.metrics.typeComplexity.interfaceCount;\n genericParameterCount += file.metrics.typeComplexity.genericParameterCount;\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n callCount,\n internalCallCount,\n maxCallDepth,\n importSourceCount,\n relativeImportCount,\n externalImportCount,\n exportCount,\n averageFunctionIdentifierOverlap: files.length === 0 ? 0 : cohesionTotal / files.length,\n typeAnnotationCount,\n typeAliasCount,\n interfaceCount,\n genericParameterCount,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: CliOptions): 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\nfunction getLanguage(file: string, options: CliOptions, 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 ) {\n return undefined;\n }\n\n if (!explicitTarget && !options.includeTests && testFilePattern.test(path.basename(file))) {\n return undefined;\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"names":["languageByExtension","Map","ignoredDirectoryNames","Set","testDirectoryNames","testFilePattern","async","scanDirectory","directory","options","files","errors","visitedDirectories","visitedFiles","rootDirectory","resolvedDirectory","entries","realpath","error","push","formatPath","formatError","isWithinDirectory","has","add","readdir","withFileTypes","entry","entryPath","path","join","name","isSymbolicLink","scanSymbolicLink","isDirectory","shouldSkipDirectory","isFile","measureScannableFile","resolvedPath","entryStat","stat","basename","file","displayRoot","languageFile","realFile","language","getLanguage","measureFile","resolvedFile","code","readFile","metrics","measureCode","summarize","functionCount","linesOfCode","maxCyclomaticComplexity","maxCognitiveComplexity","callCount","internalCallCount","maxCallDepth","importSourceCount","relativeImportCount","externalImportCount","exportCount","cohesionTotal","typeAnnotationCount","typeAliasCount","interfaceCount","genericParameterCount","lines","Math","max","callGraph","coupling","cohesion","averageFunctionIdentifierOverlap","typeComplexity","fileCount","length","includeTests","candidate","relative","startsWith","sep","isAbsolute","explicitTarget","lowerFile","toLowerCase","endsWith","test","get","extname","parsePositiveInteger","value","InvalidArgumentError","parsed","Number","isSafeInteger","base","writeStdout","message","process","stdout","write","writeStderr","stderr","Error","String","program","Command","description","argument","option","action","target","resolvedTarget","os","homedir","slice","resolve","resolveTarget","result","canonicalTarget","fallbackDisplayRoot","dirname","targetStat","fatalError","scanTarget","risks","findings","flatMap","functions","filter","fn","cyclomaticComplexity","cyclomaticThreshold","cognitiveComplexity","cognitiveThreshold","isRiskyFunction","map","startLine","endLine","score","createRiskFinding","sort","left","right","findRiskyFunctions","json","summary","reportedRisks","maxFindings","JSON","stringify","thresholds","totalRisks","truncated","undefined","printJson","toFixed","totalSuffix","risk","printTextReport","failOnError","failOnRisk","exitCode","parseAsync","main","catch"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { defaultLanguages, supportedLanguages } from './languages.js';
2
2
  export { TreeMeasurer, defaultMeasurer, measureCode } from './metrics.js';
3
- export type { CodeMetrics, FunctionMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, LineMetrics, MeasureOptions, SupportedLanguage, } from './types.js';
3
+ export type { CallGraphMetrics, CodeMetrics, CohesionMetrics, CouplingMetrics, FunctionMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, LineMetrics, MeasureOptions, SupportedLanguage, TypeComplexityMetrics, } from './types.js';
package/dist/metrics.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var t=require("tree-sitter"),e=require("./languages.cjs");const n=new Set(["&&","||","and","or"]),o=new Set(["+","-","*","/","%","**","=","+=","-=","*=","/=","%=","==","!=","===","!==","<","<=",">",">=","!","~","&","|","^","<<",">>","=>","return","throw","yield","await","break","continue"]),i=new Set(["identifier","property_identifier","field_identifier","type_identifier","number","integer","float","string","string_literal","template_string","character_literal","true","false","null","undefined","nil"]);class r{registry=e.createLanguageRegistry();registerLanguage(t){this.registry.set(t.name,t);for(const e of t.aliases??[])this.registry.set(e,t)}getSupportedLanguages(){return[...new Set([...this.registry.values()].map(t=>t.name))]}measure(e,n){const r=this.registry.get(n.language);if(!r)throw new Error(`Unsupported language: ${n.language}`);const s=new t;s.setLanguage(r.parserLanguage);const d=s.parse(e,void 0,{bufferSize:e.length+1}).rootNode,y=c(d,new Set(r.functionNodeTypes)).map(t=>function(t,e){const n=a(t,e,0);return{name:u(t),startLine:t.startPosition.row+1,endLine:t.endPosition.row+1,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity}}(t,r)),h=a(d,r,0),x=function(t,e){const n=0===t.length?[]:t.split(/\r\n|\n|\r/),o=function(t){const e=[];function n(t){if("comment"===t.type||"line_comment"===t.type||"block_comment"===t.type)for(let n=t.startPosition.row;n<=t.endPosition.row;n+=1)e.push({line:n,startColumn:n===t.startPosition.row?t.startPosition.column:0,endColumn:n===t.endPosition.row?t.endPosition.column:Number.POSITIVE_INFINITY});for(const e of t.namedChildren)n(e)}return n(t),e}(e);let i=0,r=0;for(const[t,e]of n.entries())""!==e.trim()?l(e,t,o)&&(r+=1):i+=1;return{total:n.length,code:n.length-i-r,comment:r,blank:i}}(e,d),C=function(t,e){const n=new Map,r=new Map;function s(t){if("comment"!==t.type){if(0===t.childCount){const s=e.slice(t.startIndex,t.endIndex);return void(o.has(s)||o.has(t.type)?f(n,s||t.type):i.has(t.type)&&f(r,s))}o.has(t.type)&&f(n,t.type);for(const e of t.children)s(e)}}s(t);const a=n.size,c=r.size,l=p(n.values()),u=p(r.values()),m=a+c,g=l+u,d=0===m?0:g*Math.log2(m),y=0===c?0:a/2*(u/c),h=y*d;return{distinctOperators:a,distinctOperands:c,totalOperators:l,totalOperands:u,vocabulary:m,length:g,volume:d,difficulty:y,effort:h,time:h/18,bugs:d/3e3}}(d,e);return{language:r.name,bytes:Buffer.byteLength(e),lines:x,functions:y,classCount:c(d,new Set(r.classNodeTypes)).length,functionCount:y.length,cyclomaticComplexity:h.cyclomaticComplexity,maxCyclomaticComplexity:g(y,"cyclomaticComplexity"),cognitiveComplexity:h.cognitiveComplexity,maxCognitiveComplexity:g(y,"cognitiveComplexity"),nestingDepth:h.nestingDepth,halstead:C,maintainabilityIndex:m(C.volume,h.cyclomaticComplexity,x.code),syntaxTree:n.includeSyntaxTree?d.toString():void 0}}}const s=new r;function a(t,e,o){let i=1,r=0,s=o;const a=new Set(e.decisionNodeTypes),c=new Set(e.nestingNodeTypes);function l(t,e){const o=a.has(t.type),u=c.has(t.type);o&&(i+=1,r+=1+e),function(t){if(t.isNamed)return!1;return n.has(t.text)}(t)&&(i+=1,r+=1);const m=u?e+1:e;s=Math.max(s,m);for(const e of t.children)l(e,m)}for(const e of t.children)l(e,o);return{cyclomaticComplexity:i,cognitiveComplexity:r,nestingDepth:s}}function c(t,e){const n=[];return function t(o){e.has(o.type)&&n.push(o);for(const e of o.namedChildren)t(e)}(t),n}function l(t,e,n){const o=n.filter(t=>t.line===e);if(0===o.length)return!1;const i=t.search(/\S/),r=t.trimEnd().length;return o.some(t=>t.startColumn<=i&&t.endColumn>=r)}function u(t){const e=t.childForFieldName("name");if(e)return e.text;const n=t.parent;if(!n)return;const o=n.childForFieldName("name");return o?.text}function m(t,e,n){if(0===n)return 100;const o=171-5.2*Math.log(Math.max(t,1))-.23*e-16.2*Math.log(n);return Math.max(0,Math.min(100,100*o/171))}function f(t,e){t.set(e,(t.get(e)??0)+1)}function g(t,e){return 0===t.length?0:Math.max(...t.map(t=>t[e]))}function p(t){let e=0;for(const n of t)e+=n;return e}exports.TreeMeasurer=r,exports.defaultMeasurer=s,exports.measureCode=function(t,e){return s.measure(t,e)};
1
+ "use strict";var e=require("tree-sitter"),t=require("./languages.cjs");const n=new Set(["&&","||","and","or"]),o=new Set(["+","-","*","/","%","**","=","+=","-=","*=","/=","%=","==","!=","===","!==","<","<=",">",">=","!","~","&","|","^","<<",">>","=>","return","throw","yield","await","break","continue"]),i=new Set(["identifier","property_identifier","field_identifier","type_identifier","number","integer","float","string","string_literal","template_string","character_literal","true","false","null","undefined","nil"]);class r{registry=t.createLanguageRegistry();registerLanguage(e){this.registry.set(e.name,e);for(const t of e.aliases??[])this.registry.set(t,e)}getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(t,n){const r=this.registry.get(n.language);if(!r)throw new Error(`Unsupported language: ${n.language}`);const a=new e;a.setLanguage(r.parserLanguage);const C=a.parse(t,void 0,{bufferSize:t.length+1}).rootNode,x=function(e,t,n){const o=t.map(e=>function(e,t){const n=s(e,t,0),o=function(e){const t=new Set;let n=0;function o(e){if(function(e){return"call_expression"===e.type||"call"===e.type}(e)){n+=1;const o=function(e){const t=e.childForFieldName("function")??e.namedChild(0);if(!t)return;return y(t)}(e);o&&t.add(o)}for(const t of e.namedChildren)o(t)}return o(e),{callCount:n,callees:t}}(e);return{name:d(e),startLine:e.startPosition.row+1,endLine:e.endPosition.row+1,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity,callCount:o.callCount,callees:o.callees,identifiers:c(e)}}(e,n)),i=function(e){const t=new Set(e.map(e=>e.name).filter(e=>void 0!==e)),n=new Map,o=new Map,i=new Map;let r=0,a=0;const s=new Set;for(const c of e){r+=c.callCount;for(const e of c.callees)s.add(e);if(!c.name)continue;const e=new Set([...c.callees].filter(e=>t.has(e)));i.set(c.name,e),o.set(c.name,e.size);for(const t of e)n.set(t,(n.get(t)??0)+1),a+=1}const c=function(e){const t=new Set;for(const n of e.keys())g(n,n,e,new Set)&&t.add(n);return t}(i);return{fanInByName:n,fanOutByName:o,recursiveNames:c,metrics:{callCount:r,uniqueCalleeCount:s.size,internalCallCount:a,internalEdgeCount:N([...i.values()].map(e=>e.size)),recursiveFunctionCount:c.size,maxFanIn:b(n),maxFanOut:b(o),maxCallDepth:h(i)}}}(o);return{functions:o.map(e=>({name:e.name,startLine:e.startLine,endLine:e.endLine,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,callCount:e.callCount,uniqueCalleeCount:e.callees.size,fanIn:i.fanInByName.get(e.name??"")??0,fanOut:i.fanOutByName.get(e.name??"")??0,recursive:i.recursiveNames.has(e.name??"")})),callGraph:i.metrics,coupling:l(e),cohesion:f(o),typeComplexity:p(e)}}(C,u(C,new Set(r.functionNodeTypes)),r),_=x.functions,M=s(C,r,0),I=function(e,t){const n=0===e.length?[]:e.split(/\r\n|\n|\r/),o=function(e){const t=[];function n(e){if("comment"===e.type||"line_comment"===e.type||"block_comment"===e.type)for(let n=e.startPosition.row;n<=e.endPosition.row;n+=1)t.push({line:n,startColumn:n===e.startPosition.row?e.startPosition.column:0,endColumn:n===e.endPosition.row?e.endPosition.column:Number.POSITIVE_INFINITY});for(const t of e.namedChildren)n(t)}return n(e),t}(t);let i=0,r=0;for(const[e,t]of n.entries())""!==t.trim()?m(t,e,o)&&(r+=1):i+=1;return{total:n.length,code:n.length-i-r,comment:r,blank:i}}(t,C),z=function(e,t){const n=new Map,r=new Map;function a(e){if("comment"!==e.type){if(0===e.childCount){const a=t.slice(e.startIndex,e.endIndex);return void(o.has(a)||o.has(e.type)?v(n,a||e.type):i.has(e.type)&&v(r,a))}o.has(e.type)&&v(n,e.type);for(const t of e.children)a(t)}}a(e);const s=n.size,c=r.size,u=N(n.values()),l=N(r.values()),f=s+c,p=u+l,m=0===f?0:p*Math.log2(f),d=0===c?0:s/2*(l/c),y=d*m;return{distinctOperators:s,distinctOperands:c,totalOperators:u,totalOperands:l,vocabulary:f,length:p,volume:m,difficulty:d,effort:y,time:y/18,bugs:m/3e3}}(C,t);return{language:r.name,bytes:Buffer.byteLength(t),lines:I,functions:_,classCount:u(C,new Set(r.classNodeTypes)).length,functionCount:_.length,cyclomaticComplexity:M.cyclomaticComplexity,maxCyclomaticComplexity:S(_,"cyclomaticComplexity"),cognitiveComplexity:M.cognitiveComplexity,maxCognitiveComplexity:S(_,"cognitiveComplexity"),nestingDepth:M.nestingDepth,callGraph:x.callGraph,coupling:x.coupling,cohesion:x.cohesion,typeComplexity:x.typeComplexity,halstead:z,maintainabilityIndex:w(z.volume,M.cyclomaticComplexity,I.code),syntaxTree:n.includeSyntaxTree?C.toString():void 0}}}const a=new r;function s(e,t,o){let i=1,r=0,a=o;const s=new Set(t.decisionNodeTypes),c=new Set(t.nestingNodeTypes);function u(e,t){const o=s.has(e.type),l=c.has(e.type);o&&(i+=1,r+=1+t),function(e){if(e.isNamed)return!1;return n.has(e.text)}(e)&&(i+=1,r+=1);const f=l?t+1:t;a=Math.max(a,f);for(const t of e.children)u(t,f)}for(const t of e.children)u(t,o);return{cyclomaticComplexity:i,cognitiveComplexity:r,nestingDepth:a}}function c(e){const t=new Set;return function e(n){"identifier"!==n.type&&"property_identifier"!==n.type&&"field_identifier"!==n.type||t.add(n.text);for(const t of n.namedChildren)e(t)}(e),t}function u(e,t){const n=[];return function e(o){t.has(o.type)&&n.push(o);for(const t of o.namedChildren)e(t)}(e),n}function l(e){const t=new Set;let n=0,o=0,i=0;return function e(r){if(function(e){return"import_statement"===e.type||"import_declaration"===e.type||"import_from_statement"===e.type||"import_spec"===e.type||"import_spec_list"===e.type}(r)){n+=1;const e=function(e){const t=C(e);return t?(n=t.text,n.replaceAll(/^['"`]|['"`]$/gu,"")):void 0;var n}(r);e&&(t.add(e),(e.startsWith(".")||e.startsWith("/"))&&(i+=1))}(function(e){return e.type.startsWith("export")||"public_field_definition"===e.type})(r)&&(o+=1);for(const t of r.namedChildren)e(t)}(e),{importCount:n,importSourceCount:t.size,relativeImportCount:i,externalImportCount:t.size-i,exportCount:o}}function f(e){const t=new Set,n=new Set;let o=0,i=0;for(const n of e)for(const e of n.identifiers)t.add(e);for(let t=0;t<e.length;t+=1)for(let r=t+1;r<e.length;r+=1){const a=e[t],s=e[r];if(!a||!s)continue;const c=_(a.identifiers,s.identifiers),u=new Set([...a.identifiers,...s.identifiers]).size;for(const e of c)n.add(e);o+=0===u?0:c.size/u,i+=1}return{averageFunctionIdentifierOverlap:0===i?1:o/i,sharedIdentifierCount:n.size,uniqueIdentifierCount:t.size}}function p(e){const t={typeAnnotationCount:0,typeAliasCount:0,interfaceCount:0,genericParameterCount:0,unionTypeCount:0,intersectionTypeCount:0,conditionalTypeCount:0,typeAssertionCount:0,nonNullAssertionCount:0,satisfiesExpressionCount:0};return function e(n){switch(n.type){case"type_annotation":t.typeAnnotationCount+=1;break;case"type_alias_declaration":t.typeAliasCount+=1;break;case"interface_declaration":t.interfaceCount+=1;break;case"type_parameters":case"type_parameter":t.genericParameterCount+="type_parameter"===n.type?1:0;break;case"union_type":t.unionTypeCount+=1;break;case"intersection_type":t.intersectionTypeCount+=1;break;case"conditional_type":t.conditionalTypeCount+=1;break;case"as_expression":case"type_assertion":t.typeAssertionCount+=1;break;case"non_null_expression":t.nonNullAssertionCount+=1;break;case"satisfies_expression":t.satisfiesExpressionCount+=1}for(const t of n.namedChildren)e(t)}(e),t}function m(e,t,n){const o=n.filter(e=>e.line===t);if(0===o.length)return!1;const i=e.search(/\S/),r=e.trimEnd().length;return o.some(e=>e.startColumn<=i&&e.endColumn>=r)}function d(e){const t=e.childForFieldName("name");if(t)return t.text;const n=e.parent;if(!n)return;const o=n.childForFieldName("name");return o?.text}function y(e){if("identifier"===e.type||"property_identifier"===e.type||"field_identifier"===e.type||"attribute"===e.type)return e.text;for(let t=e.namedChildCount-1;t>=0;t-=1){const n=e.namedChild(t);if(!n)continue;const o=y(n);if(o)return o}}function C(e){if("string"===e.type||"string_literal"===e.type||"interpreted_string_literal"===e.type)return e;for(const t of e.namedChildren){const e=C(t);if(e)return e}}function g(e,t,n,o){const i=n.get(e);if(!i)return!1;for(const e of i){if(e===t)return!0;if(!o.has(e)&&(o.add(e),g(e,t,n,o)))return!0}return!1}function h(e){let t=0;for(const n of e.keys())t=Math.max(t,x(n,e,new Set));return t}function x(e,t,n){const o=t.get(e);if(!o||0===o.size||n.has(e))return 0;n.add(e);let i=0;for(const e of o)i=Math.max(i,1+x(e,t,new Set(n)));return i}function _(e,t){const n=new Set;for(const o of e)t.has(o)&&n.add(o);return n}function w(e,t,n){if(0===n)return 100;const o=171-5.2*Math.log(Math.max(e,1))-.23*t-16.2*Math.log(n);return Math.max(0,Math.min(100,100*o/171))}function v(e,t){e.set(t,(e.get(t)??0)+1)}function S(e,t){return 0===e.length?0:Math.max(...e.map(e=>e[t]))}function b(e){let t=0;for(const n of e.values())t=Math.max(t,n);return t}function N(e){let t=0;for(const n of e)t+=n;return t}exports.TreeMeasurer=r,exports.defaultMeasurer=a,exports.measureCode=function(e,t){return a.measure(e,t)};
2
2
  //# sourceMappingURL=metrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","sources":["../src/metrics.ts"],"sourcesContent":["import Parser from 'tree-sitter';\nimport { createLanguageRegistry } from './languages.js';\nimport type {\n CodeMetrics,\n FunctionMetrics,\n HalsteadMetrics,\n LanguageDefinition,\n LanguageName,\n MeasureOptions,\n} from './types.js';\n\nconst booleanOperators = new Set(['&&', '||', 'and', 'or']);\nconst operatorTexts = new Set([\n '+',\n '-',\n '*',\n '/',\n '%',\n '**',\n '=',\n '+=',\n '-=',\n '*=',\n '/=',\n '%=',\n '==',\n '!=',\n '===',\n '!==',\n '<',\n '<=',\n '>',\n '>=',\n '!',\n '~',\n '&',\n '|',\n '^',\n '<<',\n '>>',\n '=>',\n 'return',\n 'throw',\n 'yield',\n 'await',\n 'break',\n 'continue',\n]);\n\nconst operandNodeTypes = new Set([\n 'identifier',\n 'property_identifier',\n 'field_identifier',\n 'type_identifier',\n 'number',\n 'integer',\n 'float',\n 'string',\n 'string_literal',\n 'template_string',\n 'character_literal',\n 'true',\n 'false',\n 'null',\n 'undefined',\n 'nil',\n]);\n\ninterface ComplexityResult {\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n nestingDepth: number;\n}\n\ninterface CommentSpan {\n line: number;\n startColumn: number;\n endColumn: number;\n}\n\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n registerLanguage(language: LanguageDefinition): void {\n this.registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n this.registry.set(alias, language);\n }\n }\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.registry.get(options.language);\n if (!language) {\n throw new Error(`Unsupported language: ${options.language}`);\n }\n\n const parser = new Parser();\n parser.setLanguage(language.parserLanguage);\n const tree = parser.parse(code, undefined, {\n bufferSize: code.length + 1,\n });\n const root = tree.rootNode;\n const functions = collectNodes(root, new Set(language.functionNodeTypes));\n const functionMetrics = functions.map((node) => measureFunction(node, language));\n const globalComplexity = measureComplexity(root, language, 0);\n const lines = measureLines(code, root);\n const halstead = measureHalstead(root, code);\n\n return {\n language: language.name,\n bytes: Buffer.byteLength(code),\n lines,\n functions: functionMetrics,\n classCount: collectNodes(root, new Set(language.classNodeTypes)).length,\n functionCount: functionMetrics.length,\n cyclomaticComplexity: globalComplexity.cyclomaticComplexity,\n maxCyclomaticComplexity: maxMetric(functionMetrics, 'cyclomaticComplexity'),\n cognitiveComplexity: globalComplexity.cognitiveComplexity,\n maxCognitiveComplexity: maxMetric(functionMetrics, 'cognitiveComplexity'),\n nestingDepth: globalComplexity.nestingDepth,\n halstead,\n maintainabilityIndex: calculateMaintainabilityIndex(\n halstead.volume,\n globalComplexity.cyclomaticComplexity,\n lines.code\n ),\n syntaxTree: options.includeSyntaxTree ? root.toString() : undefined,\n };\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\nfunction measureFunction(node: Parser.SyntaxNode, language: LanguageDefinition): FunctionMetrics {\n const complexity = measureComplexity(node, language, 0);\n\n return {\n name: findFunctionName(node),\n startLine: node.startPosition.row + 1,\n endLine: node.endPosition.row + 1,\n cyclomaticComplexity: complexity.cyclomaticComplexity,\n cognitiveComplexity: complexity.cognitiveComplexity,\n };\n}\n\nfunction measureComplexity(node: Parser.SyntaxNode, language: LanguageDefinition, nesting: number): ComplexityResult {\n let cyclomaticComplexity = 1;\n let cognitiveComplexity = 0;\n let nestingDepth = nesting;\n const decisionNodes = new Set(language.decisionNodeTypes);\n const nestingNodes = new Set(language.nestingNodeTypes);\n\n function visit(current: Parser.SyntaxNode, currentNesting: number): void {\n const isDecision = decisionNodes.has(current.type);\n const isNesting = nestingNodes.has(current.type);\n\n if (isDecision) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1 + currentNesting;\n }\n\n if (isBooleanOperator(current)) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1;\n }\n\n const childNesting = isNesting ? currentNesting + 1 : currentNesting;\n nestingDepth = Math.max(nestingDepth, childNesting);\n\n for (const child of current.children) {\n visit(child, childNesting);\n }\n }\n\n for (const child of node.children) {\n visit(child, nesting);\n }\n\n return { cyclomaticComplexity, cognitiveComplexity, nestingDepth };\n}\n\nfunction isBooleanOperator(node: Parser.SyntaxNode): boolean {\n if (node.isNamed) {\n return false;\n }\n\n return booleanOperators.has(node.text);\n}\n\nfunction collectNodes(root: Parser.SyntaxNode, nodeTypes: Set<string>): Parser.SyntaxNode[] {\n const nodes: Parser.SyntaxNode[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (nodeTypes.has(node.type)) {\n nodes.push(node);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return nodes;\n}\n\nfunction measureLines(code: string, root: Parser.SyntaxNode): CodeMetrics['lines'] {\n const sourceLines = code.length === 0 ? [] : code.split(/\\r\\n|\\n|\\r/);\n const commentSpans = collectCommentSpans(root);\n let blank = 0;\n let comment = 0;\n\n for (const [index, line] of sourceLines.entries()) {\n if (line.trim() === '') {\n blank += 1;\n continue;\n }\n\n if (isCommentOnlyLine(line, index, commentSpans)) {\n comment += 1;\n }\n }\n\n return {\n total: sourceLines.length,\n code: sourceLines.length - blank - comment,\n comment,\n blank,\n };\n}\n\nfunction collectCommentSpans(root: Parser.SyntaxNode): CommentSpan[] {\n const spans: CommentSpan[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment' || node.type === 'line_comment' || node.type === 'block_comment') {\n for (let row = node.startPosition.row; row <= node.endPosition.row; row += 1) {\n spans.push({\n line: row,\n startColumn: row === node.startPosition.row ? node.startPosition.column : 0,\n endColumn: row === node.endPosition.row ? node.endPosition.column : Number.POSITIVE_INFINITY,\n });\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return spans;\n}\n\nfunction isCommentOnlyLine(line: string, lineIndex: number, spans: CommentSpan[]): boolean {\n const relevantSpans = spans.filter((span) => span.line === lineIndex);\n if (relevantSpans.length === 0) {\n return false;\n }\n\n const firstContentColumn = line.search(/\\S/);\n const lastContentColumn = line.trimEnd().length;\n\n return relevantSpans.some((span) => span.startColumn <= firstContentColumn && span.endColumn >= lastContentColumn);\n}\n\nfunction measureHalstead(root: Parser.SyntaxNode, code: string): HalsteadMetrics {\n const operators = new Map<string, number>();\n const operands = new Map<string, number>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment') {\n return;\n }\n\n if (node.childCount === 0) {\n const text = code.slice(node.startIndex, node.endIndex);\n if (operatorTexts.has(text) || operatorTexts.has(node.type)) {\n incrementCount(operators, text || node.type);\n } else if (operandNodeTypes.has(node.type)) {\n incrementCount(operands, text);\n }\n return;\n }\n\n if (operatorTexts.has(node.type)) {\n incrementCount(operators, node.type);\n }\n\n for (const child of node.children) {\n visit(child);\n }\n }\n\n visit(root);\n\n const distinctOperators = operators.size;\n const distinctOperands = operands.size;\n const totalOperators = sum(operators.values());\n const totalOperands = sum(operands.values());\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 difficulty,\n effort,\n time: effort / 18,\n bugs: volume / 3000,\n };\n}\n\nfunction findFunctionName(node: Parser.SyntaxNode): string | undefined {\n const nameNode = node.childForFieldName('name');\n if (nameNode) {\n return nameNode.text;\n }\n\n const parent = node.parent;\n if (!parent) {\n return undefined;\n }\n\n const parentName = parent.childForFieldName('name');\n return parentName?.text;\n}\n\nfunction calculateMaintainabilityIndex(volume: number, complexity: number, loc: number): number {\n if (loc === 0) {\n return 100;\n }\n\n const raw = 171 - 5.2 * Math.log(Math.max(volume, 1)) - 0.23 * complexity - 16.2 * Math.log(loc);\n return Math.max(0, Math.min(100, (raw * 100) / 171));\n}\n\nfunction incrementCount(map: Map<string, number>, value: string): void {\n map.set(value, (map.get(value) ?? 0) + 1);\n}\n\nfunction maxMetric(functions: FunctionMetrics[], key: 'cyclomaticComplexity' | 'cognitiveComplexity'): number {\n return functions.length === 0 ? 0 : Math.max(...functions.map((fn) => fn[key]));\n}\n\nfunction sum(values: Iterable<number>): number {\n let total = 0;\n for (const value of values) {\n total += value;\n }\n return total;\n}\n"],"names":["booleanOperators","Set","operatorTexts","operandNodeTypes","TreeMeasurer","registry","createLanguageRegistry","registerLanguage","language","this","set","name","alias","aliases","getSupportedLanguages","values","map","measure","code","options","get","Error","parser","Parser","setLanguage","parserLanguage","root","parse","undefined","bufferSize","length","rootNode","functionMetrics","collectNodes","functionNodeTypes","node","complexity","measureComplexity","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","measureFunction","globalComplexity","lines","sourceLines","split","commentSpans","spans","visit","type","push","line","startColumn","column","endColumn","Number","POSITIVE_INFINITY","child","namedChildren","collectCommentSpans","blank","comment","index","entries","trim","isCommentOnlyLine","total","measureLines","halstead","operators","Map","operands","childCount","text","slice","startIndex","endIndex","has","incrementCount","children","distinctOperators","size","distinctOperands","totalOperators","sum","totalOperands","vocabulary","volume","Math","log2","difficulty","effort","time","bugs","measureHalstead","bytes","Buffer","byteLength","functions","classCount","classNodeTypes","functionCount","maxCyclomaticComplexity","maxMetric","maxCognitiveComplexity","nestingDepth","maintainabilityIndex","calculateMaintainabilityIndex","syntaxTree","includeSyntaxTree","toString","defaultMeasurer","nesting","decisionNodes","decisionNodeTypes","nestingNodes","nestingNodeTypes","current","currentNesting","isDecision","isNesting","isNamed","isBooleanOperator","childNesting","max","nodeTypes","nodes","lineIndex","relevantSpans","filter","span","firstContentColumn","search","lastContentColumn","trimEnd","some","nameNode","childForFieldName","parent","parentName","loc","raw","log","min","value","key","fn"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs","sources":["../src/metrics.ts"],"sourcesContent":["import Parser from 'tree-sitter';\nimport { createLanguageRegistry } from './languages.js';\nimport type {\n CallGraphMetrics,\n CodeMetrics,\n CohesionMetrics,\n CouplingMetrics,\n FunctionMetrics,\n HalsteadMetrics,\n LanguageDefinition,\n LanguageName,\n MeasureOptions,\n TypeComplexityMetrics,\n} from './types.js';\n\nconst booleanOperators = new Set(['&&', '||', 'and', 'or']);\nconst operatorTexts = new Set([\n '+',\n '-',\n '*',\n '/',\n '%',\n '**',\n '=',\n '+=',\n '-=',\n '*=',\n '/=',\n '%=',\n '==',\n '!=',\n '===',\n '!==',\n '<',\n '<=',\n '>',\n '>=',\n '!',\n '~',\n '&',\n '|',\n '^',\n '<<',\n '>>',\n '=>',\n 'return',\n 'throw',\n 'yield',\n 'await',\n 'break',\n 'continue',\n]);\n\nconst operandNodeTypes = new Set([\n 'identifier',\n 'property_identifier',\n 'field_identifier',\n 'type_identifier',\n 'number',\n 'integer',\n 'float',\n 'string',\n 'string_literal',\n 'template_string',\n 'character_literal',\n 'true',\n 'false',\n 'null',\n 'undefined',\n 'nil',\n]);\n\ninterface ComplexityResult {\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n nestingDepth: number;\n}\n\ninterface CommentSpan {\n line: number;\n startColumn: number;\n endColumn: number;\n}\n\ninterface FunctionAnalysis {\n name?: string;\n startLine: number;\n endLine: number;\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n callCount: number;\n callees: Set<string>;\n identifiers: Set<string>;\n}\n\ninterface StructuralMetrics {\n callGraph: CallGraphMetrics;\n cohesion: CohesionMetrics;\n coupling: CouplingMetrics;\n functions: FunctionMetrics[];\n typeComplexity: TypeComplexityMetrics;\n}\n\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n registerLanguage(language: LanguageDefinition): void {\n this.registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n this.registry.set(alias, language);\n }\n }\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.registry.get(options.language);\n if (!language) {\n throw new Error(`Unsupported language: ${options.language}`);\n }\n\n const parser = new Parser();\n parser.setLanguage(language.parserLanguage);\n const tree = parser.parse(code, undefined, {\n bufferSize: code.length + 1,\n });\n const root = tree.rootNode;\n const functions = collectNodes(root, new Set(language.functionNodeTypes));\n const structuralMetrics = measureStructuralMetrics(root, functions, language);\n const functionMetrics = structuralMetrics.functions;\n const globalComplexity = measureComplexity(root, language, 0);\n const lines = measureLines(code, root);\n const halstead = measureHalstead(root, code);\n\n return {\n language: language.name,\n bytes: Buffer.byteLength(code),\n lines,\n functions: functionMetrics,\n classCount: collectNodes(root, new Set(language.classNodeTypes)).length,\n functionCount: functionMetrics.length,\n cyclomaticComplexity: globalComplexity.cyclomaticComplexity,\n maxCyclomaticComplexity: maxMetric(functionMetrics, 'cyclomaticComplexity'),\n cognitiveComplexity: globalComplexity.cognitiveComplexity,\n maxCognitiveComplexity: maxMetric(functionMetrics, 'cognitiveComplexity'),\n nestingDepth: globalComplexity.nestingDepth,\n callGraph: structuralMetrics.callGraph,\n coupling: structuralMetrics.coupling,\n cohesion: structuralMetrics.cohesion,\n typeComplexity: structuralMetrics.typeComplexity,\n halstead,\n maintainabilityIndex: calculateMaintainabilityIndex(\n halstead.volume,\n globalComplexity.cyclomaticComplexity,\n lines.code\n ),\n syntaxTree: options.includeSyntaxTree ? root.toString() : undefined,\n };\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\nfunction measureStructuralMetrics(\n root: Parser.SyntaxNode,\n functions: Parser.SyntaxNode[],\n language: LanguageDefinition\n): StructuralMetrics {\n const analyses = functions.map((node) => analyzeFunction(node, language));\n const callGraph = measureCallGraph(analyses);\n const functionsWithGraph = analyses.map((analysis) => ({\n name: analysis.name,\n startLine: analysis.startLine,\n endLine: analysis.endLine,\n cyclomaticComplexity: analysis.cyclomaticComplexity,\n cognitiveComplexity: analysis.cognitiveComplexity,\n callCount: analysis.callCount,\n uniqueCalleeCount: analysis.callees.size,\n fanIn: callGraph.fanInByName.get(analysis.name ?? '') ?? 0,\n fanOut: callGraph.fanOutByName.get(analysis.name ?? '') ?? 0,\n recursive: callGraph.recursiveNames.has(analysis.name ?? ''),\n }));\n\n return {\n functions: functionsWithGraph,\n callGraph: callGraph.metrics,\n coupling: measureCoupling(root),\n cohesion: measureCohesion(analyses),\n typeComplexity: measureTypeComplexity(root),\n };\n}\n\nfunction analyzeFunction(node: Parser.SyntaxNode, language: LanguageDefinition): FunctionAnalysis {\n const complexity = measureComplexity(node, language, 0);\n const calls = collectCalls(node);\n return {\n name: findFunctionName(node),\n startLine: node.startPosition.row + 1,\n endLine: node.endPosition.row + 1,\n cyclomaticComplexity: complexity.cyclomaticComplexity,\n cognitiveComplexity: complexity.cognitiveComplexity,\n callCount: calls.callCount,\n callees: calls.callees,\n identifiers: collectIdentifiers(node),\n };\n}\n\nfunction measureCallGraph(analyses: FunctionAnalysis[]): {\n fanInByName: Map<string, number>;\n fanOutByName: Map<string, number>;\n metrics: CallGraphMetrics;\n recursiveNames: Set<string>;\n} {\n const functionNames = new Set(analyses.map((analysis) => analysis.name).filter((name) => name !== undefined));\n const fanInByName = new Map<string, number>();\n const fanOutByName = new Map<string, number>();\n const graph = new Map<string, Set<string>>();\n let callCount = 0;\n let internalCallCount = 0;\n const allCallees = new Set<string>();\n\n for (const analysis of analyses) {\n callCount += analysis.callCount;\n for (const callee of analysis.callees) {\n allCallees.add(callee);\n }\n\n if (!analysis.name) {\n continue;\n }\n\n const internalCallees = new Set([...analysis.callees].filter((callee) => functionNames.has(callee)));\n graph.set(analysis.name, internalCallees);\n fanOutByName.set(analysis.name, internalCallees.size);\n for (const callee of internalCallees) {\n fanInByName.set(callee, (fanInByName.get(callee) ?? 0) + 1);\n internalCallCount += 1;\n }\n }\n\n const recursiveNames = findRecursiveNames(graph);\n\n return {\n fanInByName,\n fanOutByName,\n recursiveNames,\n metrics: {\n callCount,\n uniqueCalleeCount: allCallees.size,\n internalCallCount,\n internalEdgeCount: sum([...graph.values()].map((callees) => callees.size)),\n recursiveFunctionCount: recursiveNames.size,\n maxFanIn: maxMapValue(fanInByName),\n maxFanOut: maxMapValue(fanOutByName),\n maxCallDepth: measureMaxCallDepth(graph),\n },\n };\n}\n\nfunction measureComplexity(node: Parser.SyntaxNode, language: LanguageDefinition, nesting: number): ComplexityResult {\n let cyclomaticComplexity = 1;\n let cognitiveComplexity = 0;\n let nestingDepth = nesting;\n const decisionNodes = new Set(language.decisionNodeTypes);\n const nestingNodes = new Set(language.nestingNodeTypes);\n\n function visit(current: Parser.SyntaxNode, currentNesting: number): void {\n const isDecision = decisionNodes.has(current.type);\n const isNesting = nestingNodes.has(current.type);\n\n if (isDecision) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1 + currentNesting;\n }\n\n if (isBooleanOperator(current)) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1;\n }\n\n const childNesting = isNesting ? currentNesting + 1 : currentNesting;\n nestingDepth = Math.max(nestingDepth, childNesting);\n\n for (const child of current.children) {\n visit(child, childNesting);\n }\n }\n\n for (const child of node.children) {\n visit(child, nesting);\n }\n\n return { cyclomaticComplexity, cognitiveComplexity, nestingDepth };\n}\n\nfunction isBooleanOperator(node: Parser.SyntaxNode): boolean {\n if (node.isNamed) {\n return false;\n }\n\n return booleanOperators.has(node.text);\n}\n\nfunction collectCalls(root: Parser.SyntaxNode): { callCount: number; callees: Set<string> } {\n const callees = new Set<string>();\n let callCount = 0;\n\n function visit(node: Parser.SyntaxNode): void {\n if (isCallNode(node)) {\n callCount += 1;\n const callee = findCalleeName(node);\n if (callee) {\n callees.add(callee);\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return { callCount, callees };\n}\n\nfunction collectIdentifiers(root: Parser.SyntaxNode): Set<string> {\n const identifiers = new Set<string>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'identifier' || node.type === 'property_identifier' || node.type === 'field_identifier') {\n identifiers.add(node.text);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return identifiers;\n}\n\nfunction collectNodes(root: Parser.SyntaxNode, nodeTypes: Set<string>): Parser.SyntaxNode[] {\n const nodes: Parser.SyntaxNode[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (nodeTypes.has(node.type)) {\n nodes.push(node);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return nodes;\n}\n\nfunction measureCoupling(root: Parser.SyntaxNode): CouplingMetrics {\n const importSources = new Set<string>();\n let importCount = 0;\n let exportCount = 0;\n let relativeImportCount = 0;\n\n function visit(node: Parser.SyntaxNode): void {\n if (isImportNode(node)) {\n importCount += 1;\n const source = findImportSource(node);\n if (source) {\n importSources.add(source);\n if (source.startsWith('.') || source.startsWith('/')) {\n relativeImportCount += 1;\n }\n }\n }\n\n if (isExportNode(node)) {\n exportCount += 1;\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n\n return {\n importCount,\n importSourceCount: importSources.size,\n relativeImportCount,\n externalImportCount: importSources.size - relativeImportCount,\n exportCount,\n };\n}\n\nfunction measureCohesion(analyses: FunctionAnalysis[]): CohesionMetrics {\n const allIdentifiers = new Set<string>();\n const sharedIdentifiers = new Set<string>();\n let overlapTotal = 0;\n let pairCount = 0;\n\n for (const analysis of analyses) {\n for (const identifier of analysis.identifiers) {\n allIdentifiers.add(identifier);\n }\n }\n\n for (let leftIndex = 0; leftIndex < analyses.length; leftIndex += 1) {\n for (let rightIndex = leftIndex + 1; rightIndex < analyses.length; rightIndex += 1) {\n const left = analyses[leftIndex];\n const right = analyses[rightIndex];\n if (!left || !right) {\n continue;\n }\n\n const intersection = intersectSets(left.identifiers, right.identifiers);\n const unionSize = new Set([...left.identifiers, ...right.identifiers]).size;\n for (const identifier of intersection) {\n sharedIdentifiers.add(identifier);\n }\n overlapTotal += unionSize === 0 ? 0 : intersection.size / unionSize;\n pairCount += 1;\n }\n }\n\n return {\n averageFunctionIdentifierOverlap: pairCount === 0 ? 1 : overlapTotal / pairCount,\n sharedIdentifierCount: sharedIdentifiers.size,\n uniqueIdentifierCount: allIdentifiers.size,\n };\n}\n\nfunction measureTypeComplexity(root: Parser.SyntaxNode): TypeComplexityMetrics {\n const metrics: TypeComplexityMetrics = {\n typeAnnotationCount: 0,\n typeAliasCount: 0,\n interfaceCount: 0,\n genericParameterCount: 0,\n unionTypeCount: 0,\n intersectionTypeCount: 0,\n conditionalTypeCount: 0,\n typeAssertionCount: 0,\n nonNullAssertionCount: 0,\n satisfiesExpressionCount: 0,\n };\n\n function visit(node: Parser.SyntaxNode): void {\n switch (node.type) {\n case 'type_annotation': {\n metrics.typeAnnotationCount += 1;\n break;\n }\n case 'type_alias_declaration': {\n metrics.typeAliasCount += 1;\n break;\n }\n case 'interface_declaration': {\n metrics.interfaceCount += 1;\n break;\n }\n case 'type_parameters':\n case 'type_parameter': {\n metrics.genericParameterCount += node.type === 'type_parameter' ? 1 : 0;\n break;\n }\n case 'union_type': {\n metrics.unionTypeCount += 1;\n break;\n }\n case 'intersection_type': {\n metrics.intersectionTypeCount += 1;\n break;\n }\n case 'conditional_type': {\n metrics.conditionalTypeCount += 1;\n break;\n }\n case 'as_expression':\n case 'type_assertion': {\n metrics.typeAssertionCount += 1;\n break;\n }\n case 'non_null_expression': {\n metrics.nonNullAssertionCount += 1;\n break;\n }\n case 'satisfies_expression': {\n metrics.satisfiesExpressionCount += 1;\n break;\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return metrics;\n}\n\nfunction measureLines(code: string, root: Parser.SyntaxNode): CodeMetrics['lines'] {\n const sourceLines = code.length === 0 ? [] : code.split(/\\r\\n|\\n|\\r/);\n const commentSpans = collectCommentSpans(root);\n let blank = 0;\n let comment = 0;\n\n for (const [index, line] of sourceLines.entries()) {\n if (line.trim() === '') {\n blank += 1;\n continue;\n }\n\n if (isCommentOnlyLine(line, index, commentSpans)) {\n comment += 1;\n }\n }\n\n return {\n total: sourceLines.length,\n code: sourceLines.length - blank - comment,\n comment,\n blank,\n };\n}\n\nfunction collectCommentSpans(root: Parser.SyntaxNode): CommentSpan[] {\n const spans: CommentSpan[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment' || node.type === 'line_comment' || node.type === 'block_comment') {\n for (let row = node.startPosition.row; row <= node.endPosition.row; row += 1) {\n spans.push({\n line: row,\n startColumn: row === node.startPosition.row ? node.startPosition.column : 0,\n endColumn: row === node.endPosition.row ? node.endPosition.column : Number.POSITIVE_INFINITY,\n });\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return spans;\n}\n\nfunction isCommentOnlyLine(line: string, lineIndex: number, spans: CommentSpan[]): boolean {\n const relevantSpans = spans.filter((span) => span.line === lineIndex);\n if (relevantSpans.length === 0) {\n return false;\n }\n\n const firstContentColumn = line.search(/\\S/);\n const lastContentColumn = line.trimEnd().length;\n\n return relevantSpans.some((span) => span.startColumn <= firstContentColumn && span.endColumn >= lastContentColumn);\n}\n\nfunction measureHalstead(root: Parser.SyntaxNode, code: string): HalsteadMetrics {\n const operators = new Map<string, number>();\n const operands = new Map<string, number>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment') {\n return;\n }\n\n if (node.childCount === 0) {\n const text = code.slice(node.startIndex, node.endIndex);\n if (operatorTexts.has(text) || operatorTexts.has(node.type)) {\n incrementCount(operators, text || node.type);\n } else if (operandNodeTypes.has(node.type)) {\n incrementCount(operands, text);\n }\n return;\n }\n\n if (operatorTexts.has(node.type)) {\n incrementCount(operators, node.type);\n }\n\n for (const child of node.children) {\n visit(child);\n }\n }\n\n visit(root);\n\n const distinctOperators = operators.size;\n const distinctOperands = operands.size;\n const totalOperators = sum(operators.values());\n const totalOperands = sum(operands.values());\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 difficulty,\n effort,\n time: effort / 18,\n bugs: volume / 3000,\n };\n}\n\nfunction findFunctionName(node: Parser.SyntaxNode): string | undefined {\n const nameNode = node.childForFieldName('name');\n if (nameNode) {\n return nameNode.text;\n }\n\n const parent = node.parent;\n if (!parent) {\n return undefined;\n }\n\n const parentName = parent.childForFieldName('name');\n return parentName?.text;\n}\n\nfunction isCallNode(node: Parser.SyntaxNode): boolean {\n return node.type === 'call_expression' || node.type === 'call';\n}\n\nfunction findCalleeName(node: Parser.SyntaxNode): string | undefined {\n const calleeNode = node.childForFieldName('function') ?? node.namedChild(0);\n if (!calleeNode) {\n return undefined;\n }\n\n return findRightmostIdentifier(calleeNode);\n}\n\nfunction findRightmostIdentifier(node: Parser.SyntaxNode): string | undefined {\n if (\n node.type === 'identifier' ||\n node.type === 'property_identifier' ||\n node.type === 'field_identifier' ||\n node.type === 'attribute'\n ) {\n return node.text;\n }\n\n for (let index = node.namedChildCount - 1; index >= 0; index -= 1) {\n const child = node.namedChild(index);\n if (!child) {\n continue;\n }\n\n const identifier = findRightmostIdentifier(child);\n if (identifier) {\n return identifier;\n }\n }\n\n return undefined;\n}\n\nfunction isImportNode(node: Parser.SyntaxNode): boolean {\n return (\n node.type === 'import_statement' ||\n node.type === 'import_declaration' ||\n node.type === 'import_from_statement' ||\n node.type === 'import_spec' ||\n node.type === 'import_spec_list'\n );\n}\n\nfunction findImportSource(node: Parser.SyntaxNode): string | undefined {\n const sourceNode = findFirstStringNode(node);\n return sourceNode ? unquote(sourceNode.text) : undefined;\n}\n\nfunction findFirstStringNode(node: Parser.SyntaxNode): Parser.SyntaxNode | undefined {\n if (node.type === 'string' || node.type === 'string_literal' || node.type === 'interpreted_string_literal') {\n return node;\n }\n\n for (const child of node.namedChildren) {\n const stringNode = findFirstStringNode(child);\n if (stringNode) {\n return stringNode;\n }\n }\n\n return undefined;\n}\n\nfunction unquote(value: string): string {\n return value.replaceAll(/^['\"`]|['\"`]$/gu, '');\n}\n\nfunction isExportNode(node: Parser.SyntaxNode): boolean {\n return node.type.startsWith('export') || node.type === 'public_field_definition';\n}\n\nfunction findRecursiveNames(graph: Map<string, Set<string>>): Set<string> {\n const recursiveNames = new Set<string>();\n\n for (const name of graph.keys()) {\n if (canReach(name, name, graph, new Set())) {\n recursiveNames.add(name);\n }\n }\n\n return recursiveNames;\n}\n\nfunction canReach(start: string, target: string, graph: Map<string, Set<string>>, visited: Set<string>): boolean {\n const callees = graph.get(start);\n if (!callees) {\n return false;\n }\n\n for (const callee of callees) {\n if (callee === target) {\n return true;\n }\n\n if (!visited.has(callee)) {\n visited.add(callee);\n if (canReach(callee, target, graph, visited)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\nfunction measureMaxCallDepth(graph: Map<string, Set<string>>): number {\n let maxDepth = 0;\n for (const name of graph.keys()) {\n maxDepth = Math.max(maxDepth, measureCallDepth(name, graph, new Set()));\n }\n return maxDepth;\n}\n\nfunction measureCallDepth(name: string, graph: Map<string, Set<string>>, pathNames: Set<string>): number {\n const callees = graph.get(name);\n if (!callees || callees.size === 0 || pathNames.has(name)) {\n return 0;\n }\n\n pathNames.add(name);\n let maxDepth = 0;\n for (const callee of callees) {\n maxDepth = Math.max(maxDepth, 1 + measureCallDepth(callee, graph, new Set(pathNames)));\n }\n return maxDepth;\n}\n\nfunction intersectSets(left: Set<string>, right: Set<string>): Set<string> {\n const intersection = new Set<string>();\n for (const value of left) {\n if (right.has(value)) {\n intersection.add(value);\n }\n }\n return intersection;\n}\n\nfunction calculateMaintainabilityIndex(volume: number, complexity: number, loc: number): number {\n if (loc === 0) {\n return 100;\n }\n\n const raw = 171 - 5.2 * Math.log(Math.max(volume, 1)) - 0.23 * complexity - 16.2 * Math.log(loc);\n return Math.max(0, Math.min(100, (raw * 100) / 171));\n}\n\nfunction incrementCount(map: Map<string, number>, value: string): void {\n map.set(value, (map.get(value) ?? 0) + 1);\n}\n\nfunction maxMetric(functions: FunctionMetrics[], key: 'cyclomaticComplexity' | 'cognitiveComplexity'): number {\n return functions.length === 0 ? 0 : Math.max(...functions.map((fn) => fn[key]));\n}\n\nfunction maxMapValue(map: Map<string, number>): number {\n let maximum = 0;\n for (const value of map.values()) {\n maximum = Math.max(maximum, value);\n }\n return maximum;\n}\n\nfunction sum(values: Iterable<number>): number {\n let total = 0;\n for (const value of values) {\n total += value;\n }\n return total;\n}\n"],"names":["booleanOperators","Set","operatorTexts","operandNodeTypes","TreeMeasurer","registry","createLanguageRegistry","registerLanguage","language","this","set","name","alias","aliases","getSupportedLanguages","values","map","measure","code","options","get","Error","parser","Parser","setLanguage","parserLanguage","root","parse","undefined","bufferSize","length","rootNode","structuralMetrics","functions","analyses","node","complexity","measureComplexity","calls","callees","callCount","visit","type","isCallNode","callee","calleeNode","childForFieldName","namedChild","findRightmostIdentifier","findCalleeName","add","child","namedChildren","collectCalls","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","identifiers","collectIdentifiers","analyzeFunction","callGraph","functionNames","analysis","filter","fanInByName","Map","fanOutByName","graph","internalCallCount","allCallees","internalCallees","has","size","recursiveNames","keys","canReach","findRecursiveNames","metrics","uniqueCalleeCount","internalEdgeCount","sum","recursiveFunctionCount","maxFanIn","maxMapValue","maxFanOut","maxCallDepth","measureMaxCallDepth","measureCallGraph","fanIn","fanOut","recursive","coupling","measureCoupling","cohesion","measureCohesion","typeComplexity","measureTypeComplexity","measureStructuralMetrics","collectNodes","functionNodeTypes","functionMetrics","globalComplexity","lines","sourceLines","split","commentSpans","spans","push","line","startColumn","column","endColumn","Number","POSITIVE_INFINITY","collectCommentSpans","blank","comment","index","entries","trim","isCommentOnlyLine","total","measureLines","halstead","operators","operands","childCount","text","slice","startIndex","endIndex","incrementCount","children","distinctOperators","distinctOperands","totalOperators","totalOperands","vocabulary","volume","Math","log2","difficulty","effort","time","bugs","measureHalstead","bytes","Buffer","byteLength","classCount","classNodeTypes","functionCount","maxCyclomaticComplexity","maxMetric","maxCognitiveComplexity","nestingDepth","maintainabilityIndex","calculateMaintainabilityIndex","syntaxTree","includeSyntaxTree","toString","defaultMeasurer","nesting","decisionNodes","decisionNodeTypes","nestingNodes","nestingNodeTypes","current","currentNesting","isDecision","isNesting","isNamed","isBooleanOperator","childNesting","max","nodeTypes","nodes","importSources","importCount","exportCount","relativeImportCount","isImportNode","source","sourceNode","findFirstStringNode","value","replaceAll","findImportSource","startsWith","isExportNode","importSourceCount","externalImportCount","allIdentifiers","sharedIdentifiers","overlapTotal","pairCount","identifier","leftIndex","rightIndex","left","right","intersection","intersectSets","unionSize","averageFunctionIdentifierOverlap","sharedIdentifierCount","uniqueIdentifierCount","typeAnnotationCount","typeAliasCount","interfaceCount","genericParameterCount","unionTypeCount","intersectionTypeCount","conditionalTypeCount","typeAssertionCount","nonNullAssertionCount","satisfiesExpressionCount","lineIndex","relevantSpans","span","firstContentColumn","search","lastContentColumn","trimEnd","some","nameNode","parent","parentName","namedChildCount","stringNode","start","target","visited","maxDepth","measureCallDepth","pathNames","loc","raw","log","min","key","fn","maximum"],"mappings":"uEAeA,MAAMA,EAAmB,IAAIC,IAAI,CAAC,KAAM,KAAM,MAAO,OAC/CC,EAAgB,IAAID,IAAI,CAC5B,IACA,IACA,IACA,IACA,IACA,KACA,IACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,MACA,MACA,IACA,KACA,IACA,KACA,IACA,IACA,IACA,IACA,IACA,KACA,KACA,KACA,SACA,QACA,QACA,QACA,QACA,aAGIE,EAAmB,IAAIF,IAAI,CAC/B,aACA,sBACA,mBACA,kBACA,SACA,UACA,QACA,SACA,iBACA,kBACA,oBACA,OACA,QACA,OACA,YACA,QAkCK,MAAMG,EACMC,SAAWC,EAAAA,yBAE5BC,gBAAAA,CAAiBC,GACfC,KAAKJ,SAASK,IAAIF,EAASG,KAAMH,GACjC,IAAK,MAAMI,KAASJ,EAASK,SAAW,GACtCJ,KAAKJ,SAASK,IAAIE,EAAOJ,EAE7B,CAEAM,qBAAAA,GACE,MAAO,IAAI,IAAIb,IAAI,IAAIQ,KAAKJ,SAASU,UAAUC,IAAKR,GAAaA,EAASG,OAC5E,CAEAM,OAAAA,CAAQC,EAAcC,GACpB,MAAMX,EAAWC,KAAKJ,SAASe,IAAID,EAAQX,UAC3C,IAAKA,EACH,MAAM,IAAIa,MAAM,yBAAyBF,EAAQX,YAGnD,MAAMc,EAAS,IAAIC,EACnBD,EAAOE,YAAYhB,EAASiB,gBAC5B,MAGMC,EAHOJ,EAAOK,MAAMT,OAAMU,EAAW,CACzCC,WAAYX,EAAKY,OAAS,IAEVC,SAEZC,EAuCV,SACEN,EACAO,EACAzB,GAEA,MAAM0B,EAAWD,EAAUjB,IAAKmB,GAwBlC,SAAyBA,EAAyB3B,GAChD,MAAM4B,EAAaC,EAAkBF,EAAM3B,EAAU,GAC/C8B,EA6GR,SAAsBZ,GACpB,MAAMa,EAAU,IAAItC,IACpB,IAAIuC,EAAY,EAEhB,SAASC,EAAMN,GACb,GAqUJ,SAAoBA,GAClB,MAAqB,oBAAdA,EAAKO,MAA4C,SAAdP,EAAKO,IACjD,CAvUQC,CAAWR,GAAO,CACpBK,GAAa,EACb,MAAMI,EAuUZ,SAAwBT,GACtB,MAAMU,EAAaV,EAAKW,kBAAkB,aAAeX,EAAKY,WAAW,GACzE,IAAKF,EACH,OAGF,OAAOG,EAAwBH,EACjC,CA9UqBI,CAAed,GAC1BS,GACFL,EAAQW,IAAIN,EAEhB,CAEA,IAAK,MAAMO,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAGA,OADAV,EAAMf,GACC,CAAEc,YAAWD,UACtB,CAjIgBc,CAAalB,GAC3B,MAAO,CACLxB,KAAM2C,EAAiBnB,GACvBoB,UAAWpB,EAAKqB,cAAcC,IAAM,EACpCC,QAASvB,EAAKwB,YAAYF,IAAM,EAChCG,qBAAsBxB,EAAWwB,qBACjCC,oBAAqBzB,EAAWyB,oBAChCrB,UAAWF,EAAME,UACjBD,QAASD,EAAMC,QACfuB,YAAaC,EAAmB5B,GAEpC,CArC2C6B,CAAgB7B,EAAM3B,IACzDyD,EAsCR,SAA0B/B,GAMxB,MAAMgC,EAAgB,IAAIjE,IAAIiC,EAASlB,IAAKmD,GAAaA,EAASxD,MAAMyD,OAAQzD,QAAkBiB,IAATjB,IACnF0D,EAAc,IAAIC,IAClBC,EAAe,IAAID,IACnBE,EAAQ,IAAIF,IAClB,IAAI9B,EAAY,EACZiC,EAAoB,EACxB,MAAMC,EAAa,IAAIzE,IAEvB,IAAK,MAAMkE,KAAYjC,EAAU,CAC/BM,GAAa2B,EAAS3B,UACtB,IAAK,MAAMI,KAAUuB,EAAS5B,QAC5BmC,EAAWxB,IAAIN,GAGjB,IAAKuB,EAASxD,KACZ,SAGF,MAAMgE,EAAkB,IAAI1E,IAAI,IAAIkE,EAAS5B,SAAS6B,OAAQxB,GAAWsB,EAAcU,IAAIhC,KAC3F4B,EAAM9D,IAAIyD,EAASxD,KAAMgE,GACzBJ,EAAa7D,IAAIyD,EAASxD,KAAMgE,EAAgBE,MAChD,IAAK,MAAMjC,KAAU+B,EACnBN,EAAY3D,IAAIkC,GAASyB,EAAYjD,IAAIwB,IAAW,GAAK,GACzD6B,GAAqB,CAEzB,CAEA,MAAMK,EAqdR,SAA4BN,GAC1B,MAAMM,EAAiB,IAAI7E,IAE3B,IAAK,MAAMU,KAAQ6D,EAAMO,OACnBC,EAASrE,EAAMA,EAAM6D,EAAO,IAAIvE,MAClC6E,EAAe5B,IAAIvC,GAIvB,OAAOmE,CACT,CA/dyBG,CAAmBT,GAE1C,MAAO,CACLH,cACAE,eACAO,iBACAI,QAAS,CACP1C,YACA2C,kBAAmBT,EAAWG,KAC9BJ,oBACAW,kBAAmBC,EAAI,IAAIb,EAAMzD,UAAUC,IAAKuB,GAAYA,EAAQsC,OACpES,uBAAwBR,EAAeD,KACvCU,SAAUC,EAAYnB,GACtBoB,UAAWD,EAAYjB,GACvBmB,aAAcC,EAAoBnB,IAGxC,CAxFoBoB,CAAiB1D,GAcnC,MAAO,CACLD,UAdyBC,EAASlB,IAAKmD,IAAQ,CAC/CxD,KAAMwD,EAASxD,KACf4C,UAAWY,EAASZ,UACpBG,QAASS,EAAST,QAClBE,qBAAsBO,EAASP,qBAC/BC,oBAAqBM,EAASN,oBAC9BrB,UAAW2B,EAAS3B,UACpB2C,kBAAmBhB,EAAS5B,QAAQsC,KACpCgB,MAAO5B,EAAUI,YAAYjD,IAAI+C,EAASxD,MAAQ,KAAO,EACzDmF,OAAQ7B,EAAUM,aAAanD,IAAI+C,EAASxD,MAAQ,KAAO,EAC3DoF,UAAW9B,EAAUa,eAAeF,IAAIT,EAASxD,MAAQ,OAKzDsD,UAAWA,EAAUiB,QACrBc,SAAUC,EAAgBvE,GAC1BwE,SAAUC,EAAgBjE,GAC1BkE,eAAgBC,EAAsB3E,GAE1C,CAlE8B4E,CAAyB5E,EADjC6E,EAAa7E,EAAM,IAAIzB,IAAIO,EAASgG,oBACchG,GAC9DiG,EAAkBzE,EAAkBC,UACpCyE,EAAmBrE,EAAkBX,EAAMlB,EAAU,GACrDmG,EAwXV,SAAsBzF,EAAcQ,GAClC,MAAMkF,EAA8B,IAAhB1F,EAAKY,OAAe,GAAKZ,EAAK2F,MAAM,cAClDC,EAuBR,SAA6BpF,GAC3B,MAAMqF,EAAuB,GAE7B,SAAStE,EAAMN,GACb,GAAkB,YAAdA,EAAKO,MAAoC,iBAAdP,EAAKO,MAAyC,kBAAdP,EAAKO,KAClE,IAAK,IAAIe,EAAMtB,EAAKqB,cAAcC,IAAKA,GAAOtB,EAAKwB,YAAYF,IAAKA,GAAO,EACzEsD,EAAMC,KAAK,CACTC,KAAMxD,EACNyD,YAAazD,IAAQtB,EAAKqB,cAAcC,IAAMtB,EAAKqB,cAAc2D,OAAS,EAC1EC,UAAW3D,IAAQtB,EAAKwB,YAAYF,IAAMtB,EAAKwB,YAAYwD,OAASE,OAAOC,oBAKjF,IAAK,MAAMnE,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAGA,OADAV,EAAMf,GACCqF,CACT,CA5CuBQ,CAAoB7F,GACzC,IAAI8F,EAAQ,EACRC,EAAU,EAEd,IAAK,MAAOC,EAAOT,KAASL,EAAYe,UAClB,KAAhBV,EAAKW,OAKLC,EAAkBZ,EAAMS,EAAOZ,KACjCW,GAAW,GALXD,GAAS,EASb,MAAO,CACLM,MAAOlB,EAAY9E,OACnBZ,KAAM0F,EAAY9E,OAAS0F,EAAQC,EACnCA,UACAD,QAEJ,CA/YkBO,CAAa7G,EAAMQ,GAC3BsG,EAmbV,SAAyBtG,EAAyBR,GAChD,MAAM+G,EAAY,IAAI3D,IAChB4D,EAAW,IAAI5D,IAErB,SAAS7B,EAAMN,GACb,GAAkB,YAAdA,EAAKO,KAAT,CAIA,GAAwB,IAApBP,EAAKgG,WAAkB,CACzB,MAAMC,EAAOlH,EAAKmH,MAAMlG,EAAKmG,WAAYnG,EAAKoG,UAM9C,YALIrI,EAAc0E,IAAIwD,IAASlI,EAAc0E,IAAIzC,EAAKO,MACpD8F,EAAeP,EAAWG,GAAQjG,EAAKO,MAC9BvC,EAAiByE,IAAIzC,EAAKO,OACnC8F,EAAeN,EAAUE,GAG7B,CAEIlI,EAAc0E,IAAIzC,EAAKO,OACzB8F,EAAeP,EAAW9F,EAAKO,MAGjC,IAAK,MAAMS,KAAShB,EAAKsG,SACvBhG,EAAMU,EAjBR,CAmBF,CAEAV,EAAMf,GAEN,MAAMgH,EAAoBT,EAAUpD,KAC9B8D,EAAmBT,EAASrD,KAC5B+D,EAAiBvD,EAAI4C,EAAUlH,UAC/B8H,EAAgBxD,EAAI6C,EAASnH,UAC7B+H,EAAaJ,EAAoBC,EACjC7G,EAAS8G,EAAiBC,EAC1BE,EAAwB,IAAfD,EAAmB,EAAIhH,EAASkH,KAAKC,KAAKH,GACnDI,EAAkC,IAArBP,EAAyB,EAAKD,EAAoB,GAAMG,EAAgBF,GACrFQ,EAASD,EAAaH,EAE5B,MAAO,CACLL,oBACAC,mBACAC,iBACAC,gBACAC,aACAhH,SACAiH,SACAG,aACAC,SACAC,KAAMD,EAAS,GACfE,KAAMN,EAAS,IAEnB,CAxeqBO,CAAgB5H,EAAMR,GAEvC,MAAO,CACLV,SAAUA,EAASG,KACnB4I,MAAOC,OAAOC,WAAWvI,GACzByF,QACA1E,UAAWwE,EACXiD,WAAYnD,EAAa7E,EAAM,IAAIzB,IAAIO,EAASmJ,iBAAiB7H,OACjE8H,cAAenD,EAAgB3E,OAC/B8B,qBAAsB8C,EAAiB9C,qBACvCiG,wBAAyBC,EAAUrD,EAAiB,wBACpD5C,oBAAqB6C,EAAiB7C,oBACtCkG,uBAAwBD,EAAUrD,EAAiB,uBACnDuD,aAActD,EAAiBsD,aAC/B/F,UAAWjC,EAAkBiC,UAC7B+B,SAAUhE,EAAkBgE,SAC5BE,SAAUlE,EAAkBkE,SAC5BE,eAAgBpE,EAAkBoE,eAClC4B,WACAiC,qBAAsBC,EACpBlC,EAASe,OACTrC,EAAiB9C,qBACjB+C,EAAMzF,MAERiJ,WAAYhJ,EAAQiJ,kBAAoB1I,EAAK2I,gBAAazI,EAE9D,QAGW0I,EAAkB,IAAIlK,EAsGnC,SAASiC,EAAkBF,EAAyB3B,EAA8B+J,GAChF,IAAI3G,EAAuB,EACvBC,EAAsB,EACtBmG,EAAeO,EACnB,MAAMC,EAAgB,IAAIvK,IAAIO,EAASiK,mBACjCC,EAAe,IAAIzK,IAAIO,EAASmK,kBAEtC,SAASlI,EAAMmI,EAA4BC,GACzC,MAAMC,EAAaN,EAAc5F,IAAIgG,EAAQlI,MACvCqI,EAAYL,EAAa9F,IAAIgG,EAAQlI,MAEvCoI,IACFlH,GAAwB,EACxBC,GAAuB,EAAIgH,GAuBjC,SAA2B1I,GACzB,GAAIA,EAAK6I,QACP,OAAO,EAGT,OAAOhL,EAAiB4E,IAAIzC,EAAKiG,KACnC,CA1BQ6C,CAAkBL,KACpBhH,GAAwB,EACxBC,GAAuB,GAGzB,MAAMqH,EAAeH,EAAYF,EAAiB,EAAIA,EACtDb,EAAehB,KAAKmC,IAAInB,EAAckB,GAEtC,IAAK,MAAM/H,KAASyH,EAAQnC,SAC1BhG,EAAMU,EAAO+H,EAEjB,CAEA,IAAK,MAAM/H,KAAShB,EAAKsG,SACvBhG,EAAMU,EAAOoH,GAGf,MAAO,CAAE3G,uBAAsBC,sBAAqBmG,eACtD,CAgCA,SAASjG,EAAmBrC,GAC1B,MAAMoC,EAAc,IAAI7D,IAaxB,OAXA,SAASwC,EAAMN,GACK,eAAdA,EAAKO,MAAuC,wBAAdP,EAAKO,MAAgD,qBAAdP,EAAKO,MAC5EoB,EAAYZ,IAAIf,EAAKiG,MAGvB,IAAK,MAAMjF,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACCoC,CACT,CAEA,SAASyC,EAAa7E,EAAyB0J,GAC7C,MAAMC,EAA6B,GAanC,OAXA,SAAS5I,EAAMN,GACTiJ,EAAUxG,IAAIzC,EAAKO,OACrB2I,EAAMrE,KAAK7E,GAGb,IAAK,MAAMgB,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACC2J,CACT,CAEA,SAASpF,EAAgBvE,GACvB,MAAM4J,EAAgB,IAAIrL,IAC1B,IAAIsL,EAAc,EACdC,EAAc,EACdC,EAAsB,EAyB1B,OAvBA,SAAShJ,EAAMN,GACb,GAiTJ,SAAsBA,GACpB,MACgB,qBAAdA,EAAKO,MACS,uBAAdP,EAAKO,MACS,0BAAdP,EAAKO,MACS,gBAAdP,EAAKO,MACS,qBAAdP,EAAKO,IAET,CAzTQgJ,CAAavJ,GAAO,CACtBoJ,GAAe,EACf,MAAMI,EAyTZ,SAA0BxJ,GACxB,MAAMyJ,EAAaC,EAAoB1J,GACvC,OAAOyJ,GAkBQE,EAlBaF,EAAWxD,KAmBhC0D,EAAMC,WAAW,kBAAmB,UAnBInK,EAkBjD,IAAiBkK,CAjBjB,CA5TqBE,CAAiB7J,GAC5BwJ,IACFL,EAAcpI,IAAIyI,IACdA,EAAOM,WAAW,MAAQN,EAAOM,WAAW,QAC9CR,GAAuB,GAG7B,EA0UJ,SAAsBtJ,GACpB,OAAOA,EAAKO,KAAKuJ,WAAW,WAA2B,4BAAd9J,EAAKO,IAChD,EA1UQwJ,CAAa/J,KACfqJ,GAAe,GAGjB,IAAK,MAAMrI,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GAEC,CACL6J,cACAY,kBAAmBb,EAAczG,KACjC4G,sBACAW,oBAAqBd,EAAczG,KAAO4G,EAC1CD,cAEJ,CAEA,SAASrF,EAAgBjE,GACvB,MAAMmK,EAAiB,IAAIpM,IACrBqM,EAAoB,IAAIrM,IAC9B,IAAIsM,EAAe,EACfC,EAAY,EAEhB,IAAK,MAAMrI,KAAYjC,EACrB,IAAK,MAAMuK,KAActI,EAASL,YAChCuI,EAAenJ,IAAIuJ,GAIvB,IAAK,IAAIC,EAAY,EAAGA,EAAYxK,EAASJ,OAAQ4K,GAAa,EAChE,IAAK,IAAIC,EAAaD,EAAY,EAAGC,EAAazK,EAASJ,OAAQ6K,GAAc,EAAG,CAClF,MAAMC,EAAO1K,EAASwK,GAChBG,EAAQ3K,EAASyK,GACvB,IAAKC,IAASC,EACZ,SAGF,MAAMC,EAAeC,EAAcH,EAAK9I,YAAa+I,EAAM/I,aACrDkJ,EAAY,IAAI/M,IAAI,IAAI2M,EAAK9I,eAAgB+I,EAAM/I,cAAce,KACvE,IAAK,MAAM4H,KAAcK,EACvBR,EAAkBpJ,IAAIuJ,GAExBF,GAA8B,IAAdS,EAAkB,EAAIF,EAAajI,KAAOmI,EAC1DR,GAAa,CACf,CAGF,MAAO,CACLS,iCAAgD,IAAdT,EAAkB,EAAID,EAAeC,EACvEU,sBAAuBZ,EAAkBzH,KACzCsI,sBAAuBd,EAAexH,KAE1C,CAEA,SAASwB,EAAsB3E,GAC7B,MAAMwD,EAAiC,CACrCkI,oBAAqB,EACrBC,eAAgB,EAChBC,eAAgB,EAChBC,sBAAuB,EACvBC,eAAgB,EAChBC,sBAAuB,EACvBC,qBAAsB,EACtBC,mBAAoB,EACpBC,sBAAuB,EACvBC,yBAA0B,GAuD5B,OApDA,SAASpL,EAAMN,GACb,OAAQA,EAAKO,MACX,IAAK,kBACHwC,EAAQkI,qBAAuB,EAC/B,MAEF,IAAK,yBACHlI,EAAQmI,gBAAkB,EAC1B,MAEF,IAAK,wBACHnI,EAAQoI,gBAAkB,EAC1B,MAEF,IAAK,kBACL,IAAK,iBACHpI,EAAQqI,uBAAuC,mBAAdpL,EAAKO,KAA4B,EAAI,EACtE,MAEF,IAAK,aACHwC,EAAQsI,gBAAkB,EAC1B,MAEF,IAAK,oBACHtI,EAAQuI,uBAAyB,EACjC,MAEF,IAAK,mBACHvI,EAAQwI,sBAAwB,EAChC,MAEF,IAAK,gBACL,IAAK,iBACHxI,EAAQyI,oBAAsB,EAC9B,MAEF,IAAK,sBACHzI,EAAQ0I,uBAAyB,EACjC,MAEF,IAAK,uBACH1I,EAAQ2I,0BAA4B,EAKxC,IAAK,MAAM1K,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACCwD,CACT,CAkDA,SAAS2C,EAAkBZ,EAAc6G,EAAmB/G,GAC1D,MAAMgH,EAAgBhH,EAAM3C,OAAQ4J,GAASA,EAAK/G,OAAS6G,GAC3D,GAA6B,IAAzBC,EAAcjM,OAChB,OAAO,EAGT,MAAMmM,EAAqBhH,EAAKiH,OAAO,MACjCC,EAAoBlH,EAAKmH,UAAUtM,OAEzC,OAAOiM,EAAcM,KAAML,GAASA,EAAK9G,aAAe+G,GAAsBD,EAAK5G,WAAa+G,EAClG,CAyDA,SAAS7K,EAAiBnB,GACxB,MAAMmM,EAAWnM,EAAKW,kBAAkB,QACxC,GAAIwL,EACF,OAAOA,EAASlG,KAGlB,MAAMmG,EAASpM,EAAKoM,OACpB,IAAKA,EACH,OAGF,MAAMC,EAAaD,EAAOzL,kBAAkB,QAC5C,OAAO0L,GAAYpG,IACrB,CAeA,SAASpF,EAAwBb,GAC/B,GACgB,eAAdA,EAAKO,MACS,wBAAdP,EAAKO,MACS,qBAAdP,EAAKO,MACS,cAAdP,EAAKO,KAEL,OAAOP,EAAKiG,KAGd,IAAK,IAAIV,EAAQvF,EAAKsM,gBAAkB,EAAG/G,GAAS,EAAGA,GAAS,EAAG,CACjE,MAAMvE,EAAQhB,EAAKY,WAAW2E,GAC9B,IAAKvE,EACH,SAGF,MAAMsJ,EAAazJ,EAAwBG,GAC3C,GAAIsJ,EACF,OAAOA,CAEX,CAGF,CAiBA,SAASZ,EAAoB1J,GAC3B,GAAkB,WAAdA,EAAKO,MAAmC,mBAAdP,EAAKO,MAA2C,+BAAdP,EAAKO,KACnE,OAAOP,EAGT,IAAK,MAAMgB,KAAShB,EAAKiB,cAAe,CACtC,MAAMsL,EAAa7C,EAAoB1I,GACvC,GAAIuL,EACF,OAAOA,CAEX,CAGF,CAsBA,SAAS1J,EAAS2J,EAAeC,EAAgBpK,EAAiCqK,GAChF,MAAMtM,EAAUiC,EAAMpD,IAAIuN,GAC1B,IAAKpM,EACH,OAAO,EAGT,IAAK,MAAMK,KAAUL,EAAS,CAC5B,GAAIK,IAAWgM,EACb,OAAO,EAGT,IAAKC,EAAQjK,IAAIhC,KACfiM,EAAQ3L,IAAIN,GACRoC,EAASpC,EAAQgM,EAAQpK,EAAOqK,IAClC,OAAO,CAGb,CAEA,OAAO,CACT,CAEA,SAASlJ,EAAoBnB,GAC3B,IAAIsK,EAAW,EACf,IAAK,MAAMnO,KAAQ6D,EAAMO,OACvB+J,EAAW9F,KAAKmC,IAAI2D,EAAUC,EAAiBpO,EAAM6D,EAAO,IAAIvE,MAElE,OAAO6O,CACT,CAEA,SAASC,EAAiBpO,EAAc6D,EAAiCwK,GACvE,MAAMzM,EAAUiC,EAAMpD,IAAIT,GAC1B,IAAK4B,GAA4B,IAAjBA,EAAQsC,MAAcmK,EAAUpK,IAAIjE,GAClD,OAAO,EAGTqO,EAAU9L,IAAIvC,GACd,IAAImO,EAAW,EACf,IAAK,MAAMlM,KAAUL,EACnBuM,EAAW9F,KAAKmC,IAAI2D,EAAU,EAAIC,EAAiBnM,EAAQ4B,EAAO,IAAIvE,IAAI+O,KAE5E,OAAOF,CACT,CAEA,SAAS/B,EAAcH,EAAmBC,GACxC,MAAMC,EAAe,IAAI7M,IACzB,IAAK,MAAM6L,KAASc,EACdC,EAAMjI,IAAIkH,IACZgB,EAAa5J,IAAI4I,GAGrB,OAAOgB,CACT,CAEA,SAAS5C,EAA8BnB,EAAgB3G,EAAoB6M,GACzE,GAAY,IAARA,EACF,OAAO,IAGT,MAAMC,EAAM,IAAM,IAAMlG,KAAKmG,IAAInG,KAAKmC,IAAIpC,EAAQ,IAAM,IAAO3G,EAAa,KAAO4G,KAAKmG,IAAIF,GAC5F,OAAOjG,KAAKmC,IAAI,EAAGnC,KAAKoG,IAAI,IAAY,IAANF,EAAa,KACjD,CAEA,SAAS1G,EAAexH,EAA0B8K,GAChD9K,EAAIN,IAAIoL,GAAQ9K,EAAII,IAAI0K,IAAU,GAAK,EACzC,CAEA,SAAShC,EAAU7H,EAA8BoN,GAC/C,OAA4B,IAArBpN,EAAUH,OAAe,EAAIkH,KAAKmC,OAAOlJ,EAAUjB,IAAKsO,GAAOA,EAAGD,IAC3E,CAEA,SAAS7J,EAAYxE,GACnB,IAAIuO,EAAU,EACd,IAAK,MAAMzD,KAAS9K,EAAID,SACtBwO,EAAUvG,KAAKmC,IAAIoE,EAASzD,GAE9B,OAAOyD,CACT,CAEA,SAASlK,EAAItE,GACX,IAAI+G,EAAQ,EACZ,IAAK,MAAMgE,KAAS/K,EAClB+G,GAASgE,EAEX,OAAOhE,CACT,sEAvoBO,SAAqB5G,EAAcC,GACxC,OAAOmJ,EAAgBrJ,QAAQC,EAAMC,EACvC"}
package/dist/metrics.js CHANGED
@@ -1,2 +1,2 @@
1
- import t from"tree-sitter";import{createLanguageRegistry as e}from"./languages.js";const n=new Set(["&&","||","and","or"]),o=new Set(["+","-","*","/","%","**","=","+=","-=","*=","/=","%=","==","!=","===","!==","<","<=",">",">=","!","~","&","|","^","<<",">>","=>","return","throw","yield","await","break","continue"]),i=new Set(["identifier","property_identifier","field_identifier","type_identifier","number","integer","float","string","string_literal","template_string","character_literal","true","false","null","undefined","nil"]);class r{registry=e();registerLanguage(t){this.registry.set(t.name,t);for(const e of t.aliases??[])this.registry.set(e,t)}getSupportedLanguages(){return[...new Set([...this.registry.values()].map(t=>t.name))]}measure(e,n){const r=this.registry.get(n.language);if(!r)throw new Error(`Unsupported language: ${n.language}`);const s=new t;s.setLanguage(r.parserLanguage);const a=s.parse(e,void 0,{bufferSize:e.length+1}).rootNode,y=l(a,new Set(r.functionNodeTypes)).map(t=>function(t,e){const n=c(t,e,0);return{name:m(t),startLine:t.startPosition.row+1,endLine:t.endPosition.row+1,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity}}(t,r)),h=c(a,r,0),x=function(t,e){const n=0===t.length?[]:t.split(/\r\n|\n|\r/),o=function(t){const e=[];function n(t){if("comment"===t.type||"line_comment"===t.type||"block_comment"===t.type)for(let n=t.startPosition.row;n<=t.endPosition.row;n+=1)e.push({line:n,startColumn:n===t.startPosition.row?t.startPosition.column:0,endColumn:n===t.endPosition.row?t.endPosition.column:Number.POSITIVE_INFINITY});for(const e of t.namedChildren)n(e)}return n(t),e}(e);let i=0,r=0;for(const[t,e]of n.entries())""!==e.trim()?u(e,t,o)&&(r+=1):i+=1;return{total:n.length,code:n.length-i-r,comment:r,blank:i}}(e,a),C=function(t,e){const n=new Map,r=new Map;function s(t){if("comment"!==t.type){if(0===t.childCount){const s=e.slice(t.startIndex,t.endIndex);return void(o.has(s)||o.has(t.type)?p(n,s||t.type):i.has(t.type)&&p(r,s))}o.has(t.type)&&p(n,t.type);for(const e of t.children)s(e)}}s(t);const a=n.size,c=r.size,l=d(n.values()),u=d(r.values()),m=a+c,f=l+u,g=0===m?0:f*Math.log2(m),y=0===c?0:a/2*(u/c),h=y*g;return{distinctOperators:a,distinctOperands:c,totalOperators:l,totalOperands:u,vocabulary:m,length:f,volume:g,difficulty:y,effort:h,time:h/18,bugs:g/3e3}}(a,e);return{language:r.name,bytes:Buffer.byteLength(e),lines:x,functions:y,classCount:l(a,new Set(r.classNodeTypes)).length,functionCount:y.length,cyclomaticComplexity:h.cyclomaticComplexity,maxCyclomaticComplexity:g(y,"cyclomaticComplexity"),cognitiveComplexity:h.cognitiveComplexity,maxCognitiveComplexity:g(y,"cognitiveComplexity"),nestingDepth:h.nestingDepth,halstead:C,maintainabilityIndex:f(C.volume,h.cyclomaticComplexity,x.code),syntaxTree:n.includeSyntaxTree?a.toString():void 0}}}const s=new r;function a(t,e){return s.measure(t,e)}function c(t,e,o){let i=1,r=0,s=o;const a=new Set(e.decisionNodeTypes),c=new Set(e.nestingNodeTypes);function l(t,e){const o=a.has(t.type),u=c.has(t.type);o&&(i+=1,r+=1+e),function(t){if(t.isNamed)return!1;return n.has(t.text)}(t)&&(i+=1,r+=1);const m=u?e+1:e;s=Math.max(s,m);for(const e of t.children)l(e,m)}for(const e of t.children)l(e,o);return{cyclomaticComplexity:i,cognitiveComplexity:r,nestingDepth:s}}function l(t,e){const n=[];return function t(o){e.has(o.type)&&n.push(o);for(const e of o.namedChildren)t(e)}(t),n}function u(t,e,n){const o=n.filter(t=>t.line===e);if(0===o.length)return!1;const i=t.search(/\S/),r=t.trimEnd().length;return o.some(t=>t.startColumn<=i&&t.endColumn>=r)}function m(t){const e=t.childForFieldName("name");if(e)return e.text;const n=t.parent;if(!n)return;const o=n.childForFieldName("name");return o?.text}function f(t,e,n){if(0===n)return 100;const o=171-5.2*Math.log(Math.max(t,1))-.23*e-16.2*Math.log(n);return Math.max(0,Math.min(100,100*o/171))}function p(t,e){t.set(e,(t.get(e)??0)+1)}function g(t,e){return 0===t.length?0:Math.max(...t.map(t=>t[e]))}function d(t){let e=0;for(const n of t)e+=n;return e}export{r as TreeMeasurer,s as defaultMeasurer,a as measureCode};
1
+ import e from"tree-sitter";import{createLanguageRegistry as t}from"./languages.js";const n=new Set(["&&","||","and","or"]),o=new Set(["+","-","*","/","%","**","=","+=","-=","*=","/=","%=","==","!=","===","!==","<","<=",">",">=","!","~","&","|","^","<<",">>","=>","return","throw","yield","await","break","continue"]),i=new Set(["identifier","property_identifier","field_identifier","type_identifier","number","integer","float","string","string_literal","template_string","character_literal","true","false","null","undefined","nil"]);class r{registry=t();registerLanguage(e){this.registry.set(e.name,e);for(const t of e.aliases??[])this.registry.set(t,e)}getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(t,n){const r=this.registry.get(n.language);if(!r)throw new Error(`Unsupported language: ${n.language}`);const a=new e;a.setLanguage(r.parserLanguage);const s=a.parse(t,void 0,{bufferSize:t.length+1}).rootNode,h=function(e,t,n){const o=t.map(e=>function(e,t){const n=c(e,t,0),o=function(e){const t=new Set;let n=0;function o(e){if(function(e){return"call_expression"===e.type||"call"===e.type}(e)){n+=1;const o=function(e){const t=e.childForFieldName("function")??e.namedChild(0);if(!t)return;return C(t)}(e);o&&t.add(o)}for(const t of e.namedChildren)o(t)}return o(e),{callCount:n,callees:t}}(e);return{name:y(e),startLine:e.startPosition.row+1,endLine:e.endPosition.row+1,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity,callCount:o.callCount,callees:o.callees,identifiers:l(e)}}(e,n)),i=function(e){const t=new Set(e.map(e=>e.name).filter(e=>void 0!==e)),n=new Map,o=new Map,i=new Map;let r=0,a=0;const s=new Set;for(const c of e){r+=c.callCount;for(const e of c.callees)s.add(e);if(!c.name)continue;const e=new Set([...c.callees].filter(e=>t.has(e)));i.set(c.name,e),o.set(c.name,e.size);for(const t of e)n.set(t,(n.get(t)??0)+1),a+=1}const c=function(e){const t=new Set;for(const n of e.keys())g(n,n,e,new Set)&&t.add(n);return t}(i);return{fanInByName:n,fanOutByName:o,recursiveNames:c,metrics:{callCount:r,uniqueCalleeCount:s.size,internalCallCount:a,internalEdgeCount:I([...i.values()].map(e=>e.size)),recursiveFunctionCount:c.size,maxFanIn:N(n),maxFanOut:N(o),maxCallDepth:x(i)}}}(o);return{functions:o.map(e=>({name:e.name,startLine:e.startLine,endLine:e.endLine,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,callCount:e.callCount,uniqueCalleeCount:e.callees.size,fanIn:i.fanInByName.get(e.name??"")??0,fanOut:i.fanOutByName.get(e.name??"")??0,recursive:i.recursiveNames.has(e.name??"")})),callGraph:i.metrics,coupling:f(e),cohesion:p(o),typeComplexity:m(e)}}(s,u(s,new Set(r.functionNodeTypes)),r),_=h.functions,w=c(s,r,0),M=function(e,t){const n=0===e.length?[]:e.split(/\r\n|\n|\r/),o=function(e){const t=[];function n(e){if("comment"===e.type||"line_comment"===e.type||"block_comment"===e.type)for(let n=e.startPosition.row;n<=e.endPosition.row;n+=1)t.push({line:n,startColumn:n===e.startPosition.row?e.startPosition.column:0,endColumn:n===e.endPosition.row?e.endPosition.column:Number.POSITIVE_INFINITY});for(const t of e.namedChildren)n(t)}return n(e),t}(t);let i=0,r=0;for(const[e,t]of n.entries())""!==t.trim()?d(t,e,o)&&(r+=1):i+=1;return{total:n.length,code:n.length-i-r,comment:r,blank:i}}(t,s),z=function(e,t){const n=new Map,r=new Map;function a(e){if("comment"!==e.type){if(0===e.childCount){const a=t.slice(e.startIndex,e.endIndex);return void(o.has(a)||o.has(e.type)?S(n,a||e.type):i.has(e.type)&&S(r,a))}o.has(e.type)&&S(n,e.type);for(const t of e.children)a(t)}}a(e);const s=n.size,c=r.size,l=I(n.values()),u=I(r.values()),f=s+c,p=l+u,m=0===f?0:p*Math.log2(f),d=0===c?0:s/2*(u/c),y=d*m;return{distinctOperators:s,distinctOperands:c,totalOperators:l,totalOperands:u,vocabulary:f,length:p,volume:m,difficulty:d,effort:y,time:y/18,bugs:m/3e3}}(s,t);return{language:r.name,bytes:Buffer.byteLength(t),lines:M,functions:_,classCount:u(s,new Set(r.classNodeTypes)).length,functionCount:_.length,cyclomaticComplexity:w.cyclomaticComplexity,maxCyclomaticComplexity:b(_,"cyclomaticComplexity"),cognitiveComplexity:w.cognitiveComplexity,maxCognitiveComplexity:b(_,"cognitiveComplexity"),nestingDepth:w.nestingDepth,callGraph:h.callGraph,coupling:h.coupling,cohesion:h.cohesion,typeComplexity:h.typeComplexity,halstead:z,maintainabilityIndex:v(z.volume,w.cyclomaticComplexity,M.code),syntaxTree:n.includeSyntaxTree?s.toString():void 0}}}const a=new r;function s(e,t){return a.measure(e,t)}function c(e,t,o){let i=1,r=0,a=o;const s=new Set(t.decisionNodeTypes),c=new Set(t.nestingNodeTypes);function l(e,t){const o=s.has(e.type),u=c.has(e.type);o&&(i+=1,r+=1+t),function(e){if(e.isNamed)return!1;return n.has(e.text)}(e)&&(i+=1,r+=1);const f=u?t+1:t;a=Math.max(a,f);for(const t of e.children)l(t,f)}for(const t of e.children)l(t,o);return{cyclomaticComplexity:i,cognitiveComplexity:r,nestingDepth:a}}function l(e){const t=new Set;return function e(n){"identifier"!==n.type&&"property_identifier"!==n.type&&"field_identifier"!==n.type||t.add(n.text);for(const t of n.namedChildren)e(t)}(e),t}function u(e,t){const n=[];return function e(o){t.has(o.type)&&n.push(o);for(const t of o.namedChildren)e(t)}(e),n}function f(e){const t=new Set;let n=0,o=0,i=0;return function e(r){if(function(e){return"import_statement"===e.type||"import_declaration"===e.type||"import_from_statement"===e.type||"import_spec"===e.type||"import_spec_list"===e.type}(r)){n+=1;const e=function(e){const t=h(e);return t?(n=t.text,n.replaceAll(/^['"`]|['"`]$/gu,"")):void 0;var n}(r);e&&(t.add(e),(e.startsWith(".")||e.startsWith("/"))&&(i+=1))}(function(e){return e.type.startsWith("export")||"public_field_definition"===e.type})(r)&&(o+=1);for(const t of r.namedChildren)e(t)}(e),{importCount:n,importSourceCount:t.size,relativeImportCount:i,externalImportCount:t.size-i,exportCount:o}}function p(e){const t=new Set,n=new Set;let o=0,i=0;for(const n of e)for(const e of n.identifiers)t.add(e);for(let t=0;t<e.length;t+=1)for(let r=t+1;r<e.length;r+=1){const a=e[t],s=e[r];if(!a||!s)continue;const c=w(a.identifiers,s.identifiers),l=new Set([...a.identifiers,...s.identifiers]).size;for(const e of c)n.add(e);o+=0===l?0:c.size/l,i+=1}return{averageFunctionIdentifierOverlap:0===i?1:o/i,sharedIdentifierCount:n.size,uniqueIdentifierCount:t.size}}function m(e){const t={typeAnnotationCount:0,typeAliasCount:0,interfaceCount:0,genericParameterCount:0,unionTypeCount:0,intersectionTypeCount:0,conditionalTypeCount:0,typeAssertionCount:0,nonNullAssertionCount:0,satisfiesExpressionCount:0};return function e(n){switch(n.type){case"type_annotation":t.typeAnnotationCount+=1;break;case"type_alias_declaration":t.typeAliasCount+=1;break;case"interface_declaration":t.interfaceCount+=1;break;case"type_parameters":case"type_parameter":t.genericParameterCount+="type_parameter"===n.type?1:0;break;case"union_type":t.unionTypeCount+=1;break;case"intersection_type":t.intersectionTypeCount+=1;break;case"conditional_type":t.conditionalTypeCount+=1;break;case"as_expression":case"type_assertion":t.typeAssertionCount+=1;break;case"non_null_expression":t.nonNullAssertionCount+=1;break;case"satisfies_expression":t.satisfiesExpressionCount+=1}for(const t of n.namedChildren)e(t)}(e),t}function d(e,t,n){const o=n.filter(e=>e.line===t);if(0===o.length)return!1;const i=e.search(/\S/),r=e.trimEnd().length;return o.some(e=>e.startColumn<=i&&e.endColumn>=r)}function y(e){const t=e.childForFieldName("name");if(t)return t.text;const n=e.parent;if(!n)return;const o=n.childForFieldName("name");return o?.text}function C(e){if("identifier"===e.type||"property_identifier"===e.type||"field_identifier"===e.type||"attribute"===e.type)return e.text;for(let t=e.namedChildCount-1;t>=0;t-=1){const n=e.namedChild(t);if(!n)continue;const o=C(n);if(o)return o}}function h(e){if("string"===e.type||"string_literal"===e.type||"interpreted_string_literal"===e.type)return e;for(const t of e.namedChildren){const e=h(t);if(e)return e}}function g(e,t,n,o){const i=n.get(e);if(!i)return!1;for(const e of i){if(e===t)return!0;if(!o.has(e)&&(o.add(e),g(e,t,n,o)))return!0}return!1}function x(e){let t=0;for(const n of e.keys())t=Math.max(t,_(n,e,new Set));return t}function _(e,t,n){const o=t.get(e);if(!o||0===o.size||n.has(e))return 0;n.add(e);let i=0;for(const e of o)i=Math.max(i,1+_(e,t,new Set(n)));return i}function w(e,t){const n=new Set;for(const o of e)t.has(o)&&n.add(o);return n}function v(e,t,n){if(0===n)return 100;const o=171-5.2*Math.log(Math.max(e,1))-.23*t-16.2*Math.log(n);return Math.max(0,Math.min(100,100*o/171))}function S(e,t){e.set(t,(e.get(t)??0)+1)}function b(e,t){return 0===e.length?0:Math.max(...e.map(e=>e[t]))}function N(e){let t=0;for(const n of e.values())t=Math.max(t,n);return t}function I(e){let t=0;for(const n of e)t+=n;return t}export{r as TreeMeasurer,a as defaultMeasurer,s as measureCode};
2
2
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sources":["../src/metrics.ts"],"sourcesContent":["import Parser from 'tree-sitter';\nimport { createLanguageRegistry } from './languages.js';\nimport type {\n CodeMetrics,\n FunctionMetrics,\n HalsteadMetrics,\n LanguageDefinition,\n LanguageName,\n MeasureOptions,\n} from './types.js';\n\nconst booleanOperators = new Set(['&&', '||', 'and', 'or']);\nconst operatorTexts = new Set([\n '+',\n '-',\n '*',\n '/',\n '%',\n '**',\n '=',\n '+=',\n '-=',\n '*=',\n '/=',\n '%=',\n '==',\n '!=',\n '===',\n '!==',\n '<',\n '<=',\n '>',\n '>=',\n '!',\n '~',\n '&',\n '|',\n '^',\n '<<',\n '>>',\n '=>',\n 'return',\n 'throw',\n 'yield',\n 'await',\n 'break',\n 'continue',\n]);\n\nconst operandNodeTypes = new Set([\n 'identifier',\n 'property_identifier',\n 'field_identifier',\n 'type_identifier',\n 'number',\n 'integer',\n 'float',\n 'string',\n 'string_literal',\n 'template_string',\n 'character_literal',\n 'true',\n 'false',\n 'null',\n 'undefined',\n 'nil',\n]);\n\ninterface ComplexityResult {\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n nestingDepth: number;\n}\n\ninterface CommentSpan {\n line: number;\n startColumn: number;\n endColumn: number;\n}\n\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n registerLanguage(language: LanguageDefinition): void {\n this.registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n this.registry.set(alias, language);\n }\n }\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.registry.get(options.language);\n if (!language) {\n throw new Error(`Unsupported language: ${options.language}`);\n }\n\n const parser = new Parser();\n parser.setLanguage(language.parserLanguage);\n const tree = parser.parse(code, undefined, {\n bufferSize: code.length + 1,\n });\n const root = tree.rootNode;\n const functions = collectNodes(root, new Set(language.functionNodeTypes));\n const functionMetrics = functions.map((node) => measureFunction(node, language));\n const globalComplexity = measureComplexity(root, language, 0);\n const lines = measureLines(code, root);\n const halstead = measureHalstead(root, code);\n\n return {\n language: language.name,\n bytes: Buffer.byteLength(code),\n lines,\n functions: functionMetrics,\n classCount: collectNodes(root, new Set(language.classNodeTypes)).length,\n functionCount: functionMetrics.length,\n cyclomaticComplexity: globalComplexity.cyclomaticComplexity,\n maxCyclomaticComplexity: maxMetric(functionMetrics, 'cyclomaticComplexity'),\n cognitiveComplexity: globalComplexity.cognitiveComplexity,\n maxCognitiveComplexity: maxMetric(functionMetrics, 'cognitiveComplexity'),\n nestingDepth: globalComplexity.nestingDepth,\n halstead,\n maintainabilityIndex: calculateMaintainabilityIndex(\n halstead.volume,\n globalComplexity.cyclomaticComplexity,\n lines.code\n ),\n syntaxTree: options.includeSyntaxTree ? root.toString() : undefined,\n };\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\nfunction measureFunction(node: Parser.SyntaxNode, language: LanguageDefinition): FunctionMetrics {\n const complexity = measureComplexity(node, language, 0);\n\n return {\n name: findFunctionName(node),\n startLine: node.startPosition.row + 1,\n endLine: node.endPosition.row + 1,\n cyclomaticComplexity: complexity.cyclomaticComplexity,\n cognitiveComplexity: complexity.cognitiveComplexity,\n };\n}\n\nfunction measureComplexity(node: Parser.SyntaxNode, language: LanguageDefinition, nesting: number): ComplexityResult {\n let cyclomaticComplexity = 1;\n let cognitiveComplexity = 0;\n let nestingDepth = nesting;\n const decisionNodes = new Set(language.decisionNodeTypes);\n const nestingNodes = new Set(language.nestingNodeTypes);\n\n function visit(current: Parser.SyntaxNode, currentNesting: number): void {\n const isDecision = decisionNodes.has(current.type);\n const isNesting = nestingNodes.has(current.type);\n\n if (isDecision) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1 + currentNesting;\n }\n\n if (isBooleanOperator(current)) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1;\n }\n\n const childNesting = isNesting ? currentNesting + 1 : currentNesting;\n nestingDepth = Math.max(nestingDepth, childNesting);\n\n for (const child of current.children) {\n visit(child, childNesting);\n }\n }\n\n for (const child of node.children) {\n visit(child, nesting);\n }\n\n return { cyclomaticComplexity, cognitiveComplexity, nestingDepth };\n}\n\nfunction isBooleanOperator(node: Parser.SyntaxNode): boolean {\n if (node.isNamed) {\n return false;\n }\n\n return booleanOperators.has(node.text);\n}\n\nfunction collectNodes(root: Parser.SyntaxNode, nodeTypes: Set<string>): Parser.SyntaxNode[] {\n const nodes: Parser.SyntaxNode[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (nodeTypes.has(node.type)) {\n nodes.push(node);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return nodes;\n}\n\nfunction measureLines(code: string, root: Parser.SyntaxNode): CodeMetrics['lines'] {\n const sourceLines = code.length === 0 ? [] : code.split(/\\r\\n|\\n|\\r/);\n const commentSpans = collectCommentSpans(root);\n let blank = 0;\n let comment = 0;\n\n for (const [index, line] of sourceLines.entries()) {\n if (line.trim() === '') {\n blank += 1;\n continue;\n }\n\n if (isCommentOnlyLine(line, index, commentSpans)) {\n comment += 1;\n }\n }\n\n return {\n total: sourceLines.length,\n code: sourceLines.length - blank - comment,\n comment,\n blank,\n };\n}\n\nfunction collectCommentSpans(root: Parser.SyntaxNode): CommentSpan[] {\n const spans: CommentSpan[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment' || node.type === 'line_comment' || node.type === 'block_comment') {\n for (let row = node.startPosition.row; row <= node.endPosition.row; row += 1) {\n spans.push({\n line: row,\n startColumn: row === node.startPosition.row ? node.startPosition.column : 0,\n endColumn: row === node.endPosition.row ? node.endPosition.column : Number.POSITIVE_INFINITY,\n });\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return spans;\n}\n\nfunction isCommentOnlyLine(line: string, lineIndex: number, spans: CommentSpan[]): boolean {\n const relevantSpans = spans.filter((span) => span.line === lineIndex);\n if (relevantSpans.length === 0) {\n return false;\n }\n\n const firstContentColumn = line.search(/\\S/);\n const lastContentColumn = line.trimEnd().length;\n\n return relevantSpans.some((span) => span.startColumn <= firstContentColumn && span.endColumn >= lastContentColumn);\n}\n\nfunction measureHalstead(root: Parser.SyntaxNode, code: string): HalsteadMetrics {\n const operators = new Map<string, number>();\n const operands = new Map<string, number>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment') {\n return;\n }\n\n if (node.childCount === 0) {\n const text = code.slice(node.startIndex, node.endIndex);\n if (operatorTexts.has(text) || operatorTexts.has(node.type)) {\n incrementCount(operators, text || node.type);\n } else if (operandNodeTypes.has(node.type)) {\n incrementCount(operands, text);\n }\n return;\n }\n\n if (operatorTexts.has(node.type)) {\n incrementCount(operators, node.type);\n }\n\n for (const child of node.children) {\n visit(child);\n }\n }\n\n visit(root);\n\n const distinctOperators = operators.size;\n const distinctOperands = operands.size;\n const totalOperators = sum(operators.values());\n const totalOperands = sum(operands.values());\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 difficulty,\n effort,\n time: effort / 18,\n bugs: volume / 3000,\n };\n}\n\nfunction findFunctionName(node: Parser.SyntaxNode): string | undefined {\n const nameNode = node.childForFieldName('name');\n if (nameNode) {\n return nameNode.text;\n }\n\n const parent = node.parent;\n if (!parent) {\n return undefined;\n }\n\n const parentName = parent.childForFieldName('name');\n return parentName?.text;\n}\n\nfunction calculateMaintainabilityIndex(volume: number, complexity: number, loc: number): number {\n if (loc === 0) {\n return 100;\n }\n\n const raw = 171 - 5.2 * Math.log(Math.max(volume, 1)) - 0.23 * complexity - 16.2 * Math.log(loc);\n return Math.max(0, Math.min(100, (raw * 100) / 171));\n}\n\nfunction incrementCount(map: Map<string, number>, value: string): void {\n map.set(value, (map.get(value) ?? 0) + 1);\n}\n\nfunction maxMetric(functions: FunctionMetrics[], key: 'cyclomaticComplexity' | 'cognitiveComplexity'): number {\n return functions.length === 0 ? 0 : Math.max(...functions.map((fn) => fn[key]));\n}\n\nfunction sum(values: Iterable<number>): number {\n let total = 0;\n for (const value of values) {\n total += value;\n }\n return total;\n}\n"],"names":["booleanOperators","Set","operatorTexts","operandNodeTypes","TreeMeasurer","registry","createLanguageRegistry","registerLanguage","language","this","set","name","alias","aliases","getSupportedLanguages","values","map","measure","code","options","get","Error","parser","Parser","setLanguage","parserLanguage","root","parse","undefined","bufferSize","length","rootNode","functionMetrics","collectNodes","functionNodeTypes","node","complexity","measureComplexity","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","measureFunction","globalComplexity","lines","sourceLines","split","commentSpans","spans","visit","type","push","line","startColumn","column","endColumn","Number","POSITIVE_INFINITY","child","namedChildren","collectCommentSpans","blank","comment","index","entries","trim","isCommentOnlyLine","total","measureLines","halstead","operators","Map","operands","childCount","text","slice","startIndex","endIndex","has","incrementCount","children","distinctOperators","size","distinctOperands","totalOperators","sum","totalOperands","vocabulary","volume","Math","log2","difficulty","effort","time","bugs","measureHalstead","bytes","Buffer","byteLength","functions","classCount","classNodeTypes","functionCount","maxCyclomaticComplexity","maxMetric","maxCognitiveComplexity","nestingDepth","maintainabilityIndex","calculateMaintainabilityIndex","syntaxTree","includeSyntaxTree","toString","defaultMeasurer","measureCode","nesting","decisionNodes","decisionNodeTypes","nestingNodes","nestingNodeTypes","current","currentNesting","isDecision","isNesting","isNamed","isBooleanOperator","childNesting","max","nodeTypes","nodes","lineIndex","relevantSpans","filter","span","firstContentColumn","search","lastContentColumn","trimEnd","some","nameNode","childForFieldName","parent","parentName","loc","raw","log","min","value","key","fn"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","sources":["../src/metrics.ts"],"sourcesContent":["import Parser from 'tree-sitter';\nimport { createLanguageRegistry } from './languages.js';\nimport type {\n CallGraphMetrics,\n CodeMetrics,\n CohesionMetrics,\n CouplingMetrics,\n FunctionMetrics,\n HalsteadMetrics,\n LanguageDefinition,\n LanguageName,\n MeasureOptions,\n TypeComplexityMetrics,\n} from './types.js';\n\nconst booleanOperators = new Set(['&&', '||', 'and', 'or']);\nconst operatorTexts = new Set([\n '+',\n '-',\n '*',\n '/',\n '%',\n '**',\n '=',\n '+=',\n '-=',\n '*=',\n '/=',\n '%=',\n '==',\n '!=',\n '===',\n '!==',\n '<',\n '<=',\n '>',\n '>=',\n '!',\n '~',\n '&',\n '|',\n '^',\n '<<',\n '>>',\n '=>',\n 'return',\n 'throw',\n 'yield',\n 'await',\n 'break',\n 'continue',\n]);\n\nconst operandNodeTypes = new Set([\n 'identifier',\n 'property_identifier',\n 'field_identifier',\n 'type_identifier',\n 'number',\n 'integer',\n 'float',\n 'string',\n 'string_literal',\n 'template_string',\n 'character_literal',\n 'true',\n 'false',\n 'null',\n 'undefined',\n 'nil',\n]);\n\ninterface ComplexityResult {\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n nestingDepth: number;\n}\n\ninterface CommentSpan {\n line: number;\n startColumn: number;\n endColumn: number;\n}\n\ninterface FunctionAnalysis {\n name?: string;\n startLine: number;\n endLine: number;\n cyclomaticComplexity: number;\n cognitiveComplexity: number;\n callCount: number;\n callees: Set<string>;\n identifiers: Set<string>;\n}\n\ninterface StructuralMetrics {\n callGraph: CallGraphMetrics;\n cohesion: CohesionMetrics;\n coupling: CouplingMetrics;\n functions: FunctionMetrics[];\n typeComplexity: TypeComplexityMetrics;\n}\n\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n registerLanguage(language: LanguageDefinition): void {\n this.registry.set(language.name, language);\n for (const alias of language.aliases ?? []) {\n this.registry.set(alias, language);\n }\n }\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.registry.get(options.language);\n if (!language) {\n throw new Error(`Unsupported language: ${options.language}`);\n }\n\n const parser = new Parser();\n parser.setLanguage(language.parserLanguage);\n const tree = parser.parse(code, undefined, {\n bufferSize: code.length + 1,\n });\n const root = tree.rootNode;\n const functions = collectNodes(root, new Set(language.functionNodeTypes));\n const structuralMetrics = measureStructuralMetrics(root, functions, language);\n const functionMetrics = structuralMetrics.functions;\n const globalComplexity = measureComplexity(root, language, 0);\n const lines = measureLines(code, root);\n const halstead = measureHalstead(root, code);\n\n return {\n language: language.name,\n bytes: Buffer.byteLength(code),\n lines,\n functions: functionMetrics,\n classCount: collectNodes(root, new Set(language.classNodeTypes)).length,\n functionCount: functionMetrics.length,\n cyclomaticComplexity: globalComplexity.cyclomaticComplexity,\n maxCyclomaticComplexity: maxMetric(functionMetrics, 'cyclomaticComplexity'),\n cognitiveComplexity: globalComplexity.cognitiveComplexity,\n maxCognitiveComplexity: maxMetric(functionMetrics, 'cognitiveComplexity'),\n nestingDepth: globalComplexity.nestingDepth,\n callGraph: structuralMetrics.callGraph,\n coupling: structuralMetrics.coupling,\n cohesion: structuralMetrics.cohesion,\n typeComplexity: structuralMetrics.typeComplexity,\n halstead,\n maintainabilityIndex: calculateMaintainabilityIndex(\n halstead.volume,\n globalComplexity.cyclomaticComplexity,\n lines.code\n ),\n syntaxTree: options.includeSyntaxTree ? root.toString() : undefined,\n };\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\nfunction measureStructuralMetrics(\n root: Parser.SyntaxNode,\n functions: Parser.SyntaxNode[],\n language: LanguageDefinition\n): StructuralMetrics {\n const analyses = functions.map((node) => analyzeFunction(node, language));\n const callGraph = measureCallGraph(analyses);\n const functionsWithGraph = analyses.map((analysis) => ({\n name: analysis.name,\n startLine: analysis.startLine,\n endLine: analysis.endLine,\n cyclomaticComplexity: analysis.cyclomaticComplexity,\n cognitiveComplexity: analysis.cognitiveComplexity,\n callCount: analysis.callCount,\n uniqueCalleeCount: analysis.callees.size,\n fanIn: callGraph.fanInByName.get(analysis.name ?? '') ?? 0,\n fanOut: callGraph.fanOutByName.get(analysis.name ?? '') ?? 0,\n recursive: callGraph.recursiveNames.has(analysis.name ?? ''),\n }));\n\n return {\n functions: functionsWithGraph,\n callGraph: callGraph.metrics,\n coupling: measureCoupling(root),\n cohesion: measureCohesion(analyses),\n typeComplexity: measureTypeComplexity(root),\n };\n}\n\nfunction analyzeFunction(node: Parser.SyntaxNode, language: LanguageDefinition): FunctionAnalysis {\n const complexity = measureComplexity(node, language, 0);\n const calls = collectCalls(node);\n return {\n name: findFunctionName(node),\n startLine: node.startPosition.row + 1,\n endLine: node.endPosition.row + 1,\n cyclomaticComplexity: complexity.cyclomaticComplexity,\n cognitiveComplexity: complexity.cognitiveComplexity,\n callCount: calls.callCount,\n callees: calls.callees,\n identifiers: collectIdentifiers(node),\n };\n}\n\nfunction measureCallGraph(analyses: FunctionAnalysis[]): {\n fanInByName: Map<string, number>;\n fanOutByName: Map<string, number>;\n metrics: CallGraphMetrics;\n recursiveNames: Set<string>;\n} {\n const functionNames = new Set(analyses.map((analysis) => analysis.name).filter((name) => name !== undefined));\n const fanInByName = new Map<string, number>();\n const fanOutByName = new Map<string, number>();\n const graph = new Map<string, Set<string>>();\n let callCount = 0;\n let internalCallCount = 0;\n const allCallees = new Set<string>();\n\n for (const analysis of analyses) {\n callCount += analysis.callCount;\n for (const callee of analysis.callees) {\n allCallees.add(callee);\n }\n\n if (!analysis.name) {\n continue;\n }\n\n const internalCallees = new Set([...analysis.callees].filter((callee) => functionNames.has(callee)));\n graph.set(analysis.name, internalCallees);\n fanOutByName.set(analysis.name, internalCallees.size);\n for (const callee of internalCallees) {\n fanInByName.set(callee, (fanInByName.get(callee) ?? 0) + 1);\n internalCallCount += 1;\n }\n }\n\n const recursiveNames = findRecursiveNames(graph);\n\n return {\n fanInByName,\n fanOutByName,\n recursiveNames,\n metrics: {\n callCount,\n uniqueCalleeCount: allCallees.size,\n internalCallCount,\n internalEdgeCount: sum([...graph.values()].map((callees) => callees.size)),\n recursiveFunctionCount: recursiveNames.size,\n maxFanIn: maxMapValue(fanInByName),\n maxFanOut: maxMapValue(fanOutByName),\n maxCallDepth: measureMaxCallDepth(graph),\n },\n };\n}\n\nfunction measureComplexity(node: Parser.SyntaxNode, language: LanguageDefinition, nesting: number): ComplexityResult {\n let cyclomaticComplexity = 1;\n let cognitiveComplexity = 0;\n let nestingDepth = nesting;\n const decisionNodes = new Set(language.decisionNodeTypes);\n const nestingNodes = new Set(language.nestingNodeTypes);\n\n function visit(current: Parser.SyntaxNode, currentNesting: number): void {\n const isDecision = decisionNodes.has(current.type);\n const isNesting = nestingNodes.has(current.type);\n\n if (isDecision) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1 + currentNesting;\n }\n\n if (isBooleanOperator(current)) {\n cyclomaticComplexity += 1;\n cognitiveComplexity += 1;\n }\n\n const childNesting = isNesting ? currentNesting + 1 : currentNesting;\n nestingDepth = Math.max(nestingDepth, childNesting);\n\n for (const child of current.children) {\n visit(child, childNesting);\n }\n }\n\n for (const child of node.children) {\n visit(child, nesting);\n }\n\n return { cyclomaticComplexity, cognitiveComplexity, nestingDepth };\n}\n\nfunction isBooleanOperator(node: Parser.SyntaxNode): boolean {\n if (node.isNamed) {\n return false;\n }\n\n return booleanOperators.has(node.text);\n}\n\nfunction collectCalls(root: Parser.SyntaxNode): { callCount: number; callees: Set<string> } {\n const callees = new Set<string>();\n let callCount = 0;\n\n function visit(node: Parser.SyntaxNode): void {\n if (isCallNode(node)) {\n callCount += 1;\n const callee = findCalleeName(node);\n if (callee) {\n callees.add(callee);\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return { callCount, callees };\n}\n\nfunction collectIdentifiers(root: Parser.SyntaxNode): Set<string> {\n const identifiers = new Set<string>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'identifier' || node.type === 'property_identifier' || node.type === 'field_identifier') {\n identifiers.add(node.text);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return identifiers;\n}\n\nfunction collectNodes(root: Parser.SyntaxNode, nodeTypes: Set<string>): Parser.SyntaxNode[] {\n const nodes: Parser.SyntaxNode[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (nodeTypes.has(node.type)) {\n nodes.push(node);\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return nodes;\n}\n\nfunction measureCoupling(root: Parser.SyntaxNode): CouplingMetrics {\n const importSources = new Set<string>();\n let importCount = 0;\n let exportCount = 0;\n let relativeImportCount = 0;\n\n function visit(node: Parser.SyntaxNode): void {\n if (isImportNode(node)) {\n importCount += 1;\n const source = findImportSource(node);\n if (source) {\n importSources.add(source);\n if (source.startsWith('.') || source.startsWith('/')) {\n relativeImportCount += 1;\n }\n }\n }\n\n if (isExportNode(node)) {\n exportCount += 1;\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n\n return {\n importCount,\n importSourceCount: importSources.size,\n relativeImportCount,\n externalImportCount: importSources.size - relativeImportCount,\n exportCount,\n };\n}\n\nfunction measureCohesion(analyses: FunctionAnalysis[]): CohesionMetrics {\n const allIdentifiers = new Set<string>();\n const sharedIdentifiers = new Set<string>();\n let overlapTotal = 0;\n let pairCount = 0;\n\n for (const analysis of analyses) {\n for (const identifier of analysis.identifiers) {\n allIdentifiers.add(identifier);\n }\n }\n\n for (let leftIndex = 0; leftIndex < analyses.length; leftIndex += 1) {\n for (let rightIndex = leftIndex + 1; rightIndex < analyses.length; rightIndex += 1) {\n const left = analyses[leftIndex];\n const right = analyses[rightIndex];\n if (!left || !right) {\n continue;\n }\n\n const intersection = intersectSets(left.identifiers, right.identifiers);\n const unionSize = new Set([...left.identifiers, ...right.identifiers]).size;\n for (const identifier of intersection) {\n sharedIdentifiers.add(identifier);\n }\n overlapTotal += unionSize === 0 ? 0 : intersection.size / unionSize;\n pairCount += 1;\n }\n }\n\n return {\n averageFunctionIdentifierOverlap: pairCount === 0 ? 1 : overlapTotal / pairCount,\n sharedIdentifierCount: sharedIdentifiers.size,\n uniqueIdentifierCount: allIdentifiers.size,\n };\n}\n\nfunction measureTypeComplexity(root: Parser.SyntaxNode): TypeComplexityMetrics {\n const metrics: TypeComplexityMetrics = {\n typeAnnotationCount: 0,\n typeAliasCount: 0,\n interfaceCount: 0,\n genericParameterCount: 0,\n unionTypeCount: 0,\n intersectionTypeCount: 0,\n conditionalTypeCount: 0,\n typeAssertionCount: 0,\n nonNullAssertionCount: 0,\n satisfiesExpressionCount: 0,\n };\n\n function visit(node: Parser.SyntaxNode): void {\n switch (node.type) {\n case 'type_annotation': {\n metrics.typeAnnotationCount += 1;\n break;\n }\n case 'type_alias_declaration': {\n metrics.typeAliasCount += 1;\n break;\n }\n case 'interface_declaration': {\n metrics.interfaceCount += 1;\n break;\n }\n case 'type_parameters':\n case 'type_parameter': {\n metrics.genericParameterCount += node.type === 'type_parameter' ? 1 : 0;\n break;\n }\n case 'union_type': {\n metrics.unionTypeCount += 1;\n break;\n }\n case 'intersection_type': {\n metrics.intersectionTypeCount += 1;\n break;\n }\n case 'conditional_type': {\n metrics.conditionalTypeCount += 1;\n break;\n }\n case 'as_expression':\n case 'type_assertion': {\n metrics.typeAssertionCount += 1;\n break;\n }\n case 'non_null_expression': {\n metrics.nonNullAssertionCount += 1;\n break;\n }\n case 'satisfies_expression': {\n metrics.satisfiesExpressionCount += 1;\n break;\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return metrics;\n}\n\nfunction measureLines(code: string, root: Parser.SyntaxNode): CodeMetrics['lines'] {\n const sourceLines = code.length === 0 ? [] : code.split(/\\r\\n|\\n|\\r/);\n const commentSpans = collectCommentSpans(root);\n let blank = 0;\n let comment = 0;\n\n for (const [index, line] of sourceLines.entries()) {\n if (line.trim() === '') {\n blank += 1;\n continue;\n }\n\n if (isCommentOnlyLine(line, index, commentSpans)) {\n comment += 1;\n }\n }\n\n return {\n total: sourceLines.length,\n code: sourceLines.length - blank - comment,\n comment,\n blank,\n };\n}\n\nfunction collectCommentSpans(root: Parser.SyntaxNode): CommentSpan[] {\n const spans: CommentSpan[] = [];\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment' || node.type === 'line_comment' || node.type === 'block_comment') {\n for (let row = node.startPosition.row; row <= node.endPosition.row; row += 1) {\n spans.push({\n line: row,\n startColumn: row === node.startPosition.row ? node.startPosition.column : 0,\n endColumn: row === node.endPosition.row ? node.endPosition.column : Number.POSITIVE_INFINITY,\n });\n }\n }\n\n for (const child of node.namedChildren) {\n visit(child);\n }\n }\n\n visit(root);\n return spans;\n}\n\nfunction isCommentOnlyLine(line: string, lineIndex: number, spans: CommentSpan[]): boolean {\n const relevantSpans = spans.filter((span) => span.line === lineIndex);\n if (relevantSpans.length === 0) {\n return false;\n }\n\n const firstContentColumn = line.search(/\\S/);\n const lastContentColumn = line.trimEnd().length;\n\n return relevantSpans.some((span) => span.startColumn <= firstContentColumn && span.endColumn >= lastContentColumn);\n}\n\nfunction measureHalstead(root: Parser.SyntaxNode, code: string): HalsteadMetrics {\n const operators = new Map<string, number>();\n const operands = new Map<string, number>();\n\n function visit(node: Parser.SyntaxNode): void {\n if (node.type === 'comment') {\n return;\n }\n\n if (node.childCount === 0) {\n const text = code.slice(node.startIndex, node.endIndex);\n if (operatorTexts.has(text) || operatorTexts.has(node.type)) {\n incrementCount(operators, text || node.type);\n } else if (operandNodeTypes.has(node.type)) {\n incrementCount(operands, text);\n }\n return;\n }\n\n if (operatorTexts.has(node.type)) {\n incrementCount(operators, node.type);\n }\n\n for (const child of node.children) {\n visit(child);\n }\n }\n\n visit(root);\n\n const distinctOperators = operators.size;\n const distinctOperands = operands.size;\n const totalOperators = sum(operators.values());\n const totalOperands = sum(operands.values());\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 difficulty,\n effort,\n time: effort / 18,\n bugs: volume / 3000,\n };\n}\n\nfunction findFunctionName(node: Parser.SyntaxNode): string | undefined {\n const nameNode = node.childForFieldName('name');\n if (nameNode) {\n return nameNode.text;\n }\n\n const parent = node.parent;\n if (!parent) {\n return undefined;\n }\n\n const parentName = parent.childForFieldName('name');\n return parentName?.text;\n}\n\nfunction isCallNode(node: Parser.SyntaxNode): boolean {\n return node.type === 'call_expression' || node.type === 'call';\n}\n\nfunction findCalleeName(node: Parser.SyntaxNode): string | undefined {\n const calleeNode = node.childForFieldName('function') ?? node.namedChild(0);\n if (!calleeNode) {\n return undefined;\n }\n\n return findRightmostIdentifier(calleeNode);\n}\n\nfunction findRightmostIdentifier(node: Parser.SyntaxNode): string | undefined {\n if (\n node.type === 'identifier' ||\n node.type === 'property_identifier' ||\n node.type === 'field_identifier' ||\n node.type === 'attribute'\n ) {\n return node.text;\n }\n\n for (let index = node.namedChildCount - 1; index >= 0; index -= 1) {\n const child = node.namedChild(index);\n if (!child) {\n continue;\n }\n\n const identifier = findRightmostIdentifier(child);\n if (identifier) {\n return identifier;\n }\n }\n\n return undefined;\n}\n\nfunction isImportNode(node: Parser.SyntaxNode): boolean {\n return (\n node.type === 'import_statement' ||\n node.type === 'import_declaration' ||\n node.type === 'import_from_statement' ||\n node.type === 'import_spec' ||\n node.type === 'import_spec_list'\n );\n}\n\nfunction findImportSource(node: Parser.SyntaxNode): string | undefined {\n const sourceNode = findFirstStringNode(node);\n return sourceNode ? unquote(sourceNode.text) : undefined;\n}\n\nfunction findFirstStringNode(node: Parser.SyntaxNode): Parser.SyntaxNode | undefined {\n if (node.type === 'string' || node.type === 'string_literal' || node.type === 'interpreted_string_literal') {\n return node;\n }\n\n for (const child of node.namedChildren) {\n const stringNode = findFirstStringNode(child);\n if (stringNode) {\n return stringNode;\n }\n }\n\n return undefined;\n}\n\nfunction unquote(value: string): string {\n return value.replaceAll(/^['\"`]|['\"`]$/gu, '');\n}\n\nfunction isExportNode(node: Parser.SyntaxNode): boolean {\n return node.type.startsWith('export') || node.type === 'public_field_definition';\n}\n\nfunction findRecursiveNames(graph: Map<string, Set<string>>): Set<string> {\n const recursiveNames = new Set<string>();\n\n for (const name of graph.keys()) {\n if (canReach(name, name, graph, new Set())) {\n recursiveNames.add(name);\n }\n }\n\n return recursiveNames;\n}\n\nfunction canReach(start: string, target: string, graph: Map<string, Set<string>>, visited: Set<string>): boolean {\n const callees = graph.get(start);\n if (!callees) {\n return false;\n }\n\n for (const callee of callees) {\n if (callee === target) {\n return true;\n }\n\n if (!visited.has(callee)) {\n visited.add(callee);\n if (canReach(callee, target, graph, visited)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\nfunction measureMaxCallDepth(graph: Map<string, Set<string>>): number {\n let maxDepth = 0;\n for (const name of graph.keys()) {\n maxDepth = Math.max(maxDepth, measureCallDepth(name, graph, new Set()));\n }\n return maxDepth;\n}\n\nfunction measureCallDepth(name: string, graph: Map<string, Set<string>>, pathNames: Set<string>): number {\n const callees = graph.get(name);\n if (!callees || callees.size === 0 || pathNames.has(name)) {\n return 0;\n }\n\n pathNames.add(name);\n let maxDepth = 0;\n for (const callee of callees) {\n maxDepth = Math.max(maxDepth, 1 + measureCallDepth(callee, graph, new Set(pathNames)));\n }\n return maxDepth;\n}\n\nfunction intersectSets(left: Set<string>, right: Set<string>): Set<string> {\n const intersection = new Set<string>();\n for (const value of left) {\n if (right.has(value)) {\n intersection.add(value);\n }\n }\n return intersection;\n}\n\nfunction calculateMaintainabilityIndex(volume: number, complexity: number, loc: number): number {\n if (loc === 0) {\n return 100;\n }\n\n const raw = 171 - 5.2 * Math.log(Math.max(volume, 1)) - 0.23 * complexity - 16.2 * Math.log(loc);\n return Math.max(0, Math.min(100, (raw * 100) / 171));\n}\n\nfunction incrementCount(map: Map<string, number>, value: string): void {\n map.set(value, (map.get(value) ?? 0) + 1);\n}\n\nfunction maxMetric(functions: FunctionMetrics[], key: 'cyclomaticComplexity' | 'cognitiveComplexity'): number {\n return functions.length === 0 ? 0 : Math.max(...functions.map((fn) => fn[key]));\n}\n\nfunction maxMapValue(map: Map<string, number>): number {\n let maximum = 0;\n for (const value of map.values()) {\n maximum = Math.max(maximum, value);\n }\n return maximum;\n}\n\nfunction sum(values: Iterable<number>): number {\n let total = 0;\n for (const value of values) {\n total += value;\n }\n return total;\n}\n"],"names":["booleanOperators","Set","operatorTexts","operandNodeTypes","TreeMeasurer","registry","createLanguageRegistry","registerLanguage","language","this","set","name","alias","aliases","getSupportedLanguages","values","map","measure","code","options","get","Error","parser","Parser","setLanguage","parserLanguage","root","parse","undefined","bufferSize","length","rootNode","structuralMetrics","functions","analyses","node","complexity","measureComplexity","calls","callees","callCount","visit","type","isCallNode","callee","calleeNode","childForFieldName","namedChild","findRightmostIdentifier","findCalleeName","add","child","namedChildren","collectCalls","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","identifiers","collectIdentifiers","analyzeFunction","callGraph","functionNames","analysis","filter","fanInByName","Map","fanOutByName","graph","internalCallCount","allCallees","internalCallees","has","size","recursiveNames","keys","canReach","findRecursiveNames","metrics","uniqueCalleeCount","internalEdgeCount","sum","recursiveFunctionCount","maxFanIn","maxMapValue","maxFanOut","maxCallDepth","measureMaxCallDepth","measureCallGraph","fanIn","fanOut","recursive","coupling","measureCoupling","cohesion","measureCohesion","typeComplexity","measureTypeComplexity","measureStructuralMetrics","collectNodes","functionNodeTypes","functionMetrics","globalComplexity","lines","sourceLines","split","commentSpans","spans","push","line","startColumn","column","endColumn","Number","POSITIVE_INFINITY","collectCommentSpans","blank","comment","index","entries","trim","isCommentOnlyLine","total","measureLines","halstead","operators","operands","childCount","text","slice","startIndex","endIndex","incrementCount","children","distinctOperators","distinctOperands","totalOperators","totalOperands","vocabulary","volume","Math","log2","difficulty","effort","time","bugs","measureHalstead","bytes","Buffer","byteLength","classCount","classNodeTypes","functionCount","maxCyclomaticComplexity","maxMetric","maxCognitiveComplexity","nestingDepth","maintainabilityIndex","calculateMaintainabilityIndex","syntaxTree","includeSyntaxTree","toString","defaultMeasurer","measureCode","nesting","decisionNodes","decisionNodeTypes","nestingNodes","nestingNodeTypes","current","currentNesting","isDecision","isNesting","isNamed","isBooleanOperator","childNesting","max","nodeTypes","nodes","importSources","importCount","exportCount","relativeImportCount","isImportNode","source","sourceNode","findFirstStringNode","value","replaceAll","findImportSource","startsWith","isExportNode","importSourceCount","externalImportCount","allIdentifiers","sharedIdentifiers","overlapTotal","pairCount","identifier","leftIndex","rightIndex","left","right","intersection","intersectSets","unionSize","averageFunctionIdentifierOverlap","sharedIdentifierCount","uniqueIdentifierCount","typeAnnotationCount","typeAliasCount","interfaceCount","genericParameterCount","unionTypeCount","intersectionTypeCount","conditionalTypeCount","typeAssertionCount","nonNullAssertionCount","satisfiesExpressionCount","lineIndex","relevantSpans","span","firstContentColumn","search","lastContentColumn","trimEnd","some","nameNode","parent","parentName","namedChildCount","stringNode","start","target","visited","maxDepth","measureCallDepth","pathNames","loc","raw","log","min","key","fn","maximum"],"mappings":"mFAeA,MAAMA,EAAmB,IAAIC,IAAI,CAAC,KAAM,KAAM,MAAO,OAC/CC,EAAgB,IAAID,IAAI,CAC5B,IACA,IACA,IACA,IACA,IACA,KACA,IACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,MACA,MACA,IACA,KACA,IACA,KACA,IACA,IACA,IACA,IACA,IACA,KACA,KACA,KACA,SACA,QACA,QACA,QACA,QACA,aAGIE,EAAmB,IAAIF,IAAI,CAC/B,aACA,sBACA,mBACA,kBACA,SACA,UACA,QACA,SACA,iBACA,kBACA,oBACA,OACA,QACA,OACA,YACA,QAkCK,MAAMG,EACMC,SAAWC,IAE5BC,gBAAAA,CAAiBC,GACfC,KAAKJ,SAASK,IAAIF,EAASG,KAAMH,GACjC,IAAK,MAAMI,KAASJ,EAASK,SAAW,GACtCJ,KAAKJ,SAASK,IAAIE,EAAOJ,EAE7B,CAEAM,qBAAAA,GACE,MAAO,IAAI,IAAIb,IAAI,IAAIQ,KAAKJ,SAASU,UAAUC,IAAKR,GAAaA,EAASG,OAC5E,CAEAM,OAAAA,CAAQC,EAAcC,GACpB,MAAMX,EAAWC,KAAKJ,SAASe,IAAID,EAAQX,UAC3C,IAAKA,EACH,MAAM,IAAIa,MAAM,yBAAyBF,EAAQX,YAGnD,MAAMc,EAAS,IAAIC,EACnBD,EAAOE,YAAYhB,EAASiB,gBAC5B,MAGMC,EAHOJ,EAAOK,MAAMT,OAAMU,EAAW,CACzCC,WAAYX,EAAKY,OAAS,IAEVC,SAEZC,EAuCV,SACEN,EACAO,EACAzB,GAEA,MAAM0B,EAAWD,EAAUjB,IAAKmB,GAwBlC,SAAyBA,EAAyB3B,GAChD,MAAM4B,EAAaC,EAAkBF,EAAM3B,EAAU,GAC/C8B,EA6GR,SAAsBZ,GACpB,MAAMa,EAAU,IAAItC,IACpB,IAAIuC,EAAY,EAEhB,SAASC,EAAMN,GACb,GAqUJ,SAAoBA,GAClB,MAAqB,oBAAdA,EAAKO,MAA4C,SAAdP,EAAKO,IACjD,CAvUQC,CAAWR,GAAO,CACpBK,GAAa,EACb,MAAMI,EAuUZ,SAAwBT,GACtB,MAAMU,EAAaV,EAAKW,kBAAkB,aAAeX,EAAKY,WAAW,GACzE,IAAKF,EACH,OAGF,OAAOG,EAAwBH,EACjC,CA9UqBI,CAAed,GAC1BS,GACFL,EAAQW,IAAIN,EAEhB,CAEA,IAAK,MAAMO,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAGA,OADAV,EAAMf,GACC,CAAEc,YAAWD,UACtB,CAjIgBc,CAAalB,GAC3B,MAAO,CACLxB,KAAM2C,EAAiBnB,GACvBoB,UAAWpB,EAAKqB,cAAcC,IAAM,EACpCC,QAASvB,EAAKwB,YAAYF,IAAM,EAChCG,qBAAsBxB,EAAWwB,qBACjCC,oBAAqBzB,EAAWyB,oBAChCrB,UAAWF,EAAME,UACjBD,QAASD,EAAMC,QACfuB,YAAaC,EAAmB5B,GAEpC,CArC2C6B,CAAgB7B,EAAM3B,IACzDyD,EAsCR,SAA0B/B,GAMxB,MAAMgC,EAAgB,IAAIjE,IAAIiC,EAASlB,IAAKmD,GAAaA,EAASxD,MAAMyD,OAAQzD,QAAkBiB,IAATjB,IACnF0D,EAAc,IAAIC,IAClBC,EAAe,IAAID,IACnBE,EAAQ,IAAIF,IAClB,IAAI9B,EAAY,EACZiC,EAAoB,EACxB,MAAMC,EAAa,IAAIzE,IAEvB,IAAK,MAAMkE,KAAYjC,EAAU,CAC/BM,GAAa2B,EAAS3B,UACtB,IAAK,MAAMI,KAAUuB,EAAS5B,QAC5BmC,EAAWxB,IAAIN,GAGjB,IAAKuB,EAASxD,KACZ,SAGF,MAAMgE,EAAkB,IAAI1E,IAAI,IAAIkE,EAAS5B,SAAS6B,OAAQxB,GAAWsB,EAAcU,IAAIhC,KAC3F4B,EAAM9D,IAAIyD,EAASxD,KAAMgE,GACzBJ,EAAa7D,IAAIyD,EAASxD,KAAMgE,EAAgBE,MAChD,IAAK,MAAMjC,KAAU+B,EACnBN,EAAY3D,IAAIkC,GAASyB,EAAYjD,IAAIwB,IAAW,GAAK,GACzD6B,GAAqB,CAEzB,CAEA,MAAMK,EAqdR,SAA4BN,GAC1B,MAAMM,EAAiB,IAAI7E,IAE3B,IAAK,MAAMU,KAAQ6D,EAAMO,OACnBC,EAASrE,EAAMA,EAAM6D,EAAO,IAAIvE,MAClC6E,EAAe5B,IAAIvC,GAIvB,OAAOmE,CACT,CA/dyBG,CAAmBT,GAE1C,MAAO,CACLH,cACAE,eACAO,iBACAI,QAAS,CACP1C,YACA2C,kBAAmBT,EAAWG,KAC9BJ,oBACAW,kBAAmBC,EAAI,IAAIb,EAAMzD,UAAUC,IAAKuB,GAAYA,EAAQsC,OACpES,uBAAwBR,EAAeD,KACvCU,SAAUC,EAAYnB,GACtBoB,UAAWD,EAAYjB,GACvBmB,aAAcC,EAAoBnB,IAGxC,CAxFoBoB,CAAiB1D,GAcnC,MAAO,CACLD,UAdyBC,EAASlB,IAAKmD,IAAQ,CAC/CxD,KAAMwD,EAASxD,KACf4C,UAAWY,EAASZ,UACpBG,QAASS,EAAST,QAClBE,qBAAsBO,EAASP,qBAC/BC,oBAAqBM,EAASN,oBAC9BrB,UAAW2B,EAAS3B,UACpB2C,kBAAmBhB,EAAS5B,QAAQsC,KACpCgB,MAAO5B,EAAUI,YAAYjD,IAAI+C,EAASxD,MAAQ,KAAO,EACzDmF,OAAQ7B,EAAUM,aAAanD,IAAI+C,EAASxD,MAAQ,KAAO,EAC3DoF,UAAW9B,EAAUa,eAAeF,IAAIT,EAASxD,MAAQ,OAKzDsD,UAAWA,EAAUiB,QACrBc,SAAUC,EAAgBvE,GAC1BwE,SAAUC,EAAgBjE,GAC1BkE,eAAgBC,EAAsB3E,GAE1C,CAlE8B4E,CAAyB5E,EADjC6E,EAAa7E,EAAM,IAAIzB,IAAIO,EAASgG,oBACchG,GAC9DiG,EAAkBzE,EAAkBC,UACpCyE,EAAmBrE,EAAkBX,EAAMlB,EAAU,GACrDmG,EAwXV,SAAsBzF,EAAcQ,GAClC,MAAMkF,EAA8B,IAAhB1F,EAAKY,OAAe,GAAKZ,EAAK2F,MAAM,cAClDC,EAuBR,SAA6BpF,GAC3B,MAAMqF,EAAuB,GAE7B,SAAStE,EAAMN,GACb,GAAkB,YAAdA,EAAKO,MAAoC,iBAAdP,EAAKO,MAAyC,kBAAdP,EAAKO,KAClE,IAAK,IAAIe,EAAMtB,EAAKqB,cAAcC,IAAKA,GAAOtB,EAAKwB,YAAYF,IAAKA,GAAO,EACzEsD,EAAMC,KAAK,CACTC,KAAMxD,EACNyD,YAAazD,IAAQtB,EAAKqB,cAAcC,IAAMtB,EAAKqB,cAAc2D,OAAS,EAC1EC,UAAW3D,IAAQtB,EAAKwB,YAAYF,IAAMtB,EAAKwB,YAAYwD,OAASE,OAAOC,oBAKjF,IAAK,MAAMnE,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAGA,OADAV,EAAMf,GACCqF,CACT,CA5CuBQ,CAAoB7F,GACzC,IAAI8F,EAAQ,EACRC,EAAU,EAEd,IAAK,MAAOC,EAAOT,KAASL,EAAYe,UAClB,KAAhBV,EAAKW,OAKLC,EAAkBZ,EAAMS,EAAOZ,KACjCW,GAAW,GALXD,GAAS,EASb,MAAO,CACLM,MAAOlB,EAAY9E,OACnBZ,KAAM0F,EAAY9E,OAAS0F,EAAQC,EACnCA,UACAD,QAEJ,CA/YkBO,CAAa7G,EAAMQ,GAC3BsG,EAmbV,SAAyBtG,EAAyBR,GAChD,MAAM+G,EAAY,IAAI3D,IAChB4D,EAAW,IAAI5D,IAErB,SAAS7B,EAAMN,GACb,GAAkB,YAAdA,EAAKO,KAAT,CAIA,GAAwB,IAApBP,EAAKgG,WAAkB,CACzB,MAAMC,EAAOlH,EAAKmH,MAAMlG,EAAKmG,WAAYnG,EAAKoG,UAM9C,YALIrI,EAAc0E,IAAIwD,IAASlI,EAAc0E,IAAIzC,EAAKO,MACpD8F,EAAeP,EAAWG,GAAQjG,EAAKO,MAC9BvC,EAAiByE,IAAIzC,EAAKO,OACnC8F,EAAeN,EAAUE,GAG7B,CAEIlI,EAAc0E,IAAIzC,EAAKO,OACzB8F,EAAeP,EAAW9F,EAAKO,MAGjC,IAAK,MAAMS,KAAShB,EAAKsG,SACvBhG,EAAMU,EAjBR,CAmBF,CAEAV,EAAMf,GAEN,MAAMgH,EAAoBT,EAAUpD,KAC9B8D,EAAmBT,EAASrD,KAC5B+D,EAAiBvD,EAAI4C,EAAUlH,UAC/B8H,EAAgBxD,EAAI6C,EAASnH,UAC7B+H,EAAaJ,EAAoBC,EACjC7G,EAAS8G,EAAiBC,EAC1BE,EAAwB,IAAfD,EAAmB,EAAIhH,EAASkH,KAAKC,KAAKH,GACnDI,EAAkC,IAArBP,EAAyB,EAAKD,EAAoB,GAAMG,EAAgBF,GACrFQ,EAASD,EAAaH,EAE5B,MAAO,CACLL,oBACAC,mBACAC,iBACAC,gBACAC,aACAhH,SACAiH,SACAG,aACAC,SACAC,KAAMD,EAAS,GACfE,KAAMN,EAAS,IAEnB,CAxeqBO,CAAgB5H,EAAMR,GAEvC,MAAO,CACLV,SAAUA,EAASG,KACnB4I,MAAOC,OAAOC,WAAWvI,GACzByF,QACA1E,UAAWwE,EACXiD,WAAYnD,EAAa7E,EAAM,IAAIzB,IAAIO,EAASmJ,iBAAiB7H,OACjE8H,cAAenD,EAAgB3E,OAC/B8B,qBAAsB8C,EAAiB9C,qBACvCiG,wBAAyBC,EAAUrD,EAAiB,wBACpD5C,oBAAqB6C,EAAiB7C,oBACtCkG,uBAAwBD,EAAUrD,EAAiB,uBACnDuD,aAActD,EAAiBsD,aAC/B/F,UAAWjC,EAAkBiC,UAC7B+B,SAAUhE,EAAkBgE,SAC5BE,SAAUlE,EAAkBkE,SAC5BE,eAAgBpE,EAAkBoE,eAClC4B,WACAiC,qBAAsBC,EACpBlC,EAASe,OACTrC,EAAiB9C,qBACjB+C,EAAMzF,MAERiJ,WAAYhJ,EAAQiJ,kBAAoB1I,EAAK2I,gBAAazI,EAE9D,QAGW0I,EAAkB,IAAIlK,EAE5B,SAASmK,EAAYrJ,EAAcC,GACxC,OAAOmJ,EAAgBrJ,QAAQC,EAAMC,EACvC,CAkGA,SAASkB,EAAkBF,EAAyB3B,EAA8BgK,GAChF,IAAI5G,EAAuB,EACvBC,EAAsB,EACtBmG,EAAeQ,EACnB,MAAMC,EAAgB,IAAIxK,IAAIO,EAASkK,mBACjCC,EAAe,IAAI1K,IAAIO,EAASoK,kBAEtC,SAASnI,EAAMoI,EAA4BC,GACzC,MAAMC,EAAaN,EAAc7F,IAAIiG,EAAQnI,MACvCsI,EAAYL,EAAa/F,IAAIiG,EAAQnI,MAEvCqI,IACFnH,GAAwB,EACxBC,GAAuB,EAAIiH,GAuBjC,SAA2B3I,GACzB,GAAIA,EAAK8I,QACP,OAAO,EAGT,OAAOjL,EAAiB4E,IAAIzC,EAAKiG,KACnC,CA1BQ8C,CAAkBL,KACpBjH,GAAwB,EACxBC,GAAuB,GAGzB,MAAMsH,EAAeH,EAAYF,EAAiB,EAAIA,EACtDd,EAAehB,KAAKoC,IAAIpB,EAAcmB,GAEtC,IAAK,MAAMhI,KAAS0H,EAAQpC,SAC1BhG,EAAMU,EAAOgI,EAEjB,CAEA,IAAK,MAAMhI,KAAShB,EAAKsG,SACvBhG,EAAMU,EAAOqH,GAGf,MAAO,CAAE5G,uBAAsBC,sBAAqBmG,eACtD,CAgCA,SAASjG,EAAmBrC,GAC1B,MAAMoC,EAAc,IAAI7D,IAaxB,OAXA,SAASwC,EAAMN,GACK,eAAdA,EAAKO,MAAuC,wBAAdP,EAAKO,MAAgD,qBAAdP,EAAKO,MAC5EoB,EAAYZ,IAAIf,EAAKiG,MAGvB,IAAK,MAAMjF,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACCoC,CACT,CAEA,SAASyC,EAAa7E,EAAyB2J,GAC7C,MAAMC,EAA6B,GAanC,OAXA,SAAS7I,EAAMN,GACTkJ,EAAUzG,IAAIzC,EAAKO,OACrB4I,EAAMtE,KAAK7E,GAGb,IAAK,MAAMgB,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACC4J,CACT,CAEA,SAASrF,EAAgBvE,GACvB,MAAM6J,EAAgB,IAAItL,IAC1B,IAAIuL,EAAc,EACdC,EAAc,EACdC,EAAsB,EAyB1B,OAvBA,SAASjJ,EAAMN,GACb,GAiTJ,SAAsBA,GACpB,MACgB,qBAAdA,EAAKO,MACS,uBAAdP,EAAKO,MACS,0BAAdP,EAAKO,MACS,gBAAdP,EAAKO,MACS,qBAAdP,EAAKO,IAET,CAzTQiJ,CAAaxJ,GAAO,CACtBqJ,GAAe,EACf,MAAMI,EAyTZ,SAA0BzJ,GACxB,MAAM0J,EAAaC,EAAoB3J,GACvC,OAAO0J,GAkBQE,EAlBaF,EAAWzD,KAmBhC2D,EAAMC,WAAW,kBAAmB,UAnBIpK,EAkBjD,IAAiBmK,CAjBjB,CA5TqBE,CAAiB9J,GAC5ByJ,IACFL,EAAcrI,IAAI0I,IACdA,EAAOM,WAAW,MAAQN,EAAOM,WAAW,QAC9CR,GAAuB,GAG7B,EA0UJ,SAAsBvJ,GACpB,OAAOA,EAAKO,KAAKwJ,WAAW,WAA2B,4BAAd/J,EAAKO,IAChD,EA1UQyJ,CAAahK,KACfsJ,GAAe,GAGjB,IAAK,MAAMtI,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GAEC,CACL8J,cACAY,kBAAmBb,EAAc1G,KACjC6G,sBACAW,oBAAqBd,EAAc1G,KAAO6G,EAC1CD,cAEJ,CAEA,SAAStF,EAAgBjE,GACvB,MAAMoK,EAAiB,IAAIrM,IACrBsM,EAAoB,IAAItM,IAC9B,IAAIuM,EAAe,EACfC,EAAY,EAEhB,IAAK,MAAMtI,KAAYjC,EACrB,IAAK,MAAMwK,KAAcvI,EAASL,YAChCwI,EAAepJ,IAAIwJ,GAIvB,IAAK,IAAIC,EAAY,EAAGA,EAAYzK,EAASJ,OAAQ6K,GAAa,EAChE,IAAK,IAAIC,EAAaD,EAAY,EAAGC,EAAa1K,EAASJ,OAAQ8K,GAAc,EAAG,CAClF,MAAMC,EAAO3K,EAASyK,GAChBG,EAAQ5K,EAAS0K,GACvB,IAAKC,IAASC,EACZ,SAGF,MAAMC,EAAeC,EAAcH,EAAK/I,YAAagJ,EAAMhJ,aACrDmJ,EAAY,IAAIhN,IAAI,IAAI4M,EAAK/I,eAAgBgJ,EAAMhJ,cAAce,KACvE,IAAK,MAAM6H,KAAcK,EACvBR,EAAkBrJ,IAAIwJ,GAExBF,GAA8B,IAAdS,EAAkB,EAAIF,EAAalI,KAAOoI,EAC1DR,GAAa,CACf,CAGF,MAAO,CACLS,iCAAgD,IAAdT,EAAkB,EAAID,EAAeC,EACvEU,sBAAuBZ,EAAkB1H,KACzCuI,sBAAuBd,EAAezH,KAE1C,CAEA,SAASwB,EAAsB3E,GAC7B,MAAMwD,EAAiC,CACrCmI,oBAAqB,EACrBC,eAAgB,EAChBC,eAAgB,EAChBC,sBAAuB,EACvBC,eAAgB,EAChBC,sBAAuB,EACvBC,qBAAsB,EACtBC,mBAAoB,EACpBC,sBAAuB,EACvBC,yBAA0B,GAuD5B,OApDA,SAASrL,EAAMN,GACb,OAAQA,EAAKO,MACX,IAAK,kBACHwC,EAAQmI,qBAAuB,EAC/B,MAEF,IAAK,yBACHnI,EAAQoI,gBAAkB,EAC1B,MAEF,IAAK,wBACHpI,EAAQqI,gBAAkB,EAC1B,MAEF,IAAK,kBACL,IAAK,iBACHrI,EAAQsI,uBAAuC,mBAAdrL,EAAKO,KAA4B,EAAI,EACtE,MAEF,IAAK,aACHwC,EAAQuI,gBAAkB,EAC1B,MAEF,IAAK,oBACHvI,EAAQwI,uBAAyB,EACjC,MAEF,IAAK,mBACHxI,EAAQyI,sBAAwB,EAChC,MAEF,IAAK,gBACL,IAAK,iBACHzI,EAAQ0I,oBAAsB,EAC9B,MAEF,IAAK,sBACH1I,EAAQ2I,uBAAyB,EACjC,MAEF,IAAK,uBACH3I,EAAQ4I,0BAA4B,EAKxC,IAAK,MAAM3K,KAAShB,EAAKiB,cACvBX,EAAMU,EAEV,CAEAV,CAAMf,GACCwD,CACT,CAkDA,SAAS2C,EAAkBZ,EAAc8G,EAAmBhH,GAC1D,MAAMiH,EAAgBjH,EAAM3C,OAAQ6J,GAASA,EAAKhH,OAAS8G,GAC3D,GAA6B,IAAzBC,EAAclM,OAChB,OAAO,EAGT,MAAMoM,EAAqBjH,EAAKkH,OAAO,MACjCC,EAAoBnH,EAAKoH,UAAUvM,OAEzC,OAAOkM,EAAcM,KAAML,GAASA,EAAK/G,aAAegH,GAAsBD,EAAK7G,WAAagH,EAClG,CAyDA,SAAS9K,EAAiBnB,GACxB,MAAMoM,EAAWpM,EAAKW,kBAAkB,QACxC,GAAIyL,EACF,OAAOA,EAASnG,KAGlB,MAAMoG,EAASrM,EAAKqM,OACpB,IAAKA,EACH,OAGF,MAAMC,EAAaD,EAAO1L,kBAAkB,QAC5C,OAAO2L,GAAYrG,IACrB,CAeA,SAASpF,EAAwBb,GAC/B,GACgB,eAAdA,EAAKO,MACS,wBAAdP,EAAKO,MACS,qBAAdP,EAAKO,MACS,cAAdP,EAAKO,KAEL,OAAOP,EAAKiG,KAGd,IAAK,IAAIV,EAAQvF,EAAKuM,gBAAkB,EAAGhH,GAAS,EAAGA,GAAS,EAAG,CACjE,MAAMvE,EAAQhB,EAAKY,WAAW2E,GAC9B,IAAKvE,EACH,SAGF,MAAMuJ,EAAa1J,EAAwBG,GAC3C,GAAIuJ,EACF,OAAOA,CAEX,CAGF,CAiBA,SAASZ,EAAoB3J,GAC3B,GAAkB,WAAdA,EAAKO,MAAmC,mBAAdP,EAAKO,MAA2C,+BAAdP,EAAKO,KACnE,OAAOP,EAGT,IAAK,MAAMgB,KAAShB,EAAKiB,cAAe,CACtC,MAAMuL,EAAa7C,EAAoB3I,GACvC,GAAIwL,EACF,OAAOA,CAEX,CAGF,CAsBA,SAAS3J,EAAS4J,EAAeC,EAAgBrK,EAAiCsK,GAChF,MAAMvM,EAAUiC,EAAMpD,IAAIwN,GAC1B,IAAKrM,EACH,OAAO,EAGT,IAAK,MAAMK,KAAUL,EAAS,CAC5B,GAAIK,IAAWiM,EACb,OAAO,EAGT,IAAKC,EAAQlK,IAAIhC,KACfkM,EAAQ5L,IAAIN,GACRoC,EAASpC,EAAQiM,EAAQrK,EAAOsK,IAClC,OAAO,CAGb,CAEA,OAAO,CACT,CAEA,SAASnJ,EAAoBnB,GAC3B,IAAIuK,EAAW,EACf,IAAK,MAAMpO,KAAQ6D,EAAMO,OACvBgK,EAAW/F,KAAKoC,IAAI2D,EAAUC,EAAiBrO,EAAM6D,EAAO,IAAIvE,MAElE,OAAO8O,CACT,CAEA,SAASC,EAAiBrO,EAAc6D,EAAiCyK,GACvE,MAAM1M,EAAUiC,EAAMpD,IAAIT,GAC1B,IAAK4B,GAA4B,IAAjBA,EAAQsC,MAAcoK,EAAUrK,IAAIjE,GAClD,OAAO,EAGTsO,EAAU/L,IAAIvC,GACd,IAAIoO,EAAW,EACf,IAAK,MAAMnM,KAAUL,EACnBwM,EAAW/F,KAAKoC,IAAI2D,EAAU,EAAIC,EAAiBpM,EAAQ4B,EAAO,IAAIvE,IAAIgP,KAE5E,OAAOF,CACT,CAEA,SAAS/B,EAAcH,EAAmBC,GACxC,MAAMC,EAAe,IAAI9M,IACzB,IAAK,MAAM8L,KAASc,EACdC,EAAMlI,IAAImH,IACZgB,EAAa7J,IAAI6I,GAGrB,OAAOgB,CACT,CAEA,SAAS7C,EAA8BnB,EAAgB3G,EAAoB8M,GACzE,GAAY,IAARA,EACF,OAAO,IAGT,MAAMC,EAAM,IAAM,IAAMnG,KAAKoG,IAAIpG,KAAKoC,IAAIrC,EAAQ,IAAM,IAAO3G,EAAa,KAAO4G,KAAKoG,IAAIF,GAC5F,OAAOlG,KAAKoC,IAAI,EAAGpC,KAAKqG,IAAI,IAAY,IAANF,EAAa,KACjD,CAEA,SAAS3G,EAAexH,EAA0B+K,GAChD/K,EAAIN,IAAIqL,GAAQ/K,EAAII,IAAI2K,IAAU,GAAK,EACzC,CAEA,SAASjC,EAAU7H,EAA8BqN,GAC/C,OAA4B,IAArBrN,EAAUH,OAAe,EAAIkH,KAAKoC,OAAOnJ,EAAUjB,IAAKuO,GAAOA,EAAGD,IAC3E,CAEA,SAAS9J,EAAYxE,GACnB,IAAIwO,EAAU,EACd,IAAK,MAAMzD,KAAS/K,EAAID,SACtByO,EAAUxG,KAAKoC,IAAIoE,EAASzD,GAE9B,OAAOyD,CACT,CAEA,SAASnK,EAAItE,GACX,IAAI+G,EAAQ,EACZ,IAAK,MAAMiE,KAAShL,EAClB+G,GAASiE,EAEX,OAAOjE,CACT"}
package/dist/types.d.ts CHANGED
@@ -39,6 +39,45 @@ export interface FunctionMetrics {
39
39
  endLine: number;
40
40
  cyclomaticComplexity: number;
41
41
  cognitiveComplexity: number;
42
+ callCount: number;
43
+ uniqueCalleeCount: number;
44
+ fanIn: number;
45
+ fanOut: number;
46
+ recursive: boolean;
47
+ }
48
+ export interface CallGraphMetrics {
49
+ callCount: number;
50
+ uniqueCalleeCount: number;
51
+ internalCallCount: number;
52
+ internalEdgeCount: number;
53
+ recursiveFunctionCount: number;
54
+ maxFanIn: number;
55
+ maxFanOut: number;
56
+ maxCallDepth: number;
57
+ }
58
+ export interface CouplingMetrics {
59
+ importCount: number;
60
+ importSourceCount: number;
61
+ relativeImportCount: number;
62
+ externalImportCount: number;
63
+ exportCount: number;
64
+ }
65
+ export interface CohesionMetrics {
66
+ averageFunctionIdentifierOverlap: number;
67
+ sharedIdentifierCount: number;
68
+ uniqueIdentifierCount: number;
69
+ }
70
+ export interface TypeComplexityMetrics {
71
+ typeAnnotationCount: number;
72
+ typeAliasCount: number;
73
+ interfaceCount: number;
74
+ genericParameterCount: number;
75
+ unionTypeCount: number;
76
+ intersectionTypeCount: number;
77
+ conditionalTypeCount: number;
78
+ typeAssertionCount: number;
79
+ nonNullAssertionCount: number;
80
+ satisfiesExpressionCount: number;
42
81
  }
43
82
  export interface CodeMetrics {
44
83
  language: LanguageName;
@@ -52,6 +91,10 @@ export interface CodeMetrics {
52
91
  cognitiveComplexity: number;
53
92
  maxCognitiveComplexity: number;
54
93
  nestingDepth: number;
94
+ callGraph: CallGraphMetrics;
95
+ coupling: CouplingMetrics;
96
+ cohesion: CohesionMetrics;
97
+ typeComplexity: TypeComplexityMetrics;
55
98
  halstead: HalsteadMetrics;
56
99
  maintainabilityIndex: number;
57
100
  syntaxTree?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "measure-code",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "tree-sitter",