@adguard/dead-domains-linter
Version:
Simple tool to check adblock filtering rules for dead domains.
189 lines (165 loc) • 5.81 kB
JavaScript
const fs = require('fs');
const consola = require('consola');
// eslint-disable-next-line import/no-unresolved
const consolaUtils = require('consola/utils');
const glob = require('glob');
const packageJson = require('../package.json');
const utils = require('./utils');
const fileLinter = require('./filelinter');
// eslint-disable-next-line import/order
const { argv } = require('yargs')
.usage('Usage: $0 [options]')
.example(
'$0 -i **/*.txt',
'scan all .txt files in the current directory and subdirectories in the interactive mode',
)
.example(
'$0 -a -i filter.txt',
'scan filter.txt and automatically apply suggested fixes',
)
.option('input', {
alias: 'i',
type: 'string',
description: 'glob expression that selects files that the tool will scan.',
})
.option('dnscheck', {
type: 'boolean',
description: 'Double-check dead domains with a DNS query.',
})
.option('commentout', {
type: 'boolean',
description: 'Comment out rules instead of removing them.',
})
.option('export', {
type: 'string',
description: 'Export dead domains to the specified file instead of modifying the files.',
})
.option('import', {
type: 'string',
description: 'Import dead domains from the specified file and skip other checks.',
})
.option('ignore', {
type: 'string',
description: 'File with domains to ignore.',
})
.option('auto', {
alias: 'a',
type: 'boolean',
description: 'Automatically apply suggested fixes without asking the user.',
})
.option('show', {
alias: 's',
type: 'boolean',
description: 'Show suggestions without applying them.',
})
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging',
})
.default('input', '**/*.txt')
.default('dnscheck', true)
.default('commentout', false)
.default('auto', false)
.default('show', false)
.default('verbose', false)
.version()
.help('h')
.alias('h', 'help');
if (argv.verbose) {
// trace level.
consola.level = 5;
}
/**
* Extracts the list of dead domains from raw linting results.
*
* @param {import('./filelinter').FileResult} fileResult - Result of linting
* the file.
* @returns {Array<string>} Array of dead domains.
*/
function getDeadDomains(fileResult) {
if (!fileResult || !fileResult.results) {
return [];
}
return fileResult.results.map((result) => {
return result.linterResult.deadDomains;
}).reduce((acc, val) => {
return acc.concat(val);
}, []);
}
/**
* Entry point into the CLI program logic.
*/
async function main() {
consola.info(`Starting ${packageJson.name} v${packageJson.version}`);
const globExpression = argv.input;
const files = glob.globSync(globExpression);
const plural = files.length > 1 || files.length === 0;
let predefinedDomains;
if (argv.import) {
consola.info(`Importing dead domains from ${argv.import}, other checks will be skipped`);
try {
predefinedDomains = fs.readFileSync(argv.import).toString()
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line !== '');
} catch (ex) {
consola.error(`Failed to read from ${argv.import}: ${ex}`);
process.exit(1);
}
consola.info(`Imported ${predefinedDomains.length} dead domains`);
}
consola.info(`Found ${files.length} file${plural ? 's' : ''} matching ${globExpression}`);
let ignoreDomainsList = [];
if (argv.ignore) {
consola.info(`Importing domains to ignore from ${argv.ignore}`);
try {
ignoreDomainsList = fs.readFileSync(argv.ignore).toString()
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line !== '');
} catch (ex) {
consola.error(`Failed to read from ${argv.ignore}: ${ex}`);
process.exit(1);
}
consola.info(`Imported ${ignoreDomainsList.length} domains to ignore`);
}
const ignoreDomains = new Set(ignoreDomainsList);
// This array is used when export is enabled.
const deadDomains = [];
for (let i = 0; i < files.length; i += 1) {
const file = files[i];
try {
consola.info(consolaUtils.colorize('bold', `Processing file ${file}`));
const linterOptions = {
show: argv.show,
auto: argv.auto || !!argv.export,
useDNS: argv.dnscheck,
commentOut: argv.commentout,
deadDomains: predefinedDomains,
ignoreDomains,
};
// eslint-disable-next-line no-await-in-loop
const fileResult = await fileLinter.lintFile(file, linterOptions);
if (fileResult !== null) {
if (argv.export) {
deadDomains.push(...getDeadDomains(fileResult));
} else {
// eslint-disable-next-line no-await-in-loop
await fileLinter.applyFileChanges(file, fileResult, linterOptions);
}
}
} catch (ex) {
consola.error(`Failed to process ${file} due to ${ex}`);
process.exit(1);
}
}
if (argv.export) {
consola.info(`Exporting the list of dead domains to ${argv.export}`);
const uniqueDomains = utils.unique(deadDomains);
fs.writeFileSync(argv.export, uniqueDomains.join('\n'));
}
consola.success('Finished successfully');
}
main();