Skip to content

Commit b9f51c9

Browse files
committed
feat(plugin-eslint): rule options used to identify audit, options in slug (hash) and description
1 parent a546d68 commit b9f51c9

File tree

4 files changed

+187
-117
lines changed

4 files changed

+187
-117
lines changed

packages/plugin-eslint/src/lib/__snapshots__/eslint-plugin.spec.ts.snap

+90-90
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,6 @@
33
exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
44
{
55
"audits": [
6-
{
7-
"description": "ESLint rule **arrow-body-style**.",
8-
"docsUrl": "https://eslint.org/docs/latest/rules/arrow-body-style",
9-
"slug": "arrow-body-style",
10-
"title": "Require braces around arrow function bodies",
11-
},
12-
{
13-
"description": "ESLint rule **camelcase**.",
14-
"docsUrl": "https://eslint.org/docs/latest/rules/camelcase",
15-
"slug": "camelcase",
16-
"title": "Enforce camelcase naming convention",
17-
},
18-
{
19-
"description": "ESLint rule **curly**.",
20-
"docsUrl": "https://eslint.org/docs/latest/rules/curly",
21-
"slug": "curly",
22-
"title": "Enforce consistent brace style for all control statements",
23-
},
24-
{
25-
"description": "ESLint rule **eqeqeq**.",
26-
"docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq",
27-
"slug": "eqeqeq",
28-
"title": "Require the use of \`===\` and \`!==\`",
29-
},
30-
{
31-
"description": "ESLint rule **max-lines**.",
32-
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines",
33-
"slug": "max-lines",
34-
"title": "Enforce a maximum number of lines per file",
35-
},
36-
{
37-
"description": "ESLint rule **max-lines-per-function**.",
38-
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines-per-function",
39-
"slug": "max-lines-per-function",
40-
"title": "Enforce a maximum number of lines of code in a function",
41-
},
426
{
437
"description": "ESLint rule **no-cond-assign**.",
448
"docsUrl": "https://eslint.org/docs/latest/rules/no-cond-assign",
@@ -63,12 +27,6 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
6327
"slug": "no-invalid-regexp",
6428
"title": "Disallow invalid regular expression strings in \`RegExp\` constructors",
6529
},
66-
{
67-
"description": "ESLint rule **no-shadow**.",
68-
"docsUrl": "https://eslint.org/docs/latest/rules/no-shadow",
69-
"slug": "no-shadow",
70-
"title": "Disallow variable declarations from shadowing variables declared in the outer scope",
71-
},
7230
{
7331
"description": "ESLint rule **no-undef**.",
7432
"docsUrl": "https://eslint.org/docs/latest/rules/no-undef",
@@ -99,6 +57,60 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
9957
"slug": "no-unused-vars",
10058
"title": "Disallow unused variables",
10159
},
60+
{
61+
"description": "ESLint rule **use-isnan**.",
62+
"docsUrl": "https://eslint.org/docs/latest/rules/use-isnan",
63+
"slug": "use-isnan",
64+
"title": "Require calls to \`isNaN()\` when checking for \`NaN\`",
65+
},
66+
{
67+
"description": "ESLint rule **valid-typeof**.",
68+
"docsUrl": "https://eslint.org/docs/latest/rules/valid-typeof",
69+
"slug": "valid-typeof",
70+
"title": "Enforce comparing \`typeof\` expressions against valid strings",
71+
},
72+
{
73+
"description": "ESLint rule **arrow-body-style**.",
74+
"docsUrl": "https://eslint.org/docs/latest/rules/arrow-body-style",
75+
"slug": "arrow-body-style",
76+
"title": "Require braces around arrow function bodies",
77+
},
78+
{
79+
"description": "ESLint rule **camelcase**.",
80+
"docsUrl": "https://eslint.org/docs/latest/rules/camelcase",
81+
"slug": "camelcase",
82+
"title": "Enforce camelcase naming convention",
83+
},
84+
{
85+
"description": "ESLint rule **curly**.",
86+
"docsUrl": "https://eslint.org/docs/latest/rules/curly",
87+
"slug": "curly",
88+
"title": "Enforce consistent brace style for all control statements",
89+
},
90+
{
91+
"description": "ESLint rule **eqeqeq**.",
92+
"docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq",
93+
"slug": "eqeqeq",
94+
"title": "Require the use of \`===\` and \`!==\`",
95+
},
96+
{
97+
"description": "ESLint rule **max-lines-per-function**.",
98+
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines-per-function",
99+
"slug": "max-lines-per-function",
100+
"title": "Enforce a maximum number of lines of code in a function",
101+
},
102+
{
103+
"description": "ESLint rule **max-lines**.",
104+
"docsUrl": "https://eslint.org/docs/latest/rules/max-lines",
105+
"slug": "max-lines",
106+
"title": "Enforce a maximum number of lines per file",
107+
},
108+
{
109+
"description": "ESLint rule **no-shadow**.",
110+
"docsUrl": "https://eslint.org/docs/latest/rules/no-shadow",
111+
"slug": "no-shadow",
112+
"title": "Disallow variable declarations from shadowing variables declared in the outer scope",
113+
},
102114
{
103115
"description": "ESLint rule **no-var**.",
104116
"docsUrl": "https://eslint.org/docs/latest/rules/no-var",
@@ -129,36 +141,48 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
129141
"slug": "prefer-object-spread",
130142
"title": "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",
131143
},
132-
{
133-
"description": "ESLint rule **use-isnan**.",
134-
"docsUrl": "https://eslint.org/docs/latest/rules/use-isnan",
135-
"slug": "use-isnan",
136-
"title": "Require calls to \`isNaN()\` when checking for \`NaN\`",
137-
},
138-
{
139-
"description": "ESLint rule **valid-typeof**.",
140-
"docsUrl": "https://eslint.org/docs/latest/rules/valid-typeof",
141-
"slug": "valid-typeof",
142-
"title": "Enforce comparing \`typeof\` expressions against valid strings",
143-
},
144144
{
145145
"description": "ESLint rule **yoda**.",
146146
"docsUrl": "https://eslint.org/docs/latest/rules/yoda",
147147
"slug": "yoda",
148148
"title": "Require or disallow \\"Yoda\\" conditions",
149149
},
150-
{
151-
"description": "ESLint rule **display-name**, from _react_ plugin.",
152-
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/display-name.md",
153-
"slug": "react-display-name",
154-
"title": "Disallow missing displayName in a React component definition",
155-
},
156150
{
157151
"description": "ESLint rule **jsx-key**, from _react_ plugin.",
158152
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/jsx-key.md",
159153
"slug": "react-jsx-key",
160154
"title": "Disallow missing \`key\` props in iterators/collection literals",
161155
},
156+
{
157+
"description": "ESLint rule **prop-types**, from _react_ plugin.",
158+
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/prop-types.md",
159+
"slug": "react-prop-types",
160+
"title": "Disallow missing props validation in a React component definition",
161+
},
162+
{
163+
"description": "ESLint rule **react-in-jsx-scope**, from _react_ plugin.",
164+
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/react-in-jsx-scope.md",
165+
"slug": "react-react-in-jsx-scope",
166+
"title": "Disallow missing React when using JSX",
167+
},
168+
{
169+
"description": "ESLint rule **rules-of-hooks**, from _react-hooks_ plugin.",
170+
"docsUrl": "https://reactjs.org/docs/hooks-rules.html",
171+
"slug": "react-hooks-rules-of-hooks",
172+
"title": "enforces the Rules of Hooks",
173+
},
174+
{
175+
"description": "ESLint rule **exhaustive-deps**, from _react-hooks_ plugin.",
176+
"docsUrl": "https://github.com/facebook/react/issues/14920",
177+
"slug": "react-hooks-exhaustive-deps",
178+
"title": "verifies the list of dependencies for Hooks like useEffect and similar",
179+
},
180+
{
181+
"description": "ESLint rule **display-name**, from _react_ plugin.",
182+
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/display-name.md",
183+
"slug": "react-display-name",
184+
"title": "Disallow missing displayName in a React component definition",
185+
},
162186
{
163187
"description": "ESLint rule **jsx-no-comment-textnodes**, from _react_ plugin.",
164188
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/jsx-no-comment-textnodes.md",
@@ -231,18 +255,18 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
231255
"slug": "react-no-is-mounted",
232256
"title": "Disallow usage of isMounted",
233257
},
234-
{
235-
"description": "ESLint rule **no-string-refs**, from _react_ plugin.",
236-
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-string-refs.md",
237-
"slug": "react-no-string-refs",
238-
"title": "Disallow using string references",
239-
},
240258
{
241259
"description": "ESLint rule **no-render-return-value**, from _react_ plugin.",
242260
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-render-return-value.md",
243261
"slug": "react-no-render-return-value",
244262
"title": "Disallow usage of the return value of ReactDOM.render",
245263
},
264+
{
265+
"description": "ESLint rule **no-string-refs**, from _react_ plugin.",
266+
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-string-refs.md",
267+
"slug": "react-no-string-refs",
268+
"title": "Disallow using string references",
269+
},
246270
{
247271
"description": "ESLint rule **no-unescaped-entities**, from _react_ plugin.",
248272
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/no-unescaped-entities.md",
@@ -261,36 +285,12 @@ exports[`eslintPlugin > should initialize ESLint plugin 1`] = `
261285
"slug": "react-no-unsafe",
262286
"title": "Disallow usage of unsafe lifecycle methods",
263287
},
264-
{
265-
"description": "ESLint rule **prop-types**, from _react_ plugin.",
266-
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/prop-types.md",
267-
"slug": "react-prop-types",
268-
"title": "Disallow missing props validation in a React component definition",
269-
},
270-
{
271-
"description": "ESLint rule **react-in-jsx-scope**, from _react_ plugin.",
272-
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/react-in-jsx-scope.md",
273-
"slug": "react-react-in-jsx-scope",
274-
"title": "Disallow missing React when using JSX",
275-
},
276288
{
277289
"description": "ESLint rule **require-render-return**, from _react_ plugin.",
278290
"docsUrl": "https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/require-render-return.md",
279291
"slug": "react-require-render-return",
280292
"title": "Enforce ES5 or ES6 class for returning value in render function",
281293
},
282-
{
283-
"description": "ESLint rule **rules-of-hooks**, from _react-hooks_ plugin.",
284-
"docsUrl": "https://reactjs.org/docs/hooks-rules.html",
285-
"slug": "react-hooks-rules-of-hooks",
286-
"title": "enforces the Rules of Hooks",
287-
},
288-
{
289-
"description": "ESLint rule **exhaustive-deps**, from _react-hooks_ plugin.",
290-
"docsUrl": "https://github.com/facebook/react/issues/14920",
291-
"slug": "react-hooks-exhaustive-deps",
292-
"title": "verifies the list of dependencies for Hooks like useEffect and similar",
293-
},
294294
],
295295
"description": "Official Code PushUp ESLint plugin",
296296
"icon": "eslint",
+17-27
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,33 @@
11
import type { Audit } from '@quality-metrics/models';
2-
import { distinct, slugify, toArray } from '@quality-metrics/utils';
3-
import type { ESLint, Linter, Rule } from 'eslint';
2+
import type { ESLint } from 'eslint';
3+
import { ruleIdToSlug } from './hash';
4+
import { RuleData, listRules } from './rules';
45

56
export async function listAudits(
67
eslint: ESLint,
78
patterns: string | string[],
89
): Promise<Audit[]> {
9-
const configs = await toArray(patterns).reduce(
10-
async (acc, pattern) => [
11-
...(await acc),
12-
await eslint.calculateConfigForFile(pattern),
13-
],
14-
Promise.resolve<Linter.Config[]>([]),
15-
);
16-
17-
const rulesIds = distinct(
18-
configs.flatMap(config => Object.keys(config.rules ?? {})),
19-
);
20-
const rulesMeta = eslint.getRulesMetaForResults([
21-
{
22-
messages: rulesIds.map(ruleId => ({ ruleId })),
23-
suppressedMessages: [] as Linter.SuppressedLintMessage[],
24-
} as ESLint.LintResult,
25-
]);
26-
27-
return Object.entries(rulesMeta).map(args => ruleToAudit(...args));
10+
const rules = await listRules(eslint, patterns);
11+
return rules.map(ruleToAudit);
2812
}
2913

30-
function ruleToAudit(ruleId: string, meta: Rule.RuleMetaData): Audit {
14+
export function ruleToAudit({ ruleId, meta, options }: RuleData): Audit {
3115
const name = ruleId.split('/').at(-1) ?? ruleId;
3216
const plugin =
3317
name === ruleId ? null : ruleId.slice(0, ruleId.lastIndexOf('/'));
34-
// TODO: add custom options hash to slug, copy to description
18+
19+
const lines: string[] = [
20+
`ESLint rule **${name}**${plugin ? `, from _${plugin}_ plugin` : ''}.`,
21+
...(options?.length ? ['Custom options:'] : []),
22+
...(options?.map(option =>
23+
['```json', JSON.stringify(option, null, 2), '```'].join('\n'),
24+
) ?? []),
25+
];
26+
3527
return {
36-
slug: slugify(ruleId),
28+
slug: ruleIdToSlug(ruleId, options),
3729
title: meta.docs?.description ?? name,
38-
description: `ESLint rule **${name}**${
39-
plugin ? `, from _${plugin}_ plugin` : ''
40-
}.`,
30+
description: lines.join('\n\n'),
4131
docsUrl: meta.docs?.url,
4232
};
4333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { slugify } from '@quality-metrics/utils';
2+
import { createHash } from 'crypto';
3+
4+
export function ruleIdToSlug(
5+
ruleId: string,
6+
options: unknown[] | undefined,
7+
): string {
8+
const slug = slugify(ruleId);
9+
if (!options?.length) {
10+
return slug;
11+
}
12+
return `${slug}-${jsonHash(options)}`;
13+
}
14+
15+
export function jsonHash(data: unknown, bytes = 8): string {
16+
return createHash('shake256', { outputLength: bytes })
17+
.update(JSON.stringify(data))
18+
.digest('hex');
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { distinct, toArray } from '@quality-metrics/utils';
2+
import type { ESLint, Linter, Rule } from 'eslint';
3+
import { jsonHash } from './hash';
4+
5+
export type RuleData = {
6+
ruleId: string;
7+
meta: Rule.RuleMetaData;
8+
options: unknown[] | undefined;
9+
};
10+
11+
export async function listRules(
12+
eslint: ESLint,
13+
patterns: string | string[],
14+
): Promise<RuleData[]> {
15+
const configs = await toArray(patterns).reduce(
16+
async (acc, pattern) => [
17+
...(await acc),
18+
await eslint.calculateConfigForFile(pattern),
19+
],
20+
Promise.resolve<Linter.Config[]>([]),
21+
);
22+
23+
const rulesIds = distinct(
24+
configs.flatMap(config => Object.keys(config.rules ?? {})),
25+
);
26+
const rulesMeta = eslint.getRulesMetaForResults([
27+
{
28+
messages: rulesIds.map(ruleId => ({ ruleId })),
29+
suppressedMessages: [] as Linter.SuppressedLintMessage[],
30+
} as ESLint.LintResult,
31+
]);
32+
33+
const rulesMap = configs
34+
.flatMap(config => Object.entries(config.rules ?? {}))
35+
.reduce<Record<string, Record<string, RuleData>>>(
36+
(acc, [ruleId, ruleEntry]) => {
37+
const meta = rulesMeta[ruleId];
38+
if (!meta) {
39+
console.warn(`Metadata not found for ESLint rule ${ruleId}`);
40+
return acc;
41+
}
42+
const options = toArray(ruleEntry).slice(1);
43+
const optionsHash = jsonHash(options);
44+
const ruleData: RuleData = {
45+
ruleId,
46+
meta,
47+
options,
48+
};
49+
return {
50+
...acc,
51+
[ruleId]: {
52+
...acc[ruleId],
53+
[optionsHash]: ruleData,
54+
},
55+
};
56+
},
57+
{},
58+
);
59+
60+
return Object.values(rulesMap).flatMap(Object.values);
61+
}

0 commit comments

Comments
 (0)