clean-code-metrics
Version:
metrics for clean code
169 lines (149 loc) • 4.96 kB
text/typescript
import fs from "fs";
import gitignore_parser from "gitignore-parser";
import node_dir from "node-dir";
// @ts-ignore no types available TODO add them
import extract from "esprima-extract-comments";
import { SummaryTask } from "./types/SummaryTask";
import { TaskOccurrence, TaskFile, Config } from "./types/TaskMetrics";
import { load } from "./CCMConfig";
/**
* Removes leading "*" from content, then removes leading task from content
* @param content
* @param task
*/
function asTaskComment(content: string, task: string) {
content = content.trim();
if (content.startsWith("*")) {
content = content.slice(1).trim();
}
if (content.startsWith(task)) {
return content.slice(task.length).trim();
} else {
return content;
}
}
export default class TaskMetrics {
public static async createMetrics(
configOverridePath?: fs.PathLike,
packageJSONPath?: fs.PathLike,
configPath?: fs.PathLike,
): Promise<TaskMetrics> {
const config = load(configOverridePath, packageJSONPath, configPath);
const metrics = new TaskMetrics(config);
await metrics.update();
return metrics;
}
private readonly searchConfig: Config;
private readonly matchPattern;
private taskData: TaskData;
private static matchIgnoreCase(value: string, pattern: string): boolean {
return value.toUpperCase().includes(pattern.toUpperCase());
}
private static matchUpperCase(value: string, pattern: string): boolean {
return value.includes(pattern.toUpperCase());
}
private constructor(config: Config) {
this.searchConfig = config;
this.matchPattern = this.searchConfig.ignoreCase
? TaskMetrics.matchIgnoreCase
: TaskMetrics.matchUpperCase;
this.taskData = new TaskData([], this.searchConfig);
}
public async allFiles(): Promise<string[]> {
const winRegex = new RegExp(/\\/, "g");
const ignore = this.searchConfig.ignorePath
.map((path) => fs.readFileSync(path, "utf-8")) //load ignore files
.map(gitignore_parser.compile); //parse ignore files
const allFiles = await node_dir.promiseFiles(this.searchConfig.basePath); //load all files
const ntfsCorrected = allFiles.map((path) => path.replace(winRegex, "/")); //Corrects the path to a unix path if it is a windows path (replaces \\ with /)
return ntfsCorrected.filter((file) => ignore.every((i) => i.accepts(file))); //remove ignored files
}
public searchTasks(file: string): TaskOccurrence[] {
const data = fs.readFileSync(file).toString("utf-8");
const matches: TaskOccurrence[] = [];
for (const comment of extract(data)) {
const lines: string[] = comment.value.split(/\r?\n/);
lines.forEach((value, line) => {
for (const pattern of this.searchConfig.searchPattern) {
if (this.matchPattern(value, pattern)) {
const task = {
file: file,
lineNumber: comment.loc.start.line + line,
content: asTaskComment(value.trim(), pattern.toUpperCase()),
comment: value.trim(),
task: pattern.toUpperCase(),
};
matches.push(task);
}
}
});
}
return matches;
}
public get(): TaskData {
return this.taskData;
}
public async update(): Promise<void> {
this.taskData = await this.taskList();
}
private async taskList(): Promise<TaskData> {
const all_Files = await this.allFiles();
let allTasks: TaskOccurrence[] = [];
for (const file of all_Files) {
allTasks = allTasks.concat(this.searchTasks(file));
}
return new TaskData(allTasks, this.searchConfig);
}
}
class TaskData {
private readonly list: TaskOccurrence[];
private readonly searchConfig: Config;
constructor(list: TaskOccurrence[], searchConfig: Config) {
this.list = list;
this.searchConfig = { ...searchConfig };
}
public getConfig(): Config {
return { ...this.searchConfig } as Config;
}
public getRawData() {
return this.list;
}
public taskSummary(
includeEmpty = false,
filter: string[] = this.searchConfig.searchPattern,
): SummaryTask[] {
return this.searchConfig.searchPattern
.filter((task) => filter.includes(task))
.map((task) => {
return {
task: task,
amount: this.list.filter((occ) => occ.task === task).length,
};
})
.filter((task) => task.amount || includeEmpty);
}
public getList(fileFilter?: string[]): TaskFile[] {
const result: TaskFile[] = [];
const filtered = this.list.filter(
(o) => !fileFilter || fileFilter.includes(o.file),
);
for (const occurrence of filtered) {
let file = result.find((f) => f.file === occurrence.file);
if (!file) {
file = { file: occurrence.file, tasks: [] };
result.push(file);
}
file.tasks.push({
task: occurrence.task,
lineNumber: occurrence.lineNumber,
comment: occurrence.content,
content: asTaskComment(occurrence.content, occurrence.task),
});
}
return result;
}
public getTasksOfType(task: string | string[]): TaskOccurrence[] {
if (typeof task == "string") task = [task];
return this.list.filter((occ) => task.includes(occ.task));
}
}