| // 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); | |
| }); |