measure-code 1.0.0 → 1.1.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
@@ -1,6 +1,6 @@
1
- # tree-measurer
1
+ # measure-code
2
2
 
3
- [![Test](https://github.com/WillBooster/tree-measurer/actions/workflows/test.yml/badge.svg)](https://github.com/WillBooster/tree-measurer/actions/workflows/test.yml)
3
+ [![Test](https://github.com/WillBooster/measure-code/actions/workflows/test.yml/badge.svg)](https://github.com/WillBooster/measure-code/actions/workflows/test.yml)
4
4
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
5
5
 
6
6
  A library for measuring code metrics with tree-sitter.
@@ -39,3 +39,11 @@ function score(value) {
39
39
 
40
40
  console.log(metrics.cyclomaticComplexity);
41
41
  ```
42
+
43
+ ## CLI
44
+
45
+ ```sh
46
+ measure-code ~/ghq/github.com/WillBoosterLab/exercode
47
+ ```
48
+
49
+ The CLI scans JavaScript, JSX, TypeScript, TSX, Python, and Go files, skips generated/vendor/test directories by default, and reports functions whose complexity is above the risk thresholds. Use `--include-tests` to include test files, `--json` for machine-readable output, `--max-findings` to control report length, `--fail-on-risk` or `--fail-on-error` for CI, or tune the defaults with `--cyclomatic-threshold` and `--cognitive-threshold`.
package/dist/cli.cjs ADDED
@@ -0,0 +1,3 @@
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});
3
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +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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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
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});
3
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +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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"}
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).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 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)};
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);\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","rootNode","functionMetrics","collectNodes","functionNodeTypes","node","complexity","measureComplexity","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","measureFunction","globalComplexity","lines","sourceLines","length","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","undefined","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 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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"}
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).rootNode,d=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=y(n.values()),u=y(r.values()),m=a+c,f=l+u,g=0===m?0:f*Math.log2(m),d=0===c?0:a/2*(u/c),h=d*g;return{distinctOperators:a,distinctOperands:c,totalOperators:l,totalOperands:u,vocabulary:m,length:f,volume:g,difficulty:d,effort:h,time:h/18,bugs:g/3e3}}(a,e);return{language:r.name,bytes:Buffer.byteLength(e),lines:x,functions:d,classCount:l(a,new Set(r.classNodeTypes)).length,functionCount:d.length,cyclomaticComplexity:h.cyclomaticComplexity,maxCyclomaticComplexity:g(d,"cyclomaticComplexity"),cognitiveComplexity:h.cognitiveComplexity,maxCognitiveComplexity:g(d,"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 y(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 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};
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);\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","rootNode","functionMetrics","collectNodes","functionNodeTypes","node","complexity","measureComplexity","findFunctionName","startLine","startPosition","row","endLine","endPosition","cyclomaticComplexity","cognitiveComplexity","measureFunction","globalComplexity","lines","sourceLines","length","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","undefined","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 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "measure-code",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "tree-sitter",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/WillBooster/tree-measurer.git"
14
+ "url": "git+https://github.com/WillBooster/measure-code.git"
15
15
  },
16
16
  "license": "Apache-2.0",
17
17
  "author": "WillBooster Inc.",
@@ -26,11 +26,12 @@
26
26
  "main": "dist/index.cjs",
27
27
  "module": "dist/index.js",
28
28
  "types": "dist/index.d.ts",
29
+ "bin": "./dist/cli.js",
29
30
  "files": [
30
31
  "dist/"
31
32
  ],
32
33
  "scripts": {
33
- "build": "build-ts lib",
34
+ "build": "build-ts lib -i src/index.ts -i src/cli.ts",
34
35
  "cleanup": "yarn format && yarn lint-fix",
35
36
  "format": "sort-package-json && yarn format-code",
36
37
  "format-code": "oxfmt --write --no-error-on-unmatched-pattern . '!**/package.json'",
@@ -43,6 +44,7 @@
43
44
  "verify-full": "wb verify --full"
44
45
  },
45
46
  "dependencies": {
47
+ "commander": "15.0.0",
46
48
  "tree-sitter": "^0.21.1",
47
49
  "tree-sitter-go": "^0.21.2",
48
50
  "tree-sitter-javascript": "^0.21.4",