Skip to content

Commit 46bc6a2

Browse files
author
Simon Renoult
committed
refactor: make internal structure clearer
1 parent 15bcf94 commit 46bc6a2

File tree

18 files changed

+440
-350
lines changed

18 files changed

+440
-350
lines changed

.eslintrc.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
"prettier/prettier": "error",
1313
"no-console": "off",
1414
"@typescript-eslint/no-use-before-define": "off",
15-
"@typescript-eslint/no-explicit-any": "off",
16-
"sort-imports": [
17-
"error",
18-
{ "ignoreCase": true, "allowSeparatedGroups": true }
19-
]
15+
"@typescript-eslint/no-explicit-any": "off"
2016
}
2117
}

src/io/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default async function main(): Promise<void> {
1515

1616
const statistics = await Statistics.compute(options);
1717
Cli.cleanup(options);
18-
Output.render(statistics, options);
18+
Output.render(statistics.list(), options);
1919
}
2020

2121
function warnIfUsingComplexityWithIncompatibleFileTypes(options: Options) {

src/io/output.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as Table from "cli-table3";
2+
import { IStatistic } from "../../dist/src/lib/statistics";
3+
import { Options } from "../lib/types";
24

35
import { buildDebugger, withDuration } from "../utils";
4-
import { Options } from "../lib/types";
5-
import Statistics from "../lib/statistics";
66

77
const internal = { debug: buildDebugger("output") };
88

99
export default {
1010
render: (...args: any[]): void => withDuration(render, args, internal.debug),
1111
};
1212

13-
function render(statistics: Statistics[], options: Options): void {
13+
function render(statistics: IStatistic[], options: Options): void {
1414
let stdout;
1515
switch (options.format) {
1616
case "table":
@@ -29,11 +29,11 @@ function render(statistics: Statistics[], options: Options): void {
2929
console.log(stdout);
3030
}
3131

32-
function toJson(statistics: Statistics[]): string {
33-
return JSON.stringify(statistics.map((s) => s.toState()));
32+
function toJson(statistics: IStatistic[]): string {
33+
return JSON.stringify(statistics);
3434
}
3535

36-
function toTable(statistics: Statistics[]): string {
36+
function toTable(statistics: IStatistic[]): string {
3737
const table = new Table({
3838
head: ["file", "complexity", "churn", "score"],
3939
});
@@ -49,7 +49,7 @@ function toTable(statistics: Statistics[]): string {
4949
return table.toString();
5050
}
5151

52-
function toCSV(statistics: Statistics[]): string {
52+
function toCSV(statistics: IStatistic[]): string {
5353
let csv = "file,complexity,churn,score\n";
5454
statistics.forEach((stat) => {
5555
csv +=

src/lib/churn/churn.ts

Lines changed: 12 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,20 @@
1-
import { execSync } from "node:child_process";
2-
import { existsSync } from "node:fs";
3-
import { resolve } from "node:path";
1+
import { Path } from "../types";
42

5-
import * as micromatch from "micromatch";
3+
export default class Churn {
4+
private path: Path;
5+
private changes: number;
66

7-
import { buildDebugger, withDuration } from "../../utils";
8-
import { Options, Path } from "../types";
9-
10-
const internal = { debug: buildDebugger("churn") };
11-
const PER_LINE = "\n";
12-
13-
export default {
14-
compute: (...args: any[]): Promise<Map<Path, number>> =>
15-
withDuration(compute, args, internal.debug),
16-
};
17-
18-
async function compute(options: Options): Promise<Map<Path, number>> {
19-
const gitLogCommand = buildGitLogCommand(options);
20-
const singleStringWithAllChurns = executeGitLogCommand(gitLogCommand);
21-
return computeChurnsPerFiles(
22-
singleStringWithAllChurns,
23-
options.directory,
24-
options.filter
25-
);
26-
}
27-
28-
function executeGitLogCommand(gitLogCommand: string): string {
29-
return execSync(gitLogCommand, { encoding: "utf8", maxBuffer: 32_000_000 });
30-
}
31-
32-
function buildGitLogCommand(options: Options): string {
33-
const isWindows = process.platform === "win32";
34-
35-
return [
36-
"git",
37-
`-C ${options.directory}`,
38-
`log`,
39-
`--follow`,
40-
41-
// Windows CMD handle quotes differently than linux, this is why we should put empty string as said in:
42-
// https://github.com/git-for-windows/git/issues/3131
43-
`--format=${isWindows ? "" : "''"}`,
44-
`--name-only`,
45-
options.since ? `--since="${options.since}"` : "",
46-
options.until ? `--until="${options.until}"` : "",
47-
48-
// Windows CMD handle quotes differently
49-
isWindows ? "*" : "'*'",
50-
]
51-
.filter((s) => s.length > 0)
52-
.join(" ");
53-
}
54-
55-
function computeChurnsPerFiles(
56-
gitLogOutput: string,
57-
directory: string,
58-
filters: string[] | undefined
59-
): Map<Path, number> {
60-
const changedFiles = gitLogOutput
61-
.split(PER_LINE)
62-
.filter((line) => line !== "")
63-
.sort();
64-
65-
return changedFiles.reduce((map: Map<Path, number>, path) => {
66-
applyFiltersAndExcludeObsoletePath(path, map);
67-
return map;
68-
}, new Map());
69-
70-
function applyFiltersAndExcludeObsoletePath(
71-
path: string,
72-
map: Map<Path, number>
73-
) {
74-
if (!filters || !filters.length) {
75-
if (pathStillExists(path)) {
76-
addOrIncrement(map, path);
77-
}
78-
} else {
79-
const pathHasAMatch = filters.every((f) => micromatch.isMatch(path, f));
80-
if (pathHasAMatch) {
81-
if (pathStillExists(path)) {
82-
addOrIncrement(map, path);
83-
}
84-
}
85-
}
7+
constructor(path: Path) {
8+
this.path = path;
9+
this.changes = 0;
8610
}
8711

88-
function addOrIncrement(map: Map<Path, number>, path: string) {
89-
map.set(path, (map.get(path) ?? 0) + 1);
12+
public increment(): this {
13+
this.changes += 1;
14+
return this;
9015
}
9116

92-
function pathStillExists(fileName: string) {
93-
return existsSync(resolve(directory, fileName));
17+
public getValue(): number {
18+
return this.changes;
9419
}
9520
}

src/lib/churn/churns.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { History } from "../githistory/githistory";
2+
3+
import { Options, Path } from "../types";
4+
import Churn from "./churn";
5+
6+
// FIXME: add commits
7+
8+
export default class Churns {
9+
private readonly churnByPath: Map<Path, Churn>;
10+
private readonly options: Options;
11+
12+
public static from(history: History, options: Options) {
13+
return new Churns(history, options);
14+
}
15+
16+
private constructor(history: History, options: Options) {
17+
this.options = options;
18+
this.churnByPath = this.computeChurnsPerFiles(history);
19+
}
20+
21+
public get files(): Path[] {
22+
return [...this.churnByPath.keys()];
23+
}
24+
25+
public getByPath(path: Path): Churn {
26+
const churn = this.churnByPath.get(path);
27+
if (!churn) {
28+
throw new Error("churn not found for path: " + path);
29+
}
30+
return churn;
31+
}
32+
33+
private computeChurnsPerFiles(history: History): Map<Path, Churn> {
34+
return history.reduce((map: Map<Path, Churn>, path) => {
35+
if (map.has(path)) {
36+
const actualChurn = map.get(path);
37+
if (actualChurn) {
38+
actualChurn.increment();
39+
} else {
40+
throw new Error("A churn should have existed for path: " + path);
41+
}
42+
} else {
43+
const churn = new Churn(path).increment();
44+
map.set(path, churn);
45+
}
46+
return map;
47+
}, new Map());
48+
}
49+
}

src/lib/complexity/complexities.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { buildDebugger } from "../../utils";
2+
import { Options, Path } from "../types";
3+
import Complexity from "./complexity";
4+
5+
const internal = { debug: buildDebugger("complexity") };
6+
7+
export default class Complexities {
8+
private readonly complexityByPath: Map<Path, Complexity>;
9+
10+
public static async computeFor(
11+
paths: Path[],
12+
options: Options
13+
): Promise<Complexities> {
14+
internal.debug(`${paths.length} files to compute complexity on`);
15+
const complexities = await Promise.all(
16+
paths.map(async (p) => await Complexity.compute(p, options))
17+
);
18+
19+
return new Complexities(complexities);
20+
}
21+
22+
public getByPath(path: Path): Complexity {
23+
const complexity = this.complexityByPath.get(path);
24+
if (!complexity) throw new Error("Complexity not found for path: " + path);
25+
return complexity;
26+
}
27+
28+
private constructor(complexities: Complexity[]) {
29+
this.complexityByPath = this.computeComplexitiesPerPath(complexities);
30+
}
31+
32+
private computeComplexitiesPerPath(complexities: Complexity[]) {
33+
return complexities.reduce((map, complexity) => {
34+
map.set(complexity.path, complexity);
35+
return map;
36+
}, new Map());
37+
}
38+
}

src/lib/complexity/complexity.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Options, Path } from "../types";
2+
import computeSloc from "./strategies/sloc";
3+
import { calculate as calculateCyclomatic } from "./strategies/cyclomatic";
4+
import { calculate as calculateHalstead } from "./strategies/halstead";
5+
import { resolve } from "node:path";
6+
import { UnsupportedExtension } from "../../utils";
7+
8+
// FIXME: add modules
9+
10+
export default class Complexity {
11+
public static async compute(path: Path, options: Options) {
12+
const complexity = await Complexity.computeComplexity(path, options);
13+
return new Complexity(path, complexity);
14+
}
15+
16+
private constructor(
17+
public readonly path: Path,
18+
public readonly complexity: number
19+
) {}
20+
21+
private static async computeComplexity(
22+
path: Path,
23+
options: Options
24+
): Promise<number> {
25+
const absolutePath = resolve(options.directory, path);
26+
27+
let result: number | UnsupportedExtension;
28+
switch (options.complexityStrategy) {
29+
case "sloc":
30+
result = await computeSloc(absolutePath);
31+
break;
32+
case "cyclomatic":
33+
result = await calculateCyclomatic(absolutePath);
34+
break;
35+
case "halstead":
36+
result = await calculateHalstead(absolutePath);
37+
break;
38+
default:
39+
result = await computeSloc(absolutePath);
40+
}
41+
42+
if (result instanceof UnsupportedExtension) {
43+
result = await computeSloc(absolutePath);
44+
}
45+
46+
return result as number;
47+
}
48+
49+
getValue(): number {
50+
return this.complexity;
51+
}
52+
}

src/lib/complexity/index.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/lib/complexity/cyclomatic.ts renamed to src/lib/complexity/strategies/cyclomatic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { extname } from "node:path";
22
import { readFileSync } from "node:fs";
33

4-
import { buildDebugger, UnsupportedExtension } from "../../utils";
4+
import { buildDebugger, UnsupportedExtension } from "../../../utils";
55
import { transformSync } from "@babel/core";
66

77
// eslint-disable-next-line @typescript-eslint/no-var-requires

src/lib/complexity/halstead.ts renamed to src/lib/complexity/strategies/halstead.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildDebugger, UnsupportedExtension } from "../../utils";
1+
import { buildDebugger, UnsupportedExtension } from "../../../utils";
22
import { extname } from "node:path";
33
import { readFileSync } from "node:fs";
44
import { transformSync } from "@babel/core";
File renamed without changes.

0 commit comments

Comments
 (0)