mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-25 16:03:48 +00:00
165 lines
5.1 KiB
JavaScript
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();
|