@adguard/aglint
Version:
Universal adblock filter list linter.
147 lines (144 loc) • 5.63 kB
JavaScript
/*
* 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 path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { pathExists } from 'fs-extra';
import cloneDeep from 'clone-deep';
import { Linter } from '../index.js';
import { walk } from './walk.js';
import { scan } from './scan.js';
import { buildConfigForDirectory } from './config-builder.js';
import { NoConfigError } from './errors/no-config-error.js';
/**
* Implements CLI functionality for the linter. Typically used by the `aglint` command in Node.js environment.
*/
class LinterCli {
/**
* The reporter to be used for the CLI.
*/
reporter;
fix;
ignore;
errors;
/**
* Creates a new `LinterCli` instance.
*
* @param reporter The reporter to be used for the CLI
* @param fix Fix fixable problems automatically
* @param ignore Use `.aglintignore` files
*/
constructor(reporter, fix = false, ignore = true) {
this.reporter = reporter;
this.fix = fix;
this.ignore = ignore;
this.errors = false;
}
/**
* Lints a file with the given config. Also compatible with the `WalkEvent` type,
* so it can be used with the `walk` function.
*
* @param file The file to be linted
* @param config The active linter config (merged from parent directories)
*/
lintFile = async (file, config) => {
// Create a new linter instance and add the default rules
const linter = new Linter(true, config);
// Get the file path from the current directory
const filePath = path.join(file.dir, file.base);
// Clone the config in order to prevent unwanted changes
const configDeepClone = cloneDeep(config);
// If the reporter has an onFileStart event, call it
if (this.reporter.onFileStart) {
this.reporter.onFileStart(file, configDeepClone);
}
// Lint the file
let result = linter.lint(await readFile(filePath, 'utf8'), this.fix || false);
// Set the errors flag if there are any errors
if (!this.errors && (result.errorCount > 0 || result.fatalErrorCount > 0)) {
this.errors = true;
}
// If fix is enabled and fixable problems were found, write the fixed file,
// then re-lint the fixed file to see if there are still problems
if (this.fix) {
if (result.fixed) {
await writeFile(filePath, result.fixed);
// TODO: Currently fixing runs only once, but it should run until there are no more fixable problems
result = linter.lint(result.fixed, this.fix || false);
}
}
// If the reporter has an onFileStart event, call it
if (this.reporter.onFileEnd) {
this.reporter.onFileEnd(file, result, configDeepClone);
}
};
/**
* Returns true if the linter found any errors.
*
* @returns `true` if the linter found any errors, `false` otherwise
*/
hasErrors = () => {
return this.errors;
};
/**
* Lints the current working directory. If you specify files, it will only lint those files.
*
* @param cwd The current working directory
* @param files The files to be linted (if not specified, it will scan the cwd)
*/
run = async (cwd, files = []) => {
// If the reporter has an onLintStart event, call it
if (this.reporter.onLintStart) {
this.reporter.onLintStart();
}
// If files are specified, use them instead of scanning the cwd
if (files.length > 0) {
for (const file of files) {
let fullPath;
if (path.isAbsolute(file)) {
// If file path is absolute, use it as is
fullPath = file;
}
else {
// Otherwise, join relative path with cwd
fullPath = path.join(cwd, file);
}
// TODO: Add support for glob patterns
// Check if the file exists
if (!(await pathExists(fullPath))) {
throw new Error(`File "${fullPath}" does not exist`);
}
// Parse the file path
const parsedFile = path.parse(fullPath);
// Get config for the directory where the file is located
const config = await buildConfigForDirectory(parsedFile.dir);
// Lint the file
await this.lintFile(parsedFile, config);
}
}
else {
// Get the config for the cwd, should exist
const rootConfig = await buildConfigForDirectory(cwd);
if (!rootConfig) {
throw new NoConfigError(cwd);
}
// Run the scanner on the cwd
const scanResult = await scan(cwd, [], this.ignore);
// Walk through the scanned directory and lint all files
await walk(scanResult, {
// Call the lint function for each file during the walk
file: this.lintFile,
},
// Use cwd config as base config for the walker
rootConfig, this.fix);
}
// If the reporter has an onLintEnd event, call it
if (this.reporter.onLintEnd) {
this.reporter.onLintEnd();
}
};
}
export { LinterCli };