Skip to content

Commit fdf0125

Browse files
authored
report rule with most (and least) selectors (#130)
* report rule with most (and least) selectors * update readme
1 parent de05ee0 commit fdf0125

File tree

8 files changed

+195
-49
lines changed

8 files changed

+195
-49
lines changed

readme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ analyze('foo{}')
8787
// 'properties.unique': [],
8888
// 'rules.total': 1,
8989
// 'rules.empty.total': 1,
90+
// 'rules.selectors.max': 1,
91+
// 'rules.selectors.min': 1,
92+
// 'rules.selectors.minimum.count': 1,
93+
// 'rules.selectors.minimum.value': ['foo'],
94+
// 'rules.selectors.maximum.count': 1,
95+
// 'rules.selectors.maximum.value': ['foo'],
96+
// 'rules.selectors.average': 1,
9097
// 'selectors.accessibility.total': 0,
9198
// 'selectors.accessibility.totalUnique': 0,
9299
// 'selectors.accessibility.unique': [],

src/analyzer/rules/index.js

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,58 @@
1+
const compareStrings = require('string-natural-compare')
2+
13
const isEmpty = rule => {
2-
return rule.declarationsCount === 0
4+
return rule.declarations.length === 0
35
}
46

57
module.exports = rules => {
68
const all = rules
9+
const total = all.length
710
const empty = rules.filter(isEmpty)
811

9-
const selectorsPerRule = rules.map(({selectorsCount}) => selectorsCount)
12+
// We only want to deal with rules that have selectors.
13+
// While parsing the CSS we have stripped the 'selectors' from
14+
// @keyframes, so we need to ignore the accompanying
15+
// declarations here too
16+
const rulesSortedBySelectorCount = [...all]
17+
.filter(({selectors}) => selectors.length > 0)
18+
.sort((a, b) => {
19+
if (a.selectors.length === b.selectors.length) {
20+
return compareStrings.caseInsensitive(
21+
a.selectors.join(''),
22+
b.selectors.join('')
23+
)
24+
}
25+
26+
return a.selectors.length - b.selectors.length
27+
})
28+
const ruleWithLeastSelectors = [...rulesSortedBySelectorCount].shift()
29+
const ruleWithMostSelectors = [...rulesSortedBySelectorCount].pop()
30+
31+
const selectorsPerRule = all.map(({selectors}) => selectors.length)
32+
const averageSelectorsPerRule =
33+
total === 0
34+
? 0
35+
: selectorsPerRule.reduce((acc, curr) => acc + curr, 0) / total
1036

1137
return {
12-
total: all.length,
38+
total,
1339
empty: {
1440
total: empty.length
1541
},
1642
selectors: {
17-
min: selectorsPerRule.length > 0 ? Math.min(...selectorsPerRule) : 0,
18-
max: selectorsPerRule.length > 0 ? Math.max(...selectorsPerRule) : 0,
19-
average:
20-
selectorsPerRule.length > 0
21-
? selectorsPerRule.reduce((acc, curr) => acc + curr, 0) / rules.length
22-
: 0
43+
/** @deprecated */
44+
min: total === 0 ? 0 : ruleWithLeastSelectors.selectors.length,
45+
/** @deprecated */
46+
max: total === 0 ? 0 : ruleWithMostSelectors.selectors.length,
47+
average: averageSelectorsPerRule,
48+
minimum: {
49+
count: total === 0 ? 0 : ruleWithLeastSelectors.selectors.length,
50+
value: total === 0 ? [] : ruleWithLeastSelectors.selectors
51+
},
52+
maximum: {
53+
count: total === 0 ? 0 : ruleWithMostSelectors.selectors.length,
54+
value: total === 0 ? [] : ruleWithMostSelectors.selectors
55+
}
2356
}
2457
}
2558
}

src/parser/declarations.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
const IGNORE_FROM_PARENT = 'font-face'
2+
13
module.exports = tree => {
24
const declarations = []
3-
const IGNORE_FROM_PARENT = 'font-face'
45

56
tree.walkDecls(declaration => {
67
if (declaration.parent.name === IGNORE_FROM_PARENT) {
@@ -10,7 +11,7 @@ module.exports = tree => {
1011
declarations.push({
1112
// Need to prefix with the 'before', otherwise PostCSS will
1213
// trim off any browser hacks prefixes like * or _
13-
property: `${declaration.raws.before.trim()}${declaration.prop}`,
14+
property: declaration.raws.before.trim() + declaration.prop,
1415
value: declaration.value,
1516
important: Boolean(declaration.important)
1617
})

src/parser/rules.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
const {isKeyframes} = require('./atrules')
2+
const getDeclarationsFromRule = require('./declarations')
3+
const {getSelectorsFromRule} = require('./selectors')
24

35
module.exports = tree => {
46
const rules = []
57

68
tree.walkRules(rule => {
7-
// Count declarations per rule
8-
let declarationsCount = 0
9+
const declarations = getDeclarationsFromRule(rule)
10+
// Don't include the 'selectors' (from, 50%, to, etc.) inside @keyframes
11+
const selectors = isKeyframes(rule) ? [] : getSelectorsFromRule(rule)
912

10-
rule.walkDecls(() => {
11-
declarationsCount += 1
12-
})
13-
14-
// Count selectors per rule, but don't include the 'selectors'
15-
// (from, 50%, to, etc.) inside @keyframes
16-
const selectorsCount = isKeyframes(rule)
17-
? 0
18-
: rule.selector.split(',').length
19-
20-
rules.push({declarationsCount, selectorsCount})
13+
rules.push({declarations, selectors})
2114
})
2215

2316
return rules

src/parser/selectors.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ module.exports = tree => {
1010
}
1111

1212
// Get selectors: flatten the list, split each by ',' and trim the results
13-
selectors.push(...rule.selector.split(',').map(s => s.trim()))
13+
selectors.push(...getSelectorsFromRule(rule))
1414
})
1515

1616
return selectors
1717
}
18+
19+
const getSelectorsFromRule = rule => {
20+
return rule.selector.split(',').map(s => s.trim())
21+
}
22+
23+
module.exports.getSelectorsFromRule = getSelectorsFromRule

test/analyzer/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ test('Returns the correct analysis object structure', async t => {
7070
'rules.selectors.max': 1,
7171
'rules.selectors.min': 1,
7272
'rules.selectors.average': 1,
73+
'rules.selectors.maximum.count': 1,
74+
'rules.selectors.maximum.value': ['foo'],
75+
'rules.selectors.minimum.count': 1,
76+
'rules.selectors.minimum.value': ['foo'],
7377
'selectors.accessibility.total': 0,
7478
'selectors.accessibility.totalUnique': 0,
7579
'selectors.accessibility.unique': [],

test/analyzer/rules/index.js

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,49 +10,105 @@ test('it responds with the correct structure', t => {
1010
total: 0
1111
},
1212
selectors: {
13-
min: 0,
14-
max: 0,
13+
min: 0 /** @deprecated */,
14+
minimum: {
15+
count: 0,
16+
value: []
17+
},
18+
max: 0 /** @deprecated */,
19+
maximum: {
20+
count: 0,
21+
value: []
22+
},
1523
average: 0
1624
}
1725
})
1826
})
1927

2028
test('it counts basic rules', t => {
21-
const {total} = analyze([{declarationsCount: 1}, {declarationsCount: 8}])
29+
const {total} = analyze([
30+
{declarations: [], selectors: ['a']},
31+
{declarations: [], selectors: ['b']}
32+
])
2233
t.is(total, 2)
2334
})
2435

2536
test('it counts empty rules', t => {
26-
const {
27-
empty: {total}
28-
} = analyze([{declarationsCount: 1}, {declarationsCount: 0}])
37+
const actual = analyze([
38+
{
39+
declarations: [{property: 'a', value: 'a', important: false}],
40+
selectors: ['a']
41+
},
42+
{declarations: [], selectors: ['a']}
43+
])
2944
const expected = 1
30-
t.is(total, expected)
45+
t.is(actual.empty.total, expected)
3146
})
3247

3348
test('it counts the average selectors per rule', t => {
34-
const {
35-
selectors: {average}
36-
} = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
49+
const actual = analyze([
50+
{selectors: ['a', 'b', 'c', 'd'], declarations: []},
51+
{selectors: ['a'], declarations: []}
52+
])
3753
const expected = 2.5
3854

39-
t.is(average, expected)
55+
t.is(actual.selectors.average, expected)
56+
})
57+
58+
test('it counts the minimum selectors per rule', t => {
59+
const actual = analyze([
60+
{selectors: ['a', 'b', 'c', 'd'], declarations: []},
61+
{selectors: ['a'], declarations: []}
62+
])
63+
64+
t.is(actual.selectors.minimum.count, 1)
65+
t.deepEqual(actual.selectors.minimum.value, ['a'])
66+
})
67+
68+
test('it counts the maximum selectors per rule', t => {
69+
const actual = analyze([
70+
{selectors: ['a', 'b', 'c', 'd'], declarations: []},
71+
{selectors: ['a'], declarations: []}
72+
])
73+
74+
t.is(actual.selectors.maximum.count, 4)
75+
t.deepEqual(actual.selectors.maximum.value, ['a', 'b', 'c', 'd'])
76+
})
77+
78+
test('it sorts the minimum selectors per rule by string length and alphabetically', t => {
79+
const actual = analyze([
80+
{selectors: ['aa'], declarations: []},
81+
{selectors: ['A'], declarations: []},
82+
{selectors: ['b'], declarations: []},
83+
{selectors: ['bb'], declarations: []}
84+
])
85+
86+
t.is(actual.selectors.minimum.count, 1)
87+
t.deepEqual(actual.selectors.minimum.value, ['A'])
4088
})
4189

90+
/**
91+
* @deprecated in v3.0.0
92+
*/
4293
test('it counts the min selectors per rule', t => {
43-
const {
44-
selectors: {min}
45-
} = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
94+
const actual = analyze([
95+
{selectors: ['a', 'b', 'c', 'd'], declarations: []},
96+
{selectors: ['a'], declarations: []}
97+
])
4698
const expected = 1
4799

48-
t.is(min, expected)
100+
t.is(actual.selectors.min, expected)
49101
})
50102

103+
/**
104+
* @deprecated v3.0.0
105+
*/
51106
test('it counts the max selectors per rule', t => {
52-
const {
53-
selectors: {max}
54-
} = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
107+
const actual = analyze([
108+
{selectors: ['a', 'b', 'c', 'd'], declarations: []},
109+
{selectors: ['a'], declarations: []}
110+
])
55111
const expected = 4
56112

57-
t.is(max, expected)
113+
t.is(actual.selectors.max, expected)
58114
})

test/parser/rules.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@ test('basic rules are parsed', async t => {
55
const fixture = 'html {color:red} @media screen { html {} }'
66
const actual = await parser(fixture)
77
const expected = [
8-
{declarationsCount: 1, selectorsCount: 1},
9-
{declarationsCount: 0, selectorsCount: 1}
8+
{
9+
selectors: ['html'],
10+
declarations: [
11+
{
12+
property: 'color',
13+
value: 'red',
14+
important: false
15+
}
16+
]
17+
},
18+
{
19+
selectors: ['html'],
20+
declarations: []
21+
}
1022
]
1123

1224
t.deepEqual(actual.rules, expected)
@@ -16,8 +28,31 @@ test('declarations per rule are counted', async t => {
1628
const fixture = 'html, body {color:red; font-size : 12px} .foo {color: red;}'
1729
const actual = await parser(fixture)
1830
const expected = [
19-
{declarationsCount: 2, selectorsCount: 2},
20-
{declarationsCount: 1, selectorsCount: 1}
31+
{
32+
selectors: ['html', 'body'],
33+
declarations: [
34+
{
35+
property: 'color',
36+
value: 'red',
37+
important: false
38+
},
39+
{
40+
property: 'font-size',
41+
value: '12px',
42+
important: false
43+
}
44+
]
45+
},
46+
{
47+
selectors: ['.foo'],
48+
declarations: [
49+
{
50+
property: 'color',
51+
value: 'red',
52+
important: false
53+
}
54+
]
55+
}
2156
]
2257
t.deepEqual(actual.rules, expected)
2358
})
@@ -35,6 +70,17 @@ test('heavily nested rules are parsed', async t => {
3570
}
3671
`
3772
const actual = await parser(fixture)
38-
const expected = [{declarationsCount: 1, selectorsCount: 1}]
73+
const expected = [
74+
{
75+
selectors: ['.rule2'],
76+
declarations: [
77+
{
78+
property: 'color',
79+
value: 'red',
80+
important: false
81+
}
82+
]
83+
}
84+
]
3985
t.deepEqual(actual.rules, expected)
4086
})

0 commit comments

Comments
 (0)