Skip to content

Commit

Permalink
feat(tasks): Re-introduce tasks/lint_rules (oxc-project#2166)
Browse files Browse the repository at this point in the history
  • Loading branch information
leaysgur authored Jan 25, 2024
1 parent 51cecbb commit 80a4546
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
members = ["crates/*", "tasks/*", "napi/*"]
exclude = ["tasks/lint_rules2"]

[workspace.package]
authors = ["Boshen <boshenc@gmail.com>", "Oxc contributors"]
Expand Down
2 changes: 2 additions & 0 deletions tasks/lint_rules2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
package-lock.json
36 changes: 36 additions & 0 deletions tasks/lint_rules2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# tasks/lint_rules2

Task to update implementation progress for each linter plugin.

```sh
Usage:
$ cmd [--target=<pluginName>]... [--update] [--help]

Options:
--target, -t: Which plugin to target, multiple allowed
--update: Update the issue instead of printing to stdout
--help, -h: Print this help message
```

Environment variables `GITHUB_TOKEN` is required when `--update` is specified.

## Design

- Always install `eslint-plugin-XXX@latest` from npm
- Load them through ESLint Node.js API
- https://eslint.org/docs/latest/integrate/nodejs-api#linter
- List all their plugin rules(name, deprecated, recommended, docs, etc...)
- List all our implemented rules(name)
- Combine these lists and render as markdown
- Update GitHub issue body

## FAQ

- Why is this task written in Node.js? Why not Rust?
- Some plugins do not provide static rules list
- https://github.com/jest-community/eslint-plugin-jest/
- Easiest way to collect the list is just evaluating config file in JavaScript
- Why `.cjs`?
- To keep dependencies as simple as possible
- Some plugins only provide their module as CommonJS
- So, CommonJS is the only way to go without bundling
10 changes: 10 additions & 0 deletions tasks/lint_rules2/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"checkJs": true,
"module": "node",
"moduleResolution": "node",
"lib": ["esnext"],
"target": "esnext",
"strict": true
}
}
18 changes: 18 additions & 0 deletions tasks/lint_rules2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"private": true,
"name": "lint_rules2",
"main": "./src/main.cjs",
"version": "0.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "latest",
"eslint": "latest",
"eslint-plugin-import": "latest",
"eslint-plugin-jest": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-n": "latest",
"eslint-plugin-unicorn": "latest"
}
}
129 changes: 129 additions & 0 deletions tasks/lint_rules2/src/eslint-rules.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const { Linter } = require("eslint");

// NOTICE!
// Plugins do not provide their type definitions, and also `@types/*` do not exist!
// Even worse, every plugin has slightly different types, different way of configuration in detail...
//
// So here, we need to list all rules while normalizing recommended and deprecated flags.
// - rule.meta.docs.recommended
// - rule.meta.deprecated

// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/index.ts
const {
rules: pluginTypeScriptAllRules,
configs: pluginTypeScriptConfigs,
} = require("@typescript-eslint/eslint-plugin");
// https://github.com/eslint-community/eslint-plugin-n/blob/master/lib/index.js
const { rules: pluginNAllRules } = require("eslint-plugin-n");
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/index.js
const {
rules: pluginUnicornAllRules,
configs: pluginUnicornConfigs,
} = require("eslint-plugin-unicorn");
// https://github.com/gajus/eslint-plugin-jsdoc/blob/main/src/index.js
const {
// @ts-expect-error: Module has no exported member
rules: pluginJSDocAllRules,
// @ts-expect-error: Module has no exported member
configs: pluginJSDocConfigs,
} = require("eslint-plugin-jsdoc");
// https://github.com/import-js/eslint-plugin-import/blob/main/src/index.js
const {
rules: pluginImportAllRules,
configs: pluginImportConfigs,
} = require("eslint-plugin-import");
// https://github.com/jest-community/eslint-plugin-jest/blob/main/src/index.ts
const { rules: pluginJestAllRules } = require("eslint-plugin-jest");

// All rules(including deprecated, recommended) are loaded initially.
exports.createESLintLinter = () => new Linter();

/** @param {import("eslint").Linter} linter */
exports.loadPluginTypeScriptRules = (linter) => {
// We want to list all rules but not support type-checked rules
const pluginTypeScriptDisableTypeCheckedRules = new Map(
Object.entries(pluginTypeScriptConfigs["disable-type-checked"].rules),
);
for (const [name, rule] of Object.entries(pluginTypeScriptAllRules)) {
if (
pluginTypeScriptDisableTypeCheckedRules.has(`@typescript-eslint/${name}`)
)
continue;

const prefixedName = `typescript/${name}`;

linter.defineRule(prefixedName, rule);
}
};

/** @param {import("eslint").Linter} linter */
exports.loadPluginNRules = (linter) => {
for (const [name, rule] of Object.entries(pluginNAllRules)) {
const prefixedName = `n/${name}`;

// @ts-expect-error: The types of 'meta.fixable', 'null' is not assignable to type '"code" | "whitespace" | undefined'.
linter.defineRule(prefixedName, rule);
}
};

/** @param {import("eslint").Linter} linter */
exports.loadPluginUnicornRules = (linter) => {
const pluginUnicornRecommendedRules = new Map(
Object.entries(pluginUnicornConfigs.recommended.rules),
);
for (const [name, rule] of Object.entries(pluginUnicornAllRules)) {
const prefixedName = `unicorn/${name}`;

// If name is presented and value is not "off", it is recommended
const recommendedValue = pluginUnicornRecommendedRules.get(prefixedName);
// @ts-expect-error: `rule.meta.docs` is possibly `undefined`
rule.meta.docs.recommended = recommendedValue && recommendedValue !== "off";

linter.defineRule(prefixedName, rule);
}
};

/** @param {import("eslint").Linter} linter */
exports.loadPluginJSDocRules = (linter) => {
const pluginJSDocRecommendedRules = new Map(
Object.entries(pluginJSDocConfigs.recommended.rules),
);
for (const [name, rule] of Object.entries(pluginJSDocAllRules)) {
const prefixedName = `jsdoc/${name}`;

// If name is presented and value is not "off", it is recommended
const recommendedValue = pluginJSDocRecommendedRules.get(prefixedName);
rule.meta.docs.recommended = recommendedValue && recommendedValue !== "off";

linter.defineRule(prefixedName, rule);
}
};

/** @param {import("eslint").Linter} linter */
exports.loadPluginImportRules = (linter) => {
const pluginImportRecommendedRules = new Map(
// @ts-expect-error: Property 'rules' does not exist on type 'Object'.
Object.entries(pluginImportConfigs.recommended.rules),
);
for (const [name, rule] of Object.entries(pluginImportAllRules)) {
const prefixedName = `import/${name}`;

// @ts-expect-error: Property 'recommended' does not exist on type
rule.meta.docs.recommended = pluginImportRecommendedRules.has(prefixedName);

// @ts-expect-error: The types of 'meta.type', 'string' is not assignable to type '"problem" | "suggestion" | "layout" | undefined'.
linter.defineRule(prefixedName, rule);
}
};

/** @param {import("eslint").Linter} linter */
exports.loadPluginJestRules = (linter) => {
for (const [name, rule] of Object.entries(pluginJestAllRules)) {
const prefixedName = `jest/${name}`;

// Presented but type is `string | false`
rule.meta.docs.recommended = typeof rule.meta.docs.recommended === "string";

linter.defineRule(prefixedName, rule);
}
};
93 changes: 93 additions & 0 deletions tasks/lint_rules2/src/main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const { parseArgs } = require("node:util");
const {
createESLintLinter,
loadPluginTypeScriptRules,
loadPluginNRules,
loadPluginUnicornRules,
loadPluginJSDocRules,
loadPluginImportRules,
loadPluginJestRules,
} = require("./eslint-rules.cjs");
const {
createRuleEntries,
readAllImplementedRuleNames,
updateNotSupportedStatus,
updateImplementedStatus,
} = require("./oxlint-rules.cjs");
const { renderRulesList, renderLayout } = require("./output-markdown.cjs");

const ALL_TARGET_PLUGIN_NAMES = new Set([
"eslint",
"typescript",
"n",
"unicorn",
"jsdoc",
"import",
"jest",
]);

const HELP = `
Usage:
$ cmd [--target=<pluginName>]... [--update] [--help]
Options:
--target, -t: Which plugin to target, multiple allowed
--update: Update the issue instead of printing to stdout
--help, -h: Print this help message
Plugins: ${[...ALL_TARGET_PLUGIN_NAMES].join(", ")}
`;

(async () => {
//
// Parse arguments
//
const { values } = parseArgs({
options: {
target: { type: "string", short: "t", multiple: true },
update: { type: "boolean" },
help: { type: "boolean", short: "h" },
},
});

if (values.help) return console.log(HELP);

const targetPluginNames = new Set(values.target ?? ALL_TARGET_PLUGIN_NAMES);
for (const pluginName of targetPluginNames) {
if (!ALL_TARGET_PLUGIN_NAMES.has(pluginName))
throw new Error(`Unknown plugin name: ${pluginName}`);
}

//
// Load linter and all plugins
//
const linter = createESLintLinter();
loadPluginTypeScriptRules(linter);
loadPluginNRules(linter);
loadPluginUnicornRules(linter);
loadPluginJSDocRules(linter);
loadPluginImportRules(linter);
loadPluginJestRules(linter);
// TODO: more plugins

//
// Generate entry and update status
//
const ruleEntries = createRuleEntries(linter.getRules());
const implementedRuleNames = await readAllImplementedRuleNames();
updateImplementedStatus(ruleEntries, implementedRuleNames);
updateNotSupportedStatus(ruleEntries);

//
// Render list and update if necessary
//
await Promise.allSettled(
Array.from(targetPluginNames).map(async (pluginName) => {
const listPart = renderRulesList(ruleEntries, pluginName);
const content = renderLayout(listPart, pluginName);

if (!values.update) return console.log(content);
// TODO: Update issue
}),
);
})();
54 changes: 54 additions & 0 deletions tasks/lint_rules2/src/output-markdown.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @param {import("./oxlint-rules.cjs").RuleEntries} ruleEntries
* @param {string} pluginName
*/
exports.renderRulesList = (ruleEntries, pluginName) => {
/* prettier-ignore */
const list = [
"| Name | Kind | Status | Docs |",
"| :--- | :--: | :----: | :--- |",
];

for (const [name, entry] of ruleEntries) {
if (!name.startsWith(`${pluginName}/`)) continue;

// These should be exclusive, but show it for sure...
let kind = "";
if (entry.isRecommended) kind += "🍀";
if (entry.isDeprecated) kind += "⚠️";

let status = "";
if (entry.isImplemented) status += "✨";
if (entry.isNotSupported) status += "🚫";

list.push(`| ${name} | ${kind} | ${status} | ${entry.docsUrl} |`);
}

return `
- Kind: 🍀 = recommended | ⚠️ = deprecated
- Status: ✨ = implemented | 🚫 = not supported
${list.join("\n")}
`;
};

/**
* @param {string} listPart
* @param {string} pluginName
*/
exports.renderLayout = (listPart, pluginName) => `
> [!WARNING]
> This comment is maintained by CI. Do not edit this comment directly.
> To update comment template, see https://github.com/oxc-project/oxc/tree/main/tasks/lint_rules
## Rules
${listPart}
## Getting started
\`\`\`sh
just new-${pluginName}-rule <RULE_NAME>
\`\`\`
Then register the rule in \`crates/oxc_linter/src/rules.rs\` and also \`declare_all_lint_rules\` at the bottom.
`;
Loading

0 comments on commit 80a4546

Please sign in to comment.