diffjam
Version:
cli for diffjam.com
171 lines (149 loc) • 5.76 kB
text/typescript
import cluster from "cluster";
import { isBoolean, isNumber, isString, partition } from "lodash";
import mm from 'micromatch';
import { FileMatcher } from "./FileMatcher";
import { hasProp } from "./hasProp";
import { Match } from "./match";
const regexPrefix = "regex:";
const inversePrefix = "-:";
export type Needles = {
regex: RegExp;
otherRegexes: RegExp[];
positive: string[];
negative: string[];
}
export interface PolicyJson {
description: string;
filePattern: string | string[];
ignoreFilePatterns?: string[];
search: string | string[];
baseline: number;
hiddenFromOutput?: boolean;
}
const escapeStringRegexp = (str: string) =>
str
.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&")
.replace(/-/g, "\\x2d");
export class Policy {
public ran: boolean = false;
public needles: Needles
public ignoreFilePatterns: undefined | string[];
public filePattern: string[]
public search: string[]
constructor(
public name: string,
public description: string,
filePattern: string | string[],
search: string | string[],
public baseline: number,
ignoreFilePatterns?: string | string[],
public hiddenFromOutput: boolean = false,
) {
this.search = Array.isArray(search) ? search : [search];
this.filePattern = Array.isArray(filePattern) ? filePattern : [filePattern];
try {
this.needles = Policy.searchConfigToNeedles(this.search);
} catch (err) {
if (err instanceof Error) {
throw new Error(`Error in policy (${name}): ${err.message}`);
} else {
throw new Error(`Error in policy (${name})`);
}
}
if (ignoreFilePatterns && ignoreFilePatterns.length) {
if (!Array.isArray(ignoreFilePatterns)) {
this.ignoreFilePatterns = [ignoreFilePatterns];
} else {
this.ignoreFilePatterns = ignoreFilePatterns;
}
}
}
toJson(): PolicyJson {
const json: PolicyJson = {
description: this.description,
filePattern: this.filePattern.length === 1 ? this.filePattern[0] : this.filePattern,
search: this.search.length === 1 ? this.search[0] : this.search,
baseline: this.baseline,
};
if (this.hiddenFromOutput) json.hiddenFromOutput = this.hiddenFromOutput;
if (this.ignoreFilePatterns) json.ignoreFilePatterns = this.ignoreFilePatterns;
return json
}
isFileUnderPolicy(filePath: string): boolean {
return this.filePattern.some(filePattern =>
mm.any(filePath, filePattern) && (
!this.ignoreFilePatterns ||
!mm.any(filePath, this.ignoreFilePatterns)
));
}
processFile(file: FileMatcher, onMatch: (match: Match, policy: this) => void): void {
if (!this.isFileUnderPolicy(file.path)) return;
return file.findMatches(this.needles, match => onMatch(match, this));
}
isCountAcceptable(matches: Match[]): boolean {
return matches.length <= this.baseline;
}
isCountCinchable(matches: Match[]): boolean {
return matches.length < this.baseline;
}
// Converts array of `search` strings to a `Needles` object. This will include a regex to search for
// but also includes other search terms that may be used that will determine if we either include or exclude
// a match.
static searchConfigToNeedles(search: string[]): Needles {
const [inverseTerms, positiveTerms] = partition(search, term => term.startsWith(inversePrefix));
const [regexTerms, simplePositiveTerms] = partition(positiveTerms, term => term.startsWith(regexPrefix));
if (regexTerms.length) {
if (inverseTerms.length) {
throw new Error(`regex search terms (${regexTerms[0]}) cannot be combined with inverse search terms (${inverseTerms[0]})`);
}
} else if (!simplePositiveTerms.length) {
throw new Error(`no positive search terms found`);
}
const [firstRegexTerm, ...otherRegexTerms] = regexTerms.map(term => term.slice(regexPrefix.length));
const positive = simplePositiveTerms.map(escapeStringRegexp);
return {
regex: new RegExp(firstRegexTerm || positive.shift()!, "gm"),
negative: inverseTerms.map(term => term.slice(inversePrefix.length)),
positive,
otherRegexes: otherRegexTerms.map(term => new RegExp(term)),
};
}
static fromJson(name: string, obj: any): Policy {
try {
if (!obj) {
throw new Error("input was empty");
}
if (!hasProp(obj, "baseline")) {
throw new Error("baseline is required");
}
if (!isNumber(obj.baseline)) {
throw new Error("baseline must be a number");
}
if (!hasProp(obj, "search")) {
throw new Error("search is required");
}
if (!(isString(obj.search) || (Array.isArray(obj.search) && obj.search.every(isString)))) {
throw new Error("search must be a string or an array of strings");
}
if (!(isString(obj.filePattern) || (Array.isArray(obj.filePattern) && obj.filePattern.every(isString)))) {
throw new Error("filePattern must be a string or an array of strings");
}
if (!hasProp(obj, "description")) {
throw new Error("description is required");
}
if (!isString(obj.description)) {
throw new Error("description must be a string");
}
if (hasProp(obj, "hiddenFromOutput") && !isBoolean(obj.hiddenFromOutput)) {
throw new Error("hiddenFromOutput must be a boolean");
}
return new Policy(name, obj.description, obj.filePattern, obj.search, obj.baseline, obj.ignoreFilePatterns, Boolean(obj.hiddenFromOutput));
} catch (err) {
if (err instanceof Error) {
throw new Error(`Error in policy (${name}): ${err.message}`);
} else {
throw new Error(`Error in policy (${name})`);
}
}
}
}