|
1 | 1 | import type { Rule } from 'eslint'; |
2 | 2 | import type { Group, GroupRef } from '@code-pushup/models'; |
3 | | -import { objectToKeys, slugify } from '@code-pushup/utils'; |
| 3 | +import { objectToKeys, slugify, ui } from '@code-pushup/utils'; |
| 4 | +import type { CustomGroup } from '../config.js'; |
4 | 5 | import { ruleToSlug } from './hash.js'; |
5 | 6 | import { type RuleData, parseRuleId } from './parse.js'; |
| 7 | +import { expandWildcardRules } from './rules.js'; |
6 | 8 |
|
7 | 9 | type RuleType = NonNullable<Rule.RuleMetaData['type']>; |
8 | 10 |
|
@@ -87,3 +89,80 @@ export function groupsFromRuleCategories(rules: RuleData[]): Group[] { |
87 | 89 |
|
88 | 90 | return groups.toSorted((a, b) => a.slug.localeCompare(b.slug)); |
89 | 91 | } |
| 92 | + |
| 93 | +export function groupsFromCustomConfig( |
| 94 | + rules: RuleData[], |
| 95 | + groups: CustomGroup[], |
| 96 | +): Group[] { |
| 97 | + const rulesMap = createRulesMap(rules); |
| 98 | + |
| 99 | + return groups.map(group => { |
| 100 | + const groupRules = Array.isArray(group.rules) |
| 101 | + ? Object.fromEntries(group.rules.map(rule => [rule, 1])) |
| 102 | + : group.rules; |
| 103 | + |
| 104 | + const { refs, invalidRules } = resolveGroupRefs(groupRules, rulesMap); |
| 105 | + |
| 106 | + if (invalidRules.length > 0 && Object.entries(groupRules).length > 0) { |
| 107 | + if (refs.length === 0) { |
| 108 | + throw new Error( |
| 109 | + `Invalid rule configuration in group ${group.slug}. All rules are invalid.`, |
| 110 | + ); |
| 111 | + } |
| 112 | + ui().logger.warning( |
| 113 | + `Some rules in group ${group.slug} are invalid: ${invalidRules.join(', ')}`, |
| 114 | + ); |
| 115 | + } |
| 116 | + |
| 117 | + return { |
| 118 | + slug: group.slug, |
| 119 | + title: group.title, |
| 120 | + refs, |
| 121 | + }; |
| 122 | + }); |
| 123 | +} |
| 124 | + |
| 125 | +export function createRulesMap(rules: RuleData[]): Record<string, RuleData[]> { |
| 126 | + return rules.reduce<Record<string, RuleData[]>>( |
| 127 | + (acc, rule) => ({ |
| 128 | + ...acc, |
| 129 | + [rule.id]: [...(acc[rule.id] || []), rule], |
| 130 | + }), |
| 131 | + {}, |
| 132 | + ); |
| 133 | +} |
| 134 | + |
| 135 | +export function resolveGroupRefs( |
| 136 | + groupRules: Record<string, number>, |
| 137 | + rulesMap: Record<string, RuleData[]>, |
| 138 | +): { refs: Group['refs']; invalidRules: string[] } { |
| 139 | + return Object.entries(groupRules).reduce<{ |
| 140 | + refs: Group['refs']; |
| 141 | + invalidRules: string[]; |
| 142 | + }>( |
| 143 | + (acc, [rule, weight]) => { |
| 144 | + const matchedRuleIds = rule.endsWith('*') |
| 145 | + ? expandWildcardRules(rule, Object.keys(rulesMap)) |
| 146 | + : [rule]; |
| 147 | + |
| 148 | + const matchedRefs = matchedRuleIds.flatMap(ruleId => { |
| 149 | + const matchingRules = rulesMap[ruleId] || []; |
| 150 | + const weightPerRule = weight / matchingRules.length; |
| 151 | + |
| 152 | + return matchingRules.map(ruleData => ({ |
| 153 | + slug: ruleToSlug(ruleData), |
| 154 | + weight: weightPerRule, |
| 155 | + })); |
| 156 | + }); |
| 157 | + |
| 158 | + return { |
| 159 | + refs: [...acc.refs, ...matchedRefs], |
| 160 | + invalidRules: |
| 161 | + matchedRefs.length > 0 |
| 162 | + ? acc.invalidRules |
| 163 | + : [...acc.invalidRules, rule], |
| 164 | + }; |
| 165 | + }, |
| 166 | + { refs: [], invalidRules: [] }, |
| 167 | + ); |
| 168 | +} |
0 commit comments