diff --git a/keeli.config.json b/keeli.config.json index ba4ddf8..4407bcb 100644 --- a/keeli.config.json +++ b/keeli.config.json @@ -9,10 +9,13 @@ "rules": { "no-untranslated-messages": "error", "no-empty-messages": "error", - "no-extra-whitespace": "error", - "no-html-messages": "error", + "no-extra-whitespace": { + "severity": "error", + "ignoreKeys": [] + }, + "no-html-messages": "kjh", "no-missing-keys": "error", - "no-invalid-variables": "error", + "no-invalid-variables": "warn", "no-malformed-keys": { "severity": "error", "namingConvention": "kebab-case" diff --git a/package.json b/package.json index 0500295..0cbaab3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "chalk": "^4.0.0", + "cli-table3": "^0.6.5", "flattie": "^1.1.1", "lodash": "^4.17.21" }, diff --git a/src/classes/logger.class.ts b/src/classes/logger.class.ts index a75b639..b8ee83d 100644 --- a/src/classes/logger.class.ts +++ b/src/classes/logger.class.ts @@ -1,67 +1,175 @@ import { ProblemStore } from "./problem-store.class.ts"; import { Problem } from "./problem.class.ts"; +import Table, { + HorizontalTableRow, + VerticalTableRow, + CrossTableRow, + Cell, +} from "cli-table3"; +import chalk from "chalk"; +import { SEVERITY_LEVEL } from "../constants.ts"; class Logger { private problemStore: ProblemStore; - private validationProblems: Problem[]; + private validationProblems: { [locale: string]: Problem[] }; private configurationProblems: Problem[]; - private ignoredConfigurationProblems: Problem[]; - private ignoredValidationProblems: Problem[]; + private isDryRun: boolean; - public constructor(problemStore: ProblemStore) { + public constructor(problemStore: ProblemStore, isDryRun: boolean) { this.problemStore = problemStore; this.validationProblems = problemStore.getValidationProblems(); this.configurationProblems = problemStore.getConfigurationProblems(); - this.ignoredConfigurationProblems = - problemStore.getIgnoredConfigurationProblems(); - this.ignoredValidationProblems = - problemStore.getIgnoredValidationProblems(); + this.isDryRun = isDryRun; } - // TODO: Clean up this summary when the pretty reporter is done https://github.com/radiovisual/keeli/issues/3 public logErrors() { - this.problemStore.getAllProblems().forEach((problem) => { - console.log( - `${problem.severity} | ${problem?.locale ?? ""} ${problem.message}` - ); + Object.entries(this.validationProblems).forEach( + ([locale, problemArray]) => { + const problemTable = this.getNewTable(); + console.log(`\n\nProblems found in locale: ${chalk.cyan(locale)}`); + problemArray.forEach((problem) => { + problemTable.push(this.getTableRow(problem)); + }); + console.log(problemTable.toString()); + } + ); + + const configTable = this.getNewTable(); + this.configurationProblems.forEach((problem) => { + configTable.push(this.getTableRow(problem)); }); - console.log("---"); + + if (this.configurationProblems.length > 0) { + console.log(`\n\nProblems found in ${chalk.cyan("configuration")}`); + console.log(configTable.toString()); + } + console.log(this.getPrintSummary()); + + if (this.isDryRun) { + console.log( + chalk.cyan(`\n\ndryRun is enabled for keeli. Errors will not throw.`) + ); + } } - // TODO: Clean up this summary when the pretty reporter is done https://github.com/radiovisual/keeli/issues/3 - public getPrintSummary() { - let summary = ["\n"]; + public getTableRow(problem: Problem): Array { + const { + severity, + isIgnored, + ruleMeta, + locale, + message, + expected, + received, + } = problem; - summary.push(`Error(s) found: ${this.problemStore.getErrorCount()}`); + let items: Array< + string | HorizontalTableRow | VerticalTableRow | CrossTableRow | Cell + > = []; + let colSpan = 0; - if (this.problemStore.getIgnoredConfigurationProblemsCount() > 0) { - summary.push( - `Ignored Configuration Errors(s): ${this.problemStore.getIgnoredConfigurationProblemsCount()}` - ); + if (severity === SEVERITY_LEVEL.error) { + if (isIgnored) { + items.push(chalk.red.strikethrough("error")); + } else { + items.push(chalk.red("error")); + } + } else if (severity === SEVERITY_LEVEL.warn) { + if (isIgnored) { + items.push(chalk.yellow.strikethrough("warn")); + } else { + items.push(chalk.yellow("warn")); + } } - if (this.problemStore.getIgnoredValidationProblemsCount() > 0) { - summary.push( - `Ignored Validation Errors(s): ${this.problemStore.getIgnoredValidationProblemsCount()}` - ); + if (locale) { + items.push(chalk.dim(locale)); + colSpan++; + } + + let messageWitDiff = isIgnored + ? `${chalk.yellow("[ignored]")} ${message}` + : message; + + if (expected || received) { + messageWitDiff += "\n\n"; + } + + if (expected) { + messageWitDiff += `${chalk.cyan("Expected")}: ${expected}\n`; + } + + if (received) { + messageWitDiff += `${chalk.magenta("Received")}: ${received}\n`; } - if (this.problemStore.getIgnoredErrorCount() > 0) { - summary.push( - `Ignored Errors: ${this.problemStore.getIgnoredErrorCount()}` + items.push(messageWitDiff); + items.push(ruleMeta.name); + + return items as Cell[]; + } + + public getNewTable(useBorders = true) { + if (useBorders) { + return new Table(); + } + + return new Table({ + chars: { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + mid: "", + "mid-mid": "", + right: "", + "right-mid": "", + middle: " ", + }, + style: { "padding-left": 0, "padding-right": 0 }, + }); + } + + public getPrintSummary() { + const summaryTable = this.getNewTable(false); + + const ignoredErrorCount = this.problemStore.getIgnoredErrorCount(); + const errorCount = this.problemStore.getErrorCount() + ignoredErrorCount; + + const ignoredWarningCount = this.problemStore.getIgnoredWarningCount(); + const warningCount = + this.problemStore.getWarningCount() + ignoredWarningCount; + + const errorSummary = [chalk.red("Errors:"), chalk.red(errorCount)]; + + if (ignoredErrorCount > 0) { + errorSummary.push( + `${chalk.dim.italic("(" + ignoredErrorCount + " ignored)")}` ); } - summary.push(`Warnings(s) found: ${this.problemStore.getWarningCount()}`); + const warningSummary = [ + chalk.yellow("Warnings:"), + chalk.yellow(warningCount), + ]; - if (this.problemStore.getIgnoredWarningCount() > 0) { - summary.push( - `Ignored Warnings: ${this.problemStore.getIgnoredWarningCount()}` + if (ignoredWarningCount > 0) { + warningSummary.push( + `${chalk.dim.italic("(" + ignoredWarningCount + " ignored)")}` ); } - return summary.join("\n"); + summaryTable.push(errorSummary); + summaryTable.push(warningSummary); + + return `\n\n${summaryTable.toString()}`; } } diff --git a/src/classes/problem-store.class.ts b/src/classes/problem-store.class.ts index 1fa7f26..1596dfa 100644 --- a/src/classes/problem-store.class.ts +++ b/src/classes/problem-store.class.ts @@ -3,20 +3,19 @@ import { Problem } from "../types.ts"; export class ProblemStore { // Validation Problems - private validationProblemsCount = 0; - private validationProblems: Problem[]; + private validationErrorCount = 0; + private validationProblems: { [locale: string]: Problem[] } = {}; + private validationWarningCount = 0; // Ignored Validation Problems - private ignoredValidationProblemsCount = 0; - private ignoredValidationProblems: Problem[]; + private ignoredValidationErrorCount = 0; + private ignoredValidationProblems: { [locale: string]: Problem[] } = {}; // Configuration Problems - private configurationProblemsCount = 0; - private configurationProblems: Problem[]; - - // Ignored Configuration Problems - private ignoredConfigurationProblemsCount = 0; - private ignoredConfigurationProblems: Problem[]; + private configurationErrorCount = 0; + private configurationWarningCount = 0; + private configurationErrors: Problem[]; + private configurationWarnings: Problem[]; // Meta Counts private errorCount = 0; @@ -26,16 +25,15 @@ export class ProblemStore { private ignoredProblemsCount = 0; constructor() { - this.validationProblems = []; - this.configurationProblems = []; - this.ignoredConfigurationProblems = []; - this.ignoredValidationProblems = []; + this.validationProblems = {}; + this.configurationErrors = []; + this.ignoredValidationProblems = {}; + this.configurationWarnings = []; } report(problem: Problem) { if (problem.isIgnored) { - this.ignore(problem); - return; + this.incrementIgnoreStats(problem); } if (problem.severity === SEVERITY_LEVEL.error) { @@ -47,60 +45,76 @@ export class ProblemStore { } if (problem.ruleMeta.type === RULE_TYPE.configuration) { - this.configurationProblemsCount += 1; - this.configurationProblems.push(problem); + this.incrementConfigurationProblemStats(problem); + this.registerConfigurationProblem(problem); } if (problem.ruleMeta.type === RULE_TYPE.validation) { - this.validationProblemsCount += 1; - this.validationProblems.push(problem); + this.incrementValidationProblemStats(problem); + this.registerValidationProblem(problem); } } - ignore(problem: Problem) { - this.ignoredProblemsCount += 1; - + registerValidationProblem(problem: Problem) { if (problem.severity === SEVERITY_LEVEL.error) { - this.ignoredErrorCount += 1; + this.validationErrorCount += 1; + } else if (problem.severity === SEVERITY_LEVEL.warn) { + this.validationWarningCount += 1; } - if (problem.severity === SEVERITY_LEVEL.warn) { - this.ignoredWarningCount += 1; + if (!this.validationProblems?.[problem.locale]) { + this.validationProblems[problem.locale] = []; } - if (problem.ruleMeta.type === RULE_TYPE.configuration) { - this.ignoredConfigurationProblemsCount += 1; - this.ignoredConfigurationProblems.push(problem); - } + this.validationProblems[problem.locale].push(problem); + } - if (problem.ruleMeta.type === RULE_TYPE.validation) { - this.ignoredValidationProblemsCount += 1; - this.ignoredValidationProblems.push(problem); + registerConfigurationProblem(problem: Problem) { + if (problem.severity === SEVERITY_LEVEL.error) { + this.configurationErrorCount += 1; + this.configurationErrors.push(problem); + } else if (problem.severity === SEVERITY_LEVEL.warn) { + this.configurationWarningCount += 1; + this.configurationWarnings.push(problem); } } - getProblems() { - return [...this.validationProblems, ...this.configurationProblems]; + incrementValidationProblemStats(problem: Problem) { + this.validationErrorCount += 1; + + if (problem.severity === SEVERITY_LEVEL.error) { + this.validationErrorCount += 1; + } else if (problem.severity === SEVERITY_LEVEL.warn) { + this.configurationWarningCount += 1; + } } - getAllProblems() { - return [ - ...this.validationProblems, - ...this.ignoredValidationProblems, - ...this.configurationProblems, - ...this.ignoredConfigurationProblems, - ]; + incrementConfigurationProblemStats(problem: Problem) { + if (problem.severity === SEVERITY_LEVEL.error) { + this.configurationErrorCount += 1; + } else if (problem.severity === SEVERITY_LEVEL.warn) { + this.configurationWarningCount += 1; + } } - getIgnoredProblems() { - return [ - ...this.ignoredValidationProblems, - ...this.ignoredConfigurationProblems, - ]; + incrementIgnoreStats(problem: Problem) { + this.ignoredProblemsCount += 1; + + if (problem.severity === SEVERITY_LEVEL.error) { + this.ignoredErrorCount += 1; + } + + if (problem.severity === SEVERITY_LEVEL.warn) { + this.ignoredWarningCount += 1; + } + + if (problem.ruleMeta.type === RULE_TYPE.validation) { + this.ignoredValidationErrorCount += 1; + } } - getIgnoredConfigurationProblems() { - return this.ignoredConfigurationProblems; + getIgnoredProblems() { + return this.ignoredValidationProblems; } getIgnoredValidationProblems() { @@ -108,7 +122,7 @@ export class ProblemStore { } getConfigurationProblems() { - return this.configurationProblems; + return [...this.configurationErrors, ...this.configurationWarnings]; } getValidationProblems() { @@ -131,19 +145,19 @@ export class ProblemStore { return this.warningCount; } - getIgnoredValidationProblemsCount() { - return this.ignoredValidationProblemsCount; + getIgnoredValidationErrorCount() { + return this.ignoredValidationErrorCount; } - getIgnoredConfigurationProblemsCount() { - return this.ignoredConfigurationProblemsCount; + getConfigurationErrorCount() { + return this.configurationErrorCount; } - getConfigurationProblemCount() { - return this.configurationProblemsCount; + getConfigurationWarningCount() { + return this.configurationWarningCount; } getValidationProblemCount() { - return this.validationProblemsCount; + return this.validationErrorCount; } } diff --git a/src/engine/rule-engine.ts b/src/engine/rule-engine.ts index fc58622..0ba1643 100644 --- a/src/engine/rule-engine.ts +++ b/src/engine/rule-engine.ts @@ -30,7 +30,7 @@ function runRules(config: Config) { }); }); - if (problemStore.getConfigurationProblemCount() > 0) { + if (problemStore.getConfigurationErrorCount() > 0) { exitRun(problemStore, config.dryRun); return; } @@ -51,11 +51,11 @@ function runRules(config: Config) { } function exitRun(problemStore: ProblemStore, isDryRun: boolean) { - const problems = problemStore.getProblems(); + const errorCount = problemStore.getErrorCount(); - const hasErrors = problems.length > 0; + const hasErrors = errorCount > 0; - const logger = new Logger(problemStore); + const logger = new Logger(problemStore, isDryRun); logger.logErrors(); diff --git a/src/rules/no-invalid-severity/problems.ts b/src/rules/no-invalid-severity/problems.ts index af8435d..c1c5f1c 100644 --- a/src/rules/no-invalid-severity/problems.ts +++ b/src/rules/no-invalid-severity/problems.ts @@ -19,8 +19,8 @@ export function getInvalidSeverityProblem( return Problem.Builder.withRuleMeta(ruleMeta) .withSeverity(severity) .withMessage( - `Invalid severity: "${invalidSeverity}" assigned to ${ruleName}. You must use: ${validSeverities.join( - "|" + `Invalid severity: "${invalidSeverity}" assigned to ${ruleName}. Use: ${validSeverities.join( + " | " )}` ) .build(); diff --git a/src/utils/rules-helpers.ts b/src/utils/rules-helpers.ts index b6a47d5..755f892 100644 --- a/src/utils/rules-helpers.ts +++ b/src/utils/rules-helpers.ts @@ -1,3 +1,4 @@ +import { validSeverities } from "../constants"; import type { Config, Rule, RuleSeverity } from "../types"; /** @@ -9,13 +10,14 @@ import type { Config, Rule, RuleSeverity } from "../types"; export function getRuleSeverity(config: Config, rule: Rule): RuleSeverity { const ruleConfig = config.rules[rule.meta.name]; - if (typeof ruleConfig === "string") { + if (typeof ruleConfig === "string" && validSeverities.includes(ruleConfig)) { return ruleConfig; } if ( typeof ruleConfig === "object" && - typeof ruleConfig?.severity === "string" + typeof ruleConfig?.severity === "string" && + validSeverities.includes(ruleConfig.severity) ) { return ruleConfig.severity; }