Skip to content

Commit 0350e49

Browse files
committed
feat(plugin-eslint): create groups from rules' meta.type (problem/suggestion/layout)
1 parent 346596d commit 0350e49

File tree

9 files changed

+541
-14
lines changed

9 files changed

+541
-14
lines changed

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

+425
Large diffs are not rendered by default.

packages/plugin-eslint/src/lib/eslint-plugin.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
44
import { PluginConfig } from '@code-pushup/models';
55
import { name, version } from '../../package.json';
66
import { ESLintPluginConfig, eslintPluginConfigSchema } from './config';
7-
import { listAudits } from './meta';
7+
import { listAuditsAndGroups } from './meta';
88
import { createRunnerConfig } from './runner';
99

1010
/**
@@ -37,7 +37,7 @@ export async function eslintPlugin(
3737
baseConfig: { extends: eslintrc },
3838
});
3939

40-
const audits = await listAudits(eslint, patterns);
40+
const { audits, groups } = await listAuditsAndGroups(eslint, patterns);
4141

4242
const runnerScriptPath = join(
4343
fileURLToPath(dirname(import.meta.url)),
@@ -54,10 +54,7 @@ export async function eslintPlugin(
5454
version,
5555

5656
audits,
57-
58-
// TODO: groups?
59-
// - could be `problem`/`suggestion`/`layout` if based on `meta.type`
60-
// - `meta.category` (deprecated, but still used by some) could also be a source of groups
57+
groups,
6158

6259
runner: createRunnerConfig(runnerScriptPath, audits, eslintrc, patterns),
6360
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Rule } from 'eslint';
2+
import type { AuditGroup, AuditGroupRef } from '@code-pushup/models';
3+
import { objectToKeys } from '@code-pushup/utils';
4+
import { ruleIdToSlug } from './hash';
5+
import type { RuleData } from './rules';
6+
7+
type RuleType = NonNullable<Rule.RuleMetaData['type']>;
8+
9+
// docs on meta.type: https://eslint.org/docs/latest/extend/custom-rules#rule-structure
10+
const typeGroups: Record<RuleType, Omit<AuditGroup, 'refs'>> = {
11+
problem: {
12+
slug: 'problems',
13+
title: 'Problems',
14+
description:
15+
'Code that either will cause an error or may cause confusing behavior. Developers should consider this a high priority to resolve.',
16+
},
17+
suggestion: {
18+
slug: 'suggestions',
19+
title: 'Suggestions',
20+
description:
21+
"Something that could be done in a better way but no errors will occur if the code isn't changed.",
22+
},
23+
layout: {
24+
slug: 'formatting',
25+
title: 'Formatting',
26+
description:
27+
'Primarily about whitespace, semicolons, commas, and parentheses, all the parts of the program that determine how the code looks rather than how it executes.',
28+
},
29+
};
30+
31+
export function groupsFromRuleTypes(rules: RuleData[]): AuditGroup[] {
32+
const allTypes = objectToKeys(typeGroups);
33+
34+
const auditSlugsMap = rules.reduce<Partial<Record<RuleType, string[]>>>(
35+
(acc, { meta: { type }, ruleId, options }) =>
36+
type == null
37+
? acc
38+
: {
39+
...acc,
40+
[type]: [...(acc[type] ?? []), ruleIdToSlug(ruleId, options)],
41+
},
42+
{},
43+
);
44+
45+
return allTypes.map(type => ({
46+
...typeGroups[type],
47+
refs:
48+
auditSlugsMap[type]?.map(
49+
(slug): AuditGroupRef => ({ slug, weight: 1 }),
50+
) ?? [],
51+
}));
52+
}
+10-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import type { ESLint } from 'eslint';
2-
import type { Audit } from '@code-pushup/models';
2+
import type { Audit, AuditGroup } from '@code-pushup/models';
3+
import { groupsFromRuleTypes } from './groups';
34
import { listRules } from './rules';
45
import { ruleToAudit } from './transform';
56

6-
export async function listAudits(
7+
export async function listAuditsAndGroups(
78
eslint: ESLint,
89
patterns: string | string[],
9-
): Promise<Audit[]> {
10+
): Promise<{ audits: Audit[]; groups: AuditGroup[] }> {
1011
const rules = await listRules(eslint, patterns);
11-
return rules.map(ruleToAudit);
12+
13+
const audits = rules.map(ruleToAudit);
14+
15+
const groups = groupsFromRuleTypes(rules);
16+
17+
return { audits, groups };
1218
}

packages/plugin-eslint/src/lib/meta/rules.spec.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ESLint } from 'eslint';
22
import { dirname, join } from 'path';
33
import { fileURLToPath } from 'url';
44
import type { SpyInstance } from 'vitest';
5-
import { RuleData, listRules } from './rules';
5+
import { RuleData, listRules, parseRuleId } from './rules';
66

77
describe('listRules', () => {
88
const fixturesDir = join(
@@ -244,3 +244,34 @@ describe('listRules', () => {
244244
});
245245
});
246246
});
247+
248+
describe('parseRuleId', () => {
249+
it.each([
250+
{
251+
ruleId: 'prefer-const',
252+
name: 'prefer-const',
253+
},
254+
{
255+
ruleId: 'sonarjs/no-identical-functions',
256+
plugin: 'sonarjs',
257+
name: 'no-identical-functions',
258+
},
259+
{
260+
ruleId: '@typescript-eslint/no-non-null-assertion',
261+
plugin: '@typescript-eslint',
262+
name: 'no-non-null-assertion',
263+
},
264+
{
265+
ruleId: 'no-secrets/no-secrets',
266+
plugin: 'no-secrets',
267+
name: 'no-secrets',
268+
},
269+
{
270+
ruleId: '@angular-eslint/template/no-negated-async',
271+
plugin: '@angular-eslint/template',
272+
name: 'no-negated-async',
273+
},
274+
])('$ruleId => name: $name, plugin: $plugin', ({ ruleId, name, plugin }) => {
275+
expect(parseRuleId(ruleId)).toEqual({ name, plugin });
276+
});
277+
});

packages/plugin-eslint/src/lib/meta/rules.ts

+11
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,14 @@ function isRuleOff(entry: Linter.RuleEntry<unknown[]>): boolean {
7575
return false;
7676
}
7777
}
78+
79+
export function parseRuleId(ruleId: string): { plugin?: string; name: string } {
80+
const i = ruleId.lastIndexOf('/');
81+
if (i < 0) {
82+
return { name: ruleId };
83+
}
84+
return {
85+
plugin: ruleId.slice(0, i),
86+
name: ruleId.slice(i + 1),
87+
};
88+
}

packages/plugin-eslint/src/lib/runner.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { dirname, join } from 'path';
44
import { fileURLToPath } from 'url';
55
import type { SpyInstance } from 'vitest';
66
import { readJsonFile } from '@code-pushup/utils';
7-
import { listAudits } from './meta';
7+
import { listAuditsAndGroups } from './meta';
88
import {
99
RUNNER_OUTPUT_PATH,
1010
createRunnerConfig,
@@ -36,7 +36,7 @@ describe('executeRunner', () => {
3636
useEslintrc: false,
3737
baseConfig: { extends: eslintrc },
3838
});
39-
const audits = await listAudits(eslint, patterns);
39+
const { audits } = await listAuditsAndGroups(eslint, patterns);
4040

4141
const runnerConfig = createRunnerConfig(
4242
'bin.js',

packages/utils/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export {
77
executeProcess,
88
objectToCliArgs,
99
} from './lib/execute-process';
10-
export { getProgressBar, ProgressBar } from './lib/progress';
1110
export { git, latestHash } from './lib/git';
1211
export { importEsmModule } from './lib/load-file';
12+
export { ProgressBar, getProgressBar } from './lib/progress';
1313
export {
1414
CODE_PUSHUP_DOMAIN,
1515
FOOTER_PREFIX,
@@ -27,6 +27,7 @@ export {
2727
countOccurrences,
2828
distinct,
2929
objectToEntries,
30+
objectToKeys,
3031
pluralize,
3132
readJsonFile,
3233
readTextFile,

packages/utils/src/lib/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export function toArray<T>(val: T | T[]): T[] {
4747
return Array.isArray(val) ? val : [val];
4848
}
4949

50+
export function objectToKeys<T extends object>(obj: T) {
51+
return Object.keys(obj) as (keyof T)[];
52+
}
53+
5054
export function objectToEntries<T extends object>(obj: T) {
5155
return Object.entries(obj) as [keyof T, T[keyof T]][];
5256
}

0 commit comments

Comments
 (0)