Skip to content

Commit

Permalink
feat(plugin-eslint): rule options used to identify audit, options in …
Browse files Browse the repository at this point in the history
…slug (hash) and description
  • Loading branch information
matejchalk committed Oct 9, 2023
1 parent a546d68 commit b9f51c9
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,6 @@
exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
{
"audits": [
{
"description": "ESLint rule **arrow-body-style**.",
"docsUrl": "https://eslint.org/docs/latest/rules/arrow-body-style",
"slug": "arrow-body-style",
"title": "Require braces around arrow function bodies",
},
{
"description": "ESLint rule **camelcase**.",
"docsUrl": "https://eslint.org/docs/latest/rules/camelcase",
"slug": "camelcase",
"title": "Enforce camelcase naming convention",
},
{
"description": "ESLint rule **curly**.",
"docsUrl": "https://eslint.org/docs/latest/rules/curly",
"slug": "curly",
"title": "Enforce consistent brace style for all control statements",
},
{
"description": "ESLint rule **eqeqeq**.",
"docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq",
"slug": "eqeqeq",
"title": "Require the use of \`===\` and \`!==\`",
},
{
"description": "ESLint rule **max-lines**.",
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines",
"slug": "max-lines",
"title": "Enforce a maximum number of lines per file",
},
{
"description": "ESLint rule **max-lines-per-function**.",
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines-per-function",
"slug": "max-lines-per-function",
"title": "Enforce a maximum number of lines of code in a function",
},
{
"description": "ESLint rule **no-cond-assign**.",
"docsUrl": "https://eslint.org/docs/latest/rules/no-cond-assign",
Expand All @@ -63,12 +27,6 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
"slug": "no-invalid-regexp",
"title": "Disallow invalid regular expression strings in \`RegExp\` constructors",
},
{
"description": "ESLint rule **no-shadow**.",
"docsUrl": "https://eslint.org/docs/latest/rules/no-shadow",
"slug": "no-shadow",
"title": "Disallow variable declarations from shadowing variables declared in the outer scope",
},
{
"description": "ESLint rule **no-undef**.",
"docsUrl": "https://eslint.org/docs/latest/rules/no-undef",
Expand Down Expand Up @@ -99,6 +57,60 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
"slug": "no-unused-vars",
"title": "Disallow unused variables",
},
{
"description": "ESLint rule **use-isnan**.",
"docsUrl": "https://eslint.org/docs/latest/rules/use-isnan",
"slug": "use-isnan",
"title": "Require calls to \`isNaN()\` when checking for \`NaN\`",
},
{
"description": "ESLint rule **valid-typeof**.",
"docsUrl": "https://eslint.org/docs/latest/rules/valid-typeof",
"slug": "valid-typeof",
"title": "Enforce comparing \`typeof\` expressions against valid strings",
},
{
"description": "ESLint rule **arrow-body-style**.",
"docsUrl": "https://eslint.org/docs/latest/rules/arrow-body-style",
"slug": "arrow-body-style",
"title": "Require braces around arrow function bodies",
},
{
"description": "ESLint rule **camelcase**.",
"docsUrl": "https://eslint.org/docs/latest/rules/camelcase",
"slug": "camelcase",
"title": "Enforce camelcase naming convention",
},
{
"description": "ESLint rule **curly**.",
"docsUrl": "https://eslint.org/docs/latest/rules/curly",
"slug": "curly",
"title": "Enforce consistent brace style for all control statements",
},
{
"description": "ESLint rule **eqeqeq**.",
"docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq",
"slug": "eqeqeq",
"title": "Require the use of \`===\` and \`!==\`",
},
{
"description": "ESLint rule **max-lines-per-function**.",
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines-per-function",
"slug": "max-lines-per-function",
"title": "Enforce a maximum number of lines of code in a function",
},
{
"description": "ESLint rule **max-lines**.",
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines",
"slug": "max-lines",
"title": "Enforce a maximum number of lines per file",
},
{
"description": "ESLint rule **no-shadow**.",
"docsUrl": "https://eslint.org/docs/latest/rules/no-shadow",
"slug": "no-shadow",
"title": "Disallow variable declarations from shadowing variables declared in the outer scope",
},
{
"description": "ESLint rule **no-var**.",
"docsUrl": "https://eslint.org/docs/latest/rules/no-var",
Expand Down Expand Up @@ -129,36 +141,48 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
"slug": "prefer-object-spread",
"title": "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",
},
{
"description": "ESLint rule **use-isnan**.",
"docsUrl": "https://eslint.org/docs/latest/rules/use-isnan",
"slug": "use-isnan",
"title": "Require calls to \`isNaN()\` when checking for \`NaN\`",
},
{
"description": "ESLint rule **valid-typeof**.",
"docsUrl": "https://eslint.org/docs/latest/rules/valid-typeof",
"slug": "valid-typeof",
"title": "Enforce comparing \`typeof\` expressions against valid strings",
},
{
"description": "ESLint rule **yoda**.",
"docsUrl": "https://eslint.org/docs/latest/rules/yoda",
"slug": "yoda",
"title": "Require or disallow \\"Yoda\\" conditions",
},
{
"description": "ESLint rule **display-name**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/display-name.md",
"slug": "react-display-name",
"title": "Disallow missing displayName in a React component definition",
},
{
"description": "ESLint rule **jsx-key**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/jsx-key.md",
"slug": "react-jsx-key",
"title": "Disallow missing \`key\` props in iterators/collection literals",
},
{
"description": "ESLint rule **prop-types**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/prop-types.md",
"slug": "react-prop-types",
"title": "Disallow missing props validation in a React component definition",
},
{
"description": "ESLint rule **react-in-jsx-scope**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/react-in-jsx-scope.md",
"slug": "react-react-in-jsx-scope",
"title": "Disallow missing React when using JSX",
},
{
"description": "ESLint rule **rules-of-hooks**, from _react-hooks_ plugin.",
"docsUrl": "https://reactjs.org/docs/hooks-rules.html",
"slug": "react-hooks-rules-of-hooks",
"title": "enforces the Rules of Hooks",
},
{
"description": "ESLint rule **exhaustive-deps**, from _react-hooks_ plugin.",
"docsUrl": "https://github.com/facebook/react/issues/14920",
"slug": "react-hooks-exhaustive-deps",
"title": "verifies the list of dependencies for Hooks like useEffect and similar",
},
{
"description": "ESLint rule **display-name**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/display-name.md",
"slug": "react-display-name",
"title": "Disallow missing displayName in a React component definition",
},
{
"description": "ESLint rule **jsx-no-comment-textnodes**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/jsx-no-comment-textnodes.md",
Expand Down Expand Up @@ -231,18 +255,18 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
"slug": "react-no-is-mounted",
"title": "Disallow usage of isMounted",
},
{
"description": "ESLint rule **no-string-refs**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-string-refs.md",
"slug": "react-no-string-refs",
"title": "Disallow using string references",
},
{
"description": "ESLint rule **no-render-return-value**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-render-return-value.md",
"slug": "react-no-render-return-value",
"title": "Disallow usage of the return value of ReactDOM.render",
},
{
"description": "ESLint rule **no-string-refs**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-string-refs.md",
"slug": "react-no-string-refs",
"title": "Disallow using string references",
},
{
"description": "ESLint rule **no-unescaped-entities**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-unescaped-entities.md",
Expand All @@ -261,36 +285,12 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
"slug": "react-no-unsafe",
"title": "Disallow usage of unsafe lifecycle methods",
},
{
"description": "ESLint rule **prop-types**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/prop-types.md",
"slug": "react-prop-types",
"title": "Disallow missing props validation in a React component definition",
},
{
"description": "ESLint rule **react-in-jsx-scope**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/react-in-jsx-scope.md",
"slug": "react-react-in-jsx-scope",
"title": "Disallow missing React when using JSX",
},
{
"description": "ESLint rule **require-render-return**, from _react_ plugin.",
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/require-render-return.md",
"slug": "react-require-render-return",
"title": "Enforce ES5 or ES6 class for returning value in render function",
},
{
"description": "ESLint rule **rules-of-hooks**, from _react-hooks_ plugin.",
"docsUrl": "https://reactjs.org/docs/hooks-rules.html",
"slug": "react-hooks-rules-of-hooks",
"title": "enforces the Rules of Hooks",
},
{
"description": "ESLint rule **exhaustive-deps**, from _react-hooks_ plugin.",
"docsUrl": "https://github.com/facebook/react/issues/14920",
"slug": "react-hooks-exhaustive-deps",
"title": "verifies the list of dependencies for Hooks like useEffect and similar",
},
],
"description": "Official Code PushUp ESLint plugin",
"icon": "eslint",
Expand Down
44 changes: 17 additions & 27 deletions packages/plugin-eslint/src/lib/meta/audits.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,33 @@
import type { Audit } from '@quality-metrics/models';
import { distinct, slugify, toArray } from '@quality-metrics/utils';
import type { ESLint, Linter, Rule } from 'eslint';
import type { ESLint } from 'eslint';
import { ruleIdToSlug } from './hash';
import { RuleData, listRules } from './rules';

export async function listAudits(
eslint: ESLint,
patterns: string | string[],
): Promise<Audit[]> {
const configs = await toArray(patterns).reduce(
async (acc, pattern) => [
...(await acc),
await eslint.calculateConfigForFile(pattern),
],
Promise.resolve<Linter.Config[]>([]),
);

const rulesIds = distinct(
configs.flatMap(config => Object.keys(config.rules ?? {})),
);
const rulesMeta = eslint.getRulesMetaForResults([
{
messages: rulesIds.map(ruleId => ({ ruleId })),
suppressedMessages: [] as Linter.SuppressedLintMessage[],
} as ESLint.LintResult,
]);

return Object.entries(rulesMeta).map(args => ruleToAudit(...args));
const rules = await listRules(eslint, patterns);
return rules.map(ruleToAudit);
}

function ruleToAudit(ruleId: string, meta: Rule.RuleMetaData): Audit {
export function ruleToAudit({ ruleId, meta, options }: RuleData): Audit {
const name = ruleId.split('/').at(-1) ?? ruleId;
const plugin =
name === ruleId ? null : ruleId.slice(0, ruleId.lastIndexOf('/'));
// TODO: add custom options hash to slug, copy to description

const lines: string[] = [
`ESLint rule **${name}**${plugin ? `, from _${plugin}_ plugin` : ''}.`,
...(options?.length ? ['Custom options:'] : []),
...(options?.map(option =>
['```json', JSON.stringify(option, null, 2), '```'].join('\n'),
) ?? []),
];

return {
slug: slugify(ruleId),
slug: ruleIdToSlug(ruleId, options),
title: meta.docs?.description ?? name,
description: `ESLint rule **${name}**${
plugin ? `, from _${plugin}_ plugin` : ''
}.`,
description: lines.join('\n\n'),
docsUrl: meta.docs?.url,
};
}
19 changes: 19 additions & 0 deletions packages/plugin-eslint/src/lib/meta/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { slugify } from '@quality-metrics/utils';
import { createHash } from 'crypto';

export function ruleIdToSlug(
ruleId: string,
options: unknown[] | undefined,
): string {
const slug = slugify(ruleId);
if (!options?.length) {
return slug;
}
return `${slug}-${jsonHash(options)}`;
}

export function jsonHash(data: unknown, bytes = 8): string {
return createHash('shake256', { outputLength: bytes })
.update(JSON.stringify(data))
.digest('hex');
}
61 changes: 61 additions & 0 deletions packages/plugin-eslint/src/lib/meta/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { distinct, toArray } from '@quality-metrics/utils';
import type { ESLint, Linter, Rule } from 'eslint';
import { jsonHash } from './hash';

export type RuleData = {
ruleId: string;
meta: Rule.RuleMetaData;
options: unknown[] | undefined;
};

export async function listRules(
eslint: ESLint,
patterns: string | string[],
): Promise<RuleData[]> {
const configs = await toArray(patterns).reduce(
async (acc, pattern) => [
...(await acc),
await eslint.calculateConfigForFile(pattern),
],
Promise.resolve<Linter.Config[]>([]),
);

const rulesIds = distinct(
configs.flatMap(config => Object.keys(config.rules ?? {})),
);
const rulesMeta = eslint.getRulesMetaForResults([
{
messages: rulesIds.map(ruleId => ({ ruleId })),
suppressedMessages: [] as Linter.SuppressedLintMessage[],
} as ESLint.LintResult,
]);

const rulesMap = configs
.flatMap(config => Object.entries(config.rules ?? {}))
.reduce<Record<string, Record<string, RuleData>>>(
(acc, [ruleId, ruleEntry]) => {
const meta = rulesMeta[ruleId];
if (!meta) {
console.warn(`Metadata not found for ESLint rule ${ruleId}`);
return acc;
}
const options = toArray(ruleEntry).slice(1);
const optionsHash = jsonHash(options);
const ruleData: RuleData = {
ruleId,
meta,
options,
};
return {
...acc,
[ruleId]: {
...acc[ruleId],
[optionsHash]: ruleData,
},
};
},
{},
);

return Object.values(rulesMap).flatMap(Object.values);
}

0 comments on commit b9f51c9

Please sign in to comment.