UNPKG

@adguard/aglint

Version:

Universal adblock filter list linter.

185 lines (182 loc) 6.99 kB
/* * AGLint v3.0.0 (build date: Wed, 21 May 2025 13:24:14 GMT) * (c) 2025 AdGuard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/AGLint#readme */ import chalk from 'chalk'; import terminalLink from 'terminal-link'; import stripAnsi from 'strip-ansi'; import table from 'text-table'; import { inflect } from 'inflection'; import path from 'node:path'; import { DOUBLE_NEWLINE, EMPTY, NEWLINE, REPO_URL, HASHMARK, SPACE, OPEN_PARENTHESIS, COMMA, CLOSE_PARENTHESIS, DOT } from '../../../common/constants.js'; const ALIGN_LEFT = 'l'; const ALIGN_CENTER = 'c'; /** * Implements a simple reporter that logs the problems to the console. * You can implement any reporter you want, as long as it implements the * `LinterCliReporter` interface. */ class LinterConsoleReporter { /** * We save the start time here, so we can calculate the total time at the end. */ startTime = null; /** * Custom Chalk instance to use for coloring the console output. */ chalk; /** * Total number of warnings */ warnings = 0; /** * Total number of errors */ errors = 0; /** * Total number of fatal errors */ fatals = 0; /** * Creates a new console reporter instance. * * @param colors Whether to use colors in the console output or not */ constructor(colors = true) { // By default, Chalk determines the color support automatically, but we // want to disable colors if the user doesn't want them. const chalkOptions = colors ? {} : { level: 0 }; this.chalk = new chalk.Instance(chalkOptions); } /** * Collected problems, where the key is the file path and the value is an array of problems. * We only collect problems here, we don't log them, because we want to log them in the end. */ problems = {}; onLintStart = () => { // Save the start time this.startTime = performance.now(); }; onFileStart = (file) => { // Initialize the problems array for the file as empty this.problems[path.join(file.dir, file.base)] = []; }; onFileEnd = (file, result) => { // Initialize the problems array for the file as empty this.problems[path.join(file.dir, file.base)]?.push(...result.problems); // Count the problems this.warnings += result.warningCount; this.errors += result.errorCount; this.fatals += result.fatalErrorCount; }; onLintEnd = () => { // Calculate the linting time // eslint-disable-next-line max-len const lintTime = Math.round(((this.startTime ? performance.now() - this.startTime : 0) + Number.EPSILON) * 100) / 100; let output = EMPTY; let timeOutput = EMPTY; if (lintTime > 0) { timeOutput += `Linting took ${this.chalk.dim(lintTime)} ms.`; } else { timeOutput += this.chalk.red('Error: Could not calculate linting time.'); } // If there are no problems, log that there are no problems if (this.warnings === 0 && this.errors === 0 && this.fatals === 0) { output += this.chalk.green('No problems found!'); output += DOUBLE_NEWLINE; output += timeOutput; // eslint-disable-next-line no-console console.log(output); return; } // If there are problems, log them to the console for (const [file, problems] of Object.entries(this.problems)) { // Typically when found problems are fixed if (problems.length === 0) { continue; } output += this.chalk.underline(file); output += NEWLINE; const rows = []; for (const problem of problems) { const row = []; // Column 1: Empty column row.push(EMPTY); // Column 2: Problem location row.push(`${problem.position.startLine}:${problem.position.startColumn !== undefined ? problem.position.startColumn : 0}`); // Column 3: Problem type switch (problem.severity) { case 1: { row.push(this.chalk.yellow('warn')); break; } case 2: { row.push(this.chalk.red('error')); break; } case 3: { row.push(this.chalk.red('fatal')); break; } default: { row.push(this.chalk.red('error')); } } // Column 4: Problem description row.push(problem.message); // Column 5: Linter rule name (if available) if (problem.rule) { // Some terminals support links, so we can link to the rule documentation directly // in this case. if (terminalLink.isSupported) { row.push(terminalLink(problem.rule, `${REPO_URL}${HASHMARK}${problem.rule}`)); } else { row.push(this.chalk.dim(problem.rule)); } } else { row.push(EMPTY); } rows.push(row); } if (rows.length > 0) { output += table(rows, { align: [ALIGN_LEFT, ALIGN_CENTER, ALIGN_LEFT, ALIGN_LEFT, ALIGN_LEFT], stringLength(str) { return stripAnsi(str).length; }, }); output += DOUBLE_NEWLINE; } } const anyErrors = this.errors > 0 || this.fatals > 0; const total = this.warnings + this.errors + this.fatals; // Stats output += 'Found'; output += SPACE; output += this.chalk[anyErrors ? 'red' : 'yellow'](total); output += SPACE; output += inflect('problem', total); output += SPACE + OPEN_PARENTHESIS; // Warnings output += this.chalk.yellow(`${this.warnings} ${inflect('warning', this.warnings)}`); output += COMMA + SPACE; // Errors output += this.chalk.red(`${this.errors} ${inflect('error', this.errors)}`); output += SPACE; output += 'and'; output += SPACE; // Fatal errors output += this.chalk.red(`${this.fatals} fatal ${inflect('error', this.fatals)}`); output += CLOSE_PARENTHESIS + DOT + DOUBLE_NEWLINE; // Linting time output += timeOutput; // eslint-disable-next-line no-console console[anyErrors ? 'error' : 'warn'](output); }; } export { LinterConsoleReporter };