Skip to content

Commit 1e29603

Browse files
committed
feat(cli): enhance category filtering and validation logic
1 parent 884f53c commit 1e29603

File tree

4 files changed

+145
-155
lines changed

4 files changed

+145
-155
lines changed

packages/cli/src/lib/implementation/filter.middleware.ts

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,33 @@ export function filterMiddleware<T extends FilterOptions>(
2727
verbose = false,
2828
} = originalProcessArgs;
2929

30+
const plugins = processPlugins(rcPlugins);
31+
const categories = filterSkippedCategories(rcCategories, plugins);
32+
33+
if (rcCategories && categories) {
34+
validateFilteredCategories(rcCategories, categories, {
35+
onlyCategories,
36+
skipCategories,
37+
verbose,
38+
});
39+
}
40+
3041
if (
3142
skipCategories.length === 0 &&
3243
onlyCategories.length === 0 &&
3344
skipPlugins.length === 0 &&
3445
onlyPlugins.length === 0
3546
) {
36-
return originalProcessArgs;
47+
return {
48+
...originalProcessArgs,
49+
...(categories && { categories }),
50+
plugins,
51+
};
3752
}
3853

3954
handleConflictingOptions('categories', onlyCategories, skipCategories);
4055
handleConflictingOptions('plugins', onlyPlugins, skipPlugins);
4156

42-
const plugins = processPlugins(rcPlugins);
43-
const categories = filterSkippedCategories(rcCategories, plugins);
44-
45-
if (rcCategories && categories && verbose) {
46-
validateFilteredCategories(rcCategories, categories);
47-
}
48-
4957
const filteredCategories = applyCategoryFilters(
5058
{ categories, plugins },
5159
skipCategories,
@@ -158,44 +166,33 @@ function filterPluginsFromCategories({
158166

159167
function filterSkippedItems<T extends { isSkipped?: boolean }>(
160168
items: T[] | undefined,
161-
): T[] {
162-
return (items ?? []).filter(({ isSkipped }) => isSkipped !== true);
163-
}
164-
165-
export function filterSkippedGroups(
166-
groups: PluginConfig['groups'],
167-
audits: PluginConfig['audits'],
168-
): PluginConfig['groups'] {
169-
if (!groups) {
170-
return groups;
171-
}
172-
return filterItemRefsBy(groups, ref =>
173-
audits.some(audit => audit.slug === ref.slug && audit.isSkipped !== true),
174-
);
169+
): Omit<T, 'isSkipped'>[] {
170+
return (items ?? [])
171+
.filter(({ isSkipped }) => isSkipped !== true)
172+
.map(({ isSkipped, ...props }) => props);
175173
}
176174

177175
export function processPlugins(plugins: PluginConfig[]): PluginConfig[] {
178-
return plugins.map((plugin: PluginConfig) => ({
179-
...plugin,
180-
...(plugin.groups && {
181-
groups: filterSkippedGroups(
182-
filterSkippedItems(plugin.groups),
183-
filterSkippedItems(plugin.audits),
184-
),
185-
}),
186-
audits: filterSkippedItems(plugin.audits),
187-
}));
176+
return plugins.map((plugin: PluginConfig) => {
177+
const filteredAudits = filterSkippedItems(plugin.audits);
178+
return {
179+
...plugin,
180+
...(plugin.groups && {
181+
groups: filterItemRefsBy(filterSkippedItems(plugin.groups), ref =>
182+
filteredAudits.some(({ slug }) => slug === ref.slug),
183+
),
184+
}),
185+
audits: filteredAudits,
186+
};
187+
});
188188
}
189189

190190
export function filterSkippedCategories(
191191
categories: CoreConfig['categories'],
192192
plugins: CoreConfig['plugins'],
193193
): CoreConfig['categories'] {
194-
if (!categories || categories.length === 0) {
195-
return categories;
196-
}
197194
return categories
198-
.map(category => {
195+
?.map(category => {
199196
const validRefs = category.refs.filter(ref =>
200197
isValidCategoryRef(ref, plugins),
201198
);

packages/cli/src/lib/implementation/filter.middleware.unit.test.ts

Lines changed: 10 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ui } from '@code-pushup/utils';
33
import {
44
filterMiddleware,
55
filterSkippedCategories,
6-
filterSkippedGroups,
76
processPlugins,
87
} from './filter.middleware.js';
98
import { OptionValidationError } from './validate-filter-options.utils.js';
@@ -39,8 +38,8 @@ describe('filterMiddleware', () => {
3938
{
4039
slug: 'c1',
4140
refs: [
42-
{ plugin: 'p1', slug: 'a1-p1' },
43-
{ plugin: 'p2', slug: 'a1-p2' },
41+
{ type: 'audit', plugin: 'p1', slug: 'a1-p1' },
42+
{ type: 'audit', plugin: 'p2', slug: 'a1-p2' },
4443
],
4544
},
4645
] as CategoryConfig[];
@@ -63,8 +62,8 @@ describe('filterMiddleware', () => {
6362
{
6463
slug: 'c1',
6564
refs: [
66-
{ plugin: 'p1', slug: 'a1-p1' },
67-
{ plugin: 'p2', slug: 'a1-p2' },
65+
{ type: 'audit', plugin: 'p1', slug: 'a1-p1' },
66+
{ type: 'audit', plugin: 'p2', slug: 'a1-p2' },
6867
],
6968
},
7069
] as CategoryConfig[];
@@ -367,63 +366,6 @@ describe('filterMiddleware', () => {
367366
});
368367
});
369368

370-
describe('filterSkippedGroups', () => {
371-
it('should return original input when groups are undefined', () => {
372-
expect(
373-
filterSkippedGroups(undefined, [
374-
{ slug: 'a1', isSkipped: false },
375-
{ slug: 'a2', isSkipped: true },
376-
] as PluginConfig['audits']),
377-
).toBeUndefined();
378-
});
379-
380-
it('should filter out refs for skipped audits', () => {
381-
expect(
382-
filterSkippedGroups(
383-
[
384-
{
385-
slug: 'g1',
386-
refs: [
387-
{ slug: 'a1', weight: 1 },
388-
{ slug: 'a2', weight: 2 },
389-
],
390-
},
391-
] as PluginConfig['groups'],
392-
[
393-
{ slug: 'a1', isSkipped: false },
394-
{ slug: 'a2', isSkipped: true },
395-
] as PluginConfig['audits'],
396-
),
397-
).toEqual([{ slug: 'g1', refs: [{ slug: 'a1', weight: 1 }] }]);
398-
});
399-
400-
it('should return empty groups when all refs are skipped', () => {
401-
expect(
402-
filterSkippedGroups(
403-
[
404-
{
405-
slug: 'g1',
406-
refs: [{ slug: 'a1', weight: 1 }],
407-
},
408-
] as PluginConfig['groups'],
409-
[{ slug: 'a1', isSkipped: true }] as PluginConfig['audits'],
410-
),
411-
).toEqual([]);
412-
});
413-
414-
it('should return the same groups when no refs are skipped', () => {
415-
const groups = [
416-
{
417-
slug: 'g1',
418-
refs: [{ slug: 'a1', weight: 1 }],
419-
},
420-
] as PluginConfig['groups'];
421-
const audits = [{ slug: 'a1', isSkipped: false }] as PluginConfig['audits'];
422-
423-
expect(filterSkippedGroups(groups, audits)).toEqual(groups);
424-
});
425-
});
426-
427369
describe('processPlugins', () => {
428370
it('should filter out skipped audits and groups', () => {
429371
expect(
@@ -449,12 +391,11 @@ describe('processPlugins', () => {
449391
).toEqual([
450392
{
451393
slug: 'p1',
452-
audits: [{ slug: 'a1', isSkipped: false }],
394+
audits: [{ slug: 'a1' }],
453395
groups: [
454396
{
455397
slug: 'g1',
456398
refs: [{ slug: 'a1', weight: 1 }],
457-
isSkipped: false,
458399
},
459400
],
460401
},
@@ -479,7 +420,7 @@ describe('processPlugins', () => {
479420
).toEqual([
480421
{
481422
slug: 'p1',
482-
audits: [{ slug: 'a1', isSkipped: false }],
423+
audits: [{ slug: 'a1' }],
483424
groups: [],
484425
},
485426
]);
@@ -499,20 +440,7 @@ describe('filterSkippedCategories', () => {
499440
[
500441
{
501442
slug: 'p1',
502-
audits: [
503-
{ slug: 'a1', isSkipped: false },
504-
{ slug: 'a2', isSkipped: false },
505-
],
506-
groups: [
507-
{
508-
slug: 'g1',
509-
refs: [
510-
{ slug: 'a1', weight: 1 },
511-
{ slug: 'a2', weight: 1 },
512-
],
513-
isSkipped: true,
514-
},
515-
],
443+
audits: [{ slug: 'a1' }, { slug: 'a2' }],
516444
},
517445
] as PluginConfig[],
518446
),
@@ -535,35 +463,19 @@ describe('filterSkippedCategories', () => {
535463
[
536464
{
537465
slug: 'p1',
538-
audits: [
539-
{ slug: 'a1-p1', isSkipped: false },
540-
{ slug: 'a2-p1', isSkipped: false },
541-
],
542-
groups: [
543-
{
544-
slug: 'g1-p1',
545-
refs: [
546-
{ slug: 'a1-p1', weight: 1 },
547-
{ slug: 'a2-p1', weight: 1 },
548-
],
549-
isSkipped: true,
550-
},
551-
],
466+
audits: [{ slug: 'a1-p1' }, { slug: 'a2-p1' }],
467+
groups: [],
552468
},
553469
{
554470
slug: 'p2',
555-
audits: [
556-
{ slug: 'a1-p2', isSkipped: false },
557-
{ slug: 'a2-p2', isSkipped: false },
558-
],
471+
audits: [{ slug: 'a1-p2' }, { slug: 'a2-p2' }],
559472
groups: [
560473
{
561474
slug: 'g1-p2',
562475
refs: [
563476
{ slug: 'a1-p2', weight: 1 },
564477
{ slug: 'a2-p2', weight: 1 },
565478
],
566-
isSkipped: false,
567479
},
568480
],
569481
},

packages/cli/src/lib/implementation/validate-filter-options.utils.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
pluralize,
1010
ui,
1111
} from '@code-pushup/utils';
12-
import type { FilterOptionType, Filterables } from './filter.model.js';
12+
import type {
13+
FilterOptionType,
14+
FilterOptions,
15+
Filterables,
16+
} from './filter.model.js';
1317

1418
export class OptionValidationError extends Error {}
1519

@@ -62,40 +66,62 @@ export function validateFilterOption(
6266
export function validateFilteredCategories(
6367
originalCategories: NonNullable<Filterables['categories']>,
6468
filteredCategories: NonNullable<Filterables['categories']>,
69+
{
70+
onlyCategories,
71+
skipCategories,
72+
verbose,
73+
}: Pick<FilterOptions, 'onlyCategories' | 'skipCategories' | 'verbose'>,
6574
): void {
66-
originalCategories
67-
.filter(
68-
original =>
69-
!filteredCategories.some(filtered => filtered.slug === original.slug),
70-
)
71-
.forEach(category => {
75+
const skippedCategories = originalCategories.filter(
76+
original => !filteredCategories.some(({ slug }) => slug === original.slug),
77+
);
78+
if (verbose) {
79+
skippedCategories.forEach(category => {
7280
ui().logger.info(
7381
`Category ${category.slug} was removed because all its refs were skipped. Affected refs: ${category.refs
7482
.map(ref => `${ref.slug} (${ref.type})`)
7583
.join(', ')}`,
7684
);
7785
});
86+
}
87+
const invalidArgs = [
88+
{ option: 'onlyCategories', args: onlyCategories ?? [] },
89+
{ option: 'skipCategories', args: skipCategories ?? [] },
90+
].filter(({ args }) =>
91+
args.some(arg => skippedCategories.some(({ slug }) => slug === arg)),
92+
);
93+
if (invalidArgs.length > 0) {
94+
throw new OptionValidationError(
95+
invalidArgs
96+
.map(
97+
({ option, args }) =>
98+
`The --${option} argument references skipped categories: ${args.join(', ')}`,
99+
)
100+
.join('. '),
101+
);
102+
}
103+
if (filteredCategories.length === 0) {
104+
throw new OptionValidationError(
105+
`No categories remain after filtering. Removed categories: ${skippedCategories
106+
.map(({ slug }) => slug)
107+
.join(', ')}`,
108+
);
109+
}
78110
}
79111

80112
export function isValidCategoryRef(
81113
ref: CategoryRef,
82114
plugins: Filterables['plugins'],
83115
): boolean {
84-
const plugin = plugins.find(p => p.slug === ref.plugin);
116+
const plugin = plugins.find(({ slug }) => slug === ref.plugin);
85117
if (!plugin) {
86118
return false;
87119
}
88120
switch (ref.type) {
89121
case 'audit':
90-
return plugin.audits.some(
91-
audit => audit.slug === ref.slug && audit.isSkipped !== true,
92-
);
122+
return plugin.audits.some(({ slug }) => slug === ref.slug);
93123
case 'group':
94-
return (
95-
plugin.groups?.some(
96-
group => group.slug === ref.slug && group.isSkipped !== true,
97-
) ?? false
98-
);
124+
return plugin.groups?.some(({ slug }) => slug === ref.slug) ?? false;
99125
}
100126
}
101127

0 commit comments

Comments
 (0)