Skip to content

Commit fc20b41

Browse files
author
Simon Renoult
committed
feat: statistics per directory
1 parent 35e99d7 commit fc20b41

File tree

7 files changed

+173
-48
lines changed

7 files changed

+173
-48
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ While imperfect, this measure gives a good enough idea of what's going on.
2121
## Usage
2222

2323
```sh
24-
$ npx code-complexity <path-to-git-directory or URL>
24+
$ npx code-complexity <path-to-git-directory or URL> [options]
2525
```
2626

2727
## Help
@@ -50,6 +50,7 @@ $ npx code-complexity <path-to-git-directory or URL>
5050
$ code-complexity ../foo --sort score
5151
$ code-complexity /foo/bar --filter 'src/**,!src/front/**'
5252
$ code-complexity . --limit 10 --sort score
53+
$ code-complexity . --limit 10 --modules
5354
$ code-complexity . --limit 10 --sort score -cs halstead
5455
$ code-complexity . --since=2021-06-01 --limit 100
5556
$ code-complexity . --since=2021-04-01 --until=2021-07-01

src/io/cli.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ function getRawCli(
7474
"sort results (allowed valued: score, churn, complexity or file)",
7575
/^(score|churn|complexity|file)$/i
7676
)
77+
.option(
78+
"-d, --directories",
79+
"Display values for directories instead of files"
80+
)
7781
.on("--help", () => {
7882
console.log();
7983
console.log("Examples:");
@@ -83,9 +87,12 @@ function getRawCli(
8387
"$ code-complexity https://github.com/simonrenoult/code-complexity",
8488
"$ code-complexity foo --limit 3",
8589
"$ code-complexity ../foo --sort score",
86-
"$ code-complexity . -cs halstead",
8790
"$ code-complexity /foo/bar --filter 'src/**,!src/front/**'",
8891
"$ code-complexity . --limit 10 --sort score",
92+
"$ code-complexity . --limit 10 --modules",
93+
"$ code-complexity . --limit 10 --sort score -cs halstead",
94+
"$ code-complexity . --since=2021-06-01 --limit 100",
95+
"$ code-complexity . --since=2021-04-01 --until=2021-07-01",
8996
].forEach((example) => console.log(example.padStart(2)));
9097
});
9198
}
@@ -95,6 +102,7 @@ function buildOptions(args: string[], options: any): Options {
95102
return {
96103
target,
97104
directory: parseDirectory(target),
105+
directories: options.directories,
98106
format: options.format ? (String(options.format) as Format) : "table",
99107
filter: options.filter || [],
100108
limit: options.limit ? Number(options.limit) : undefined,

src/io/output.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function render(
3535
}
3636

3737
function toJson(statistics: Statistics[]): string {
38-
return JSON.stringify(statistics);
38+
return JSON.stringify(statistics.map((s) => s.toState()));
3939
}
4040

4141
function toTable(statistics: Statistics[]): string {

src/lib/statistics.ts

+92-28
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@ import Churn from "./churn/churn";
22
import Complexity from "./complexity";
33
import { Options, Path, Sort } from "./types";
44
import { buildDebugger } from "../utils";
5+
import * as NodePath from "path";
56

67
const DEFAULT_CHURN = 1;
78
const DEFAULT_COMPLEXITY = 1;
89

910
const internal = { debug: buildDebugger("statistics") };
1011

12+
export interface IStatistics {
13+
path: Path;
14+
churn: number;
15+
complexity: number;
16+
score: number;
17+
}
18+
1119
export default class Statistics {
1220
public readonly path: Path;
1321
public readonly churn: number;
1422
public readonly complexity: number;
1523
public readonly score: number;
1624

17-
constructor(path: Path, churn: number, complexity: number) {
18-
this.path = path;
19-
this.churn = churn;
20-
this.complexity = complexity;
21-
this.score = this.churn * this.complexity;
22-
}
25+
private readonly directories: string[];
2326

2427
public static async compute(
2528
options: Options
@@ -31,24 +34,92 @@ export default class Statistics {
3134
const paths = Array.from(churns.keys());
3235
const complexities = await Complexity.compute(paths, options);
3336

34-
const statistics = paths
35-
.map(toStatistics(churns, complexities))
36-
.sort(sort(options.sort))
37-
.filter(limit(options.limit));
37+
const statistics = paths.map(Statistics.toStatistics(churns, complexities));
3838

39-
return toMap(statistics);
39+
const mapOfStatistics = options.directories
40+
? Statistics.toDirectoryMap(statistics)
41+
: Statistics.toFileMap(statistics);
42+
43+
return new Map(
44+
[...mapOfStatistics.entries()]
45+
.sort(([, v1], [, v2]) => sort(options.sort)(v1, v2))
46+
.filter(([, v], index) => limit(options.limit)(v, index))
47+
);
4048
}
41-
}
4249

43-
function toStatistics(
44-
churns: Map<Path, number>,
45-
complexities: Map<Path, number>
46-
): (path: Path) => Statistics {
47-
return (path): Statistics => {
48-
const churn = churns.get(path) || DEFAULT_CHURN;
49-
const complexity = complexities.get(path) || DEFAULT_COMPLEXITY;
50-
return new Statistics(path, churn, complexity);
51-
};
50+
public static toStatistics(
51+
churns: Map<Path, number>,
52+
complexities: Map<Path, number>
53+
): (path: Path) => Statistics {
54+
return (path): Statistics => {
55+
const churn = churns.get(path) || DEFAULT_CHURN;
56+
const complexity = complexities.get(path) || DEFAULT_COMPLEXITY;
57+
return new Statistics(path, churn, complexity);
58+
};
59+
}
60+
61+
private constructor(path: Path, churn: number, complexity: number) {
62+
this.path = path;
63+
this.churn = churn;
64+
this.complexity = complexity;
65+
this.directories = this.findDirectoriesForFile(path);
66+
this.score = this.churn * this.complexity;
67+
}
68+
69+
private findDirectoriesForFile(path: string): string[] {
70+
const directories: string[] = [];
71+
const pathChunks = NodePath.parse(path).dir.split(NodePath.sep);
72+
pathChunks.forEach((chunk) => {
73+
const parentDir = directories.slice(-1);
74+
const directory = parentDir.length
75+
? parentDir + NodePath.sep + chunk
76+
: chunk;
77+
directories.push(directory);
78+
});
79+
return directories.filter((d) => d.length > 0);
80+
}
81+
82+
private static toFileMap(statistics: Statistics[]): Map<Path, Statistics> {
83+
return statistics.reduce((map: Map<Path, Statistics>, statistics) => {
84+
map.set(statistics.path, statistics);
85+
return map;
86+
}, new Map());
87+
}
88+
89+
private static toDirectoryMap(
90+
allStatistics: Statistics[]
91+
): Map<string, Statistics> {
92+
return allStatistics.reduce((map, statisticsForFile) => {
93+
statisticsForFile.directories.forEach((directoryForFile) => {
94+
computeStatisticsForDirectory(map, directoryForFile, statisticsForFile);
95+
});
96+
return map;
97+
}, new Map<string, Statistics>());
98+
99+
function computeStatisticsForDirectory(
100+
map: Map<string, Statistics>,
101+
dir: string,
102+
statisticsForFile: Statistics
103+
) {
104+
const statisticsForDir = map.get(dir);
105+
const churn =
106+
statisticsForFile.churn +
107+
(statisticsForDir ? statisticsForDir.churn : 0);
108+
const complexity =
109+
statisticsForFile.complexity +
110+
(statisticsForDir ? statisticsForDir.complexity : 0);
111+
map.set(dir, new Statistics(dir, churn, complexity));
112+
}
113+
}
114+
115+
public toState(): IStatistics {
116+
return {
117+
path: this.path,
118+
churn: this.churn,
119+
complexity: this.complexity,
120+
score: this.score,
121+
};
122+
}
52123
}
53124

54125
function limit(
@@ -80,10 +151,3 @@ function sort(sort: Sort | undefined) {
80151
return 0;
81152
};
82153
}
83-
84-
function toMap(statistics: Statistics[]): Map<Path, Statistics> {
85-
return statistics.reduce((map: Map<Path, Statistics>, statistics) => {
86-
map.set(statistics.path, statistics);
87-
return map;
88-
}, new Map());
89-
}

src/lib/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type ComplexityStrategy = "sloc" | "halstead" | "cyclomatic";
77
export type Options = {
88
target: string | URL;
99
directory: string;
10+
directories?: boolean;
1011
limit?: number;
1112
since?: string;
1213
until?: string;

test/fixtures/versioned-file.fixture.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { execSync } from "child_process";
2-
import { appendFileSync, writeFileSync } from "fs";
3-
import { sep } from "path";
2+
import { appendFileSync, mkdirSync, writeFileSync } from "fs";
3+
import * as NodePath from "path";
44

55
export default class VersionedFileFixture {
66
constructor(private readonly repositoryLocation: string) {}
@@ -81,7 +81,8 @@ export default class VersionedFileFixture {
8181
.map((value, index) => `console.log(${index});`)
8282
.join("\n");
8383

84-
writeFileSync(`${this.getFileLocation()}`, fileContent);
84+
mkdirSync(NodePath.parse(this.getFileLocation()).dir, { recursive: true });
85+
writeFileSync(this.getFileLocation(), fileContent);
8586
}
8687

8788
private addFileToRepository(): void {
@@ -103,6 +104,6 @@ export default class VersionedFileFixture {
103104
}
104105

105106
private getFileLocation(): string {
106-
return `${this.repositoryLocation}${sep}${this.name}`;
107+
return `${this.repositoryLocation}${NodePath.sep}${this.name}`;
107108
}
108109
}

0 commit comments

Comments
 (0)