blob: 17424626cee04cb2ddfea52c827d0984df37e834 [file]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {globby} from 'globby';
import {extname, isAbsolute, relative, resolve} from 'node:path';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {devtoolsRootPath} from '../devtools_paths.js';
import {runESLint} from './eslint.mjs';
import {runLitAnalyzer} from './litanalyzer.mjs';
import {runStylelint} from './stylelint.mjs';
import {runTypeCheck} from './typecheck.mjs';
const flags = yargs(hideBin(process.argv))
.option('fix', {
type: 'boolean',
default: true,
describe: 'Automatically fix, where possible, problems reported by rules.',
})
.option('force-fix', {
type: 'boolean',
default: false,
describe:
'Disables inline rule and allows auto fixers to run unconditionally.',
})
.option('debug', {
type: 'boolean',
default: false,
describe:
'Disable cache validations during debugging, useful for custom rule creation/debugging.',
})
.option('lint-only', {
type: 'boolean',
// LUCI_CONTEXT is an env that exists on the bots
// We want to disable caches and run more logging there.
default: Boolean(process.env['LUCI_CONTEXT']),
describe:
'Runs the linter against all files, ignores passed files, ignores caches, ignores --fix.',
})
.usage('$0 [<files...>]', 'Run the linter on the provided files', yargs => {
return yargs.positional('files', {
describe: 'File(s), glob(s), or directories',
type: 'string',
array: true,
default: ['.'],
});
})
.parseSync();
if (!flags.fix) {
console.log('[lint]: fix is disabled; no errors will be autofixed.');
}
if (flags.forceFix && !flags.fix) {
throw new Error('`--force-fix` needs `--fix` to work as intended');
}
if (flags.debug) {
console.log('[lint]: Cache disabled, linting may take longer.');
}
const LIT_ANALYZER_EXCLUDED_FOLDERS = [
'front_end/core',
'front_end/foundation',
'front_end/generated',
'front_end/legacy_test_runner',
'front_end/models',
'front_end/services',
'front_end/testing',
'front_end/third_party',
];
function getFilesToLint() {
if (flags.lintOnly) {
return ['.'];
}
if (Array.isArray(flags.files)) {
return flags.files;
}
return [flags.files];
}
/**
* Expands mixed file and folder targets.
* @param {string[]} targets Array of file paths or directory paths.
* @param {string} rootDir Base directory.
*/
export async function findRelavantFiles(targets, rootDir) {
const relativeTargets = [];
for (const target of targets) {
const resolvedPath = resolve(target);
const relativePath = relative(rootDir, resolvedPath);
if (!relativePath.startsWith('..') && !isAbsolute(relativePath)) {
relativeTargets.push((relativePath || '.').replaceAll('\\', '/'));
}
}
if (relativeTargets.length === 0) {
return [];
}
return await globby(relativeTargets, {
cwd: rootDir,
expandDirectories: {
extensions: ['css', 'mjs', 'js', 'ts'],
},
gitignore: false,
ignore: [
'**/node_modules/**',
'**/.git/**',
'**/out/**',
'**/third_party/**',
'build/**',
'buildtools/**',
'test/ai_evals/eval_data/**',
],
});
}
async function run() {
const files = getFilesToLint();
const scripts = [];
const styles = [];
const matchedPaths = await findRelavantFiles(files, devtoolsRootPath());
for (const path of matchedPaths) {
if (extname(path) === '.css') {
styles.push(path);
} else {
scripts.push(path);
}
}
const frontEndFiles = scripts.filter(script => {
// LitAnalyzer is filtered due to high memory usage and noise in
// specific large or legacy folders.
const isInExcludedFolder = LIT_ANALYZER_EXCLUDED_FOLDERS.some(folder =>
script.includes(folder),
);
return (
// Only include front_end files, as we use Lit
// only there
script.includes('front_end') &&
// Don't lint test files as we don't use Lit
!script.endsWith('.test.ts') &&
!script.endsWith('.test.api.ts') &&
!script.endsWith('.docs.ts') &&
!isInExcludedFolder
);
});
const options = {
fix: flags.fix,
forceFix: flags.forceFix,
debug: flags.debug,
lintOnly: flags.lintOnly,
};
const results = await Promise.allSettled([
runESLint(scripts, options),
runLitAnalyzer(frontEndFiles, options),
runStylelint(styles, options),
runTypeCheck(scripts, options),
]);
let succeed = true;
for (const result of results) {
if (result.status === 'rejected') {
console.error(result.reason);
succeed = false;
continue;
}
const {status, output} = result.value;
succeed &&= status;
if (output) {
console.log(output);
}
}
return succeed;
}
run()
.then(succeed => {
process.exit(succeed ? 0 : 1);
})
.catch(err => {
console.log(`[lint]: ${err.message}`, err.stack);
process.exit(1);
});