UNPKG

diffjam

Version:
171 lines (149 loc) 5.76 kB
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})`); } } } }