Files
gitlab-foss/scripts/frontend/generate_eslint_todo_list.mjs
2025-02-18 15:11:49 +00:00

165 lines
5.1 KiB
JavaScript

import path from 'node:path';
import fs from 'node:fs';
import { ESLint } from 'eslint';
import { program } from 'commander';
import * as prettier from 'prettier';
import kebabCase from 'lodash/kebabCase.js';
import camelCase from 'lodash/camelCase.js';
import sortBy from 'lodash/sortBy.js';
import eslintConfig from '../../eslint.config.mjs';
const ROOT_PATH = path.resolve(import.meta.dirname, '../../');
function createESLintInstance(overrideConfig) {
return new ESLint({ overrideConfigFile: true, overrideConfig, fix: false });
}
function lint(eslint, filePaths) {
return eslint.lintFiles(filePaths);
}
/**
* Creates a barebone ESLint config to lint the codebase with. We only keep configs that make use
* of the rule we are generating a todo file for. If no config use the rule, this returns `null` and
* should cause the script to abort its execution.
*
* @param {string} rule The rule to generate a todo file for.
* @returns {object|null} The config to use for the rule.
*/
function getConfigForRule(rule) {
let configHasRule = false;
const newConfig = eslintConfig
.map((config) => {
// We preserve existing configs for the rule so that we don't add valid files to the todo file.
// However, we bypass configs that disabled the rule as those are likely the todo files themselves.
const hasRuleDefinition = config.rules?.[rule] && config.rules[rule] !== 'off';
if (hasRuleDefinition) {
configHasRule = true;
return {
...config,
rules: {
[rule]: config.rules[rule],
},
};
}
return {
...config,
rules: {},
};
})
.filter((config) => config !== null);
if (configHasRule) {
return newConfig;
}
return null;
}
function getOffendingFiles(results, rule) {
return results.reduce((acc, result) => {
const hasRuleError = result.messages.some((message) => message.ruleId === rule);
if (hasRuleError) {
acc.push(result.filePath);
}
return acc;
}, []);
}
async function prettify(data) {
const prettierConfig = await prettier.resolveConfig(path.join(ROOT_PATH, '.prettierrc'));
return prettier.format(data, {
...prettierConfig,
parser: 'babel',
});
}
async function writeTodoFile(rule, offendingFiles) {
const slugifiedRule = kebabCase(rule);
const todoFileName = `${slugifiedRule}.mjs`;
const todoFilePath = path.join(ROOT_PATH, '.eslint_todo', todoFileName);
const camelCasedRule = camelCase(rule);
const relativePaths = sortBy(offendingFiles.map((file) => path.relative(ROOT_PATH, file)))
.map((relativePath) => `'${relativePath}'`)
.join(',\n');
const indexFilePath = path.join(ROOT_PATH, '.eslint_todo', 'index.mjs');
const indexFileContent = fs.readFileSync(indexFilePath, { encoding: 'utf-8' });
console.log(`Writing todo file to ${todoFilePath}.`);
const newConfig = `
/**
* Generated by \`scripts/frontend/generate_eslint_todo_list.mjs\`.
*/
export default {
files: [${relativePaths}],
rules: {
'${rule}': 'off',
},
}
`;
const formattedTodoFileContent = await prettify(newConfig);
fs.writeFileSync(todoFilePath, formattedTodoFileContent);
if (!indexFileContent.match(camelCasedRule)) {
console.log(`Adding export statement to ${indexFilePath}.`);
const exportStatement = `export { default as ${camelCasedRule} } from './${todoFileName}';`;
const newIndexFileContent = `${indexFileContent}\n${exportStatement}`;
const formattedNewIndexFileContent = await prettify(newIndexFileContent);
fs.writeFileSync(indexFilePath, formattedNewIndexFileContent);
} else {
console.log(`Export statement already exists in ${indexFilePath}.`);
}
}
async function main() {
program
.description(
'Generates a todo file to skip linting on offending files for a specific ESLint rule.',
)
.option('--debug-config', 'Prints the ESLint config used to generate the todo file.')
.argument('<rule>')
.parse(process.argv);
const options = program.opts();
const [rule] = program.args;
console.log(`Generating todo file for rule \`${rule}\`...`);
const overrideConfig = getConfigForRule(rule);
if (overrideConfig === null) {
console.error(
`The rule \`${rule}\` could not be found in the ESLint configuration. It needs to be enabled before generating a todo file.`,
);
process.exitCode = 1;
return;
}
if (options.debugConfig) {
console.log('Using ESLint configuration:');
console.log(overrideConfig);
}
const eslint = createESLintInstance(overrideConfig);
const results = await lint(eslint, [
'./app/assets/javascripts/**/*.{js,mjs,cjs,vue}',
'./ee/app/assets/javascripts/**/*.{js,mjs,cjs,vue}',
'./spec/frontend/**/*.js',
'./ee/spec/frontend/**/*.js',
'scripts/**/*.{js,mjs,cjs}',
]);
const offendingFiles = getOffendingFiles(results, rule);
if (offendingFiles.length > 0) {
console.log(`Found ${offendingFiles.length} offending files.`);
await writeTodoFile(rule, offendingFiles);
} else {
console.error('No offenses found. Delete any existing todo file if it is not needed anymore.');
process.exitCode = 1;
}
}
main();