code-complexity
Version:
Measure the churn/complexity score. Higher values mean hotspots where refactorings should happen.
158 lines (141 loc) • 4.71 kB
text/typescript
import { execSync } from "node:child_process";
import { lstatSync } from "node:fs";
import { sep } from "node:path";
import { tmpdir } from "node:os";
import { URL } from "node:url";
import { Command, program } from "commander";
import { buildDebugger, getPackageJson } from "../utils";
import { ComplexityStrategy, Format, Options, Sort } from "../lib/types";
const internal = { debug: buildDebugger("cli") };
export default { parse, cleanup };
async function parse(): Promise<Options> {
const { description, version } = await getPackageJson();
const cli = getRawCli(description, version).parse();
assertArgsAreProvided(cli);
const options = buildOptions(cli.args, cli.opts());
internal.debug(`applying options: ${JSON.stringify(options)}`);
return options;
}
function cleanup(options: Options): void {
if (options.target instanceof URL) {
execSync(`rm -rf ${options.directory}`, { stdio: "ignore" });
}
}
function getRawCli(
description: string | undefined,
version: string | undefined
): Command {
return program
.name("code-complexity")
.usage("<target> [options]")
.argument("<target>")
.version(version || "")
.description(description || "")
.option(
"--filter <strings>",
"list of globs (comma separated) to filter",
commaSeparatedList
)
.option(
"-cs, --complexity-strategy [sloc|cyclomatic|halstead]",
"choose the complexity strategy to analyze your codebase with",
/^(sloc|cyclomatic|halstead)$/i
)
.option(
"-f, --format [format]",
"format results using table or json",
/^(table|json|csv)$/i
)
.option(
"-l, --limit [limit]",
"limit the number of files to output",
parseInt
)
.option(
"-i, --since [since]",
"limit analysis to commits more recent in age than date"
)
.option(
"-u, --until [until]",
"limit analysis to commits older in age than date"
)
.option(
"-s, --sort [sort]",
"sort results (allowed valued: score, churn, complexity or file)",
/^(score|churn|complexity|file)$/i
)
.option(
"-d, --directories",
"display values for directories instead of files"
)
.on("--help", () => {
console.log();
console.log("Examples:");
console.log();
[
"$ code-complexity .",
"$ code-complexity https://github.com/simonrenoult/code-complexity",
"$ code-complexity foo --limit 3",
"$ code-complexity ../foo --sort score",
"$ code-complexity /foo/bar --filter 'src/**,!src/front/**'",
"$ code-complexity . --limit 10 --sort score",
"$ code-complexity . --limit 10 --modules",
"$ code-complexity . --limit 10 --sort score -cs halstead",
"$ code-complexity . --since=2021-06-01 --limit 100",
"$ code-complexity . --since=2021-04-01 --until=2021-07-01",
].forEach((example) => console.log(example.padStart(2)));
});
}
function buildOptions(args: string[], options: any): Options {
const target = parseTarget(args[0]);
return {
target,
directory: parseDirectory(target),
directories: options.directories,
format: options.format ? (String(options.format) as Format) : "table",
filter: options.filter || [],
limit: options.limit ? Number(options.limit) : undefined,
since: options.since ? String(options.since) : undefined,
until: options.until ? String(options.until) : undefined,
sort: options.sort ? (String(options.sort) as Sort) : undefined,
complexityStrategy: options.complexityStrategy
? (String(options.complexityStrategy) as ComplexityStrategy)
: "sloc",
};
// FIXME: I'm not a fan of pulling the code here but it's good enough.
function parseDirectory(target: string | URL): string {
if (target instanceof URL) {
const temporaryDirLocation =
tmpdir() + sep + `code-complexity-${new Date().getTime()}`;
execSync(`git clone ${target} ${temporaryDirLocation}`, {
stdio: "ignore",
});
return temporaryDirLocation;
} else {
return target;
}
}
function parseTarget(target: string): string | URL {
try {
return new URL(target);
} catch (e) {
try {
lstatSync(target);
return target;
} catch (e) {
throw new Error(
"Argument 'target' is neither a directory nor a valid URL."
);
}
}
}
}
function commaSeparatedList(value: string): string[] {
return value.split(",");
}
function assertArgsAreProvided(internalCli: Command): void {
if (!internalCli.args || !internalCli.args.length) {
internalCli.help();
process.exit(1);
}
}