Repository: projectwallace/css-analyzer (js)

Issue:
report selectors per rule
```css
.selector1,
.selector2 {
  // declarations
}

.selector1 {
  // declarations
}
```

```json
{
  "rules.selectors.avg": 1.5,
  "rules.selectors.max": 2
}
```

Pull request description:
add selectors per rule metric
closes #123 

Patch:
diff --git a/package.json b/package.json
index 3a4cac6..7180601 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,10 @@
   },
   "xo": {
     "space": true,
-    "semicolon": false
+    "semicolon": false,
+    "rules": {
+      "operator-linebreak": "off"
+    }
   },
   "prettier": {
     "semi": false,
diff --git a/src/analyzer/rules/index.js b/src/analyzer/rules/index.js
index c4c6da9..4600f52 100644
--- a/src/analyzer/rules/index.js
+++ b/src/analyzer/rules/index.js
@@ -6,10 +6,20 @@ module.exports = rules => {
   const all = rules
   const empty = rules.filter(isEmpty)
 
+  const selectorsPerRule = rules.map(({selectorsCount}) => selectorsCount)
+
   return {
     total: all.length,
     empty: {
       total: empty.length
+    },
+    selectors: {
+      min: selectorsPerRule.length > 0 ? Math.min(...selectorsPerRule) : 0,
+      max: selectorsPerRule.length > 0 ? Math.max(...selectorsPerRule) : 0,
+      average:
+        selectorsPerRule.length > 0
+          ? selectorsPerRule.reduce((acc, curr) => acc + curr, 0) / rules.length
+          : 0
     }
   }
 }
diff --git a/src/parser/atrules.js b/src/parser/atrules.js
index 3c09d42..5ec660d 100644
--- a/src/parser/atrules.js
+++ b/src/parser/atrules.js
@@ -25,10 +25,16 @@ module.exports = tree => {
       params: rule.params.trim()
     }
 
-    return atrules.push(
-      addDescriptorsToAtRuleFromTree(atRule, rule)
-    )
+    return atrules.push(addDescriptorsToAtRuleFromTree(atRule, rule))
   })
 
   return atrules
 }
+
+module.exports.isKeyframes = rule => {
+  return (
+    rule.parent &&
+    rule.parent.type === 'atrule' &&
+    rule.parent.name === 'keyframes'
+  )
+}
diff --git a/src/parser/rules.js b/src/parser/rules.js
index 2f09c5e..ce2ac26 100644
--- a/src/parser/rules.js
+++ b/src/parser/rules.js
@@ -1,15 +1,23 @@
+const {isKeyframes} = require('./atrules')
+
 module.exports = tree => {
   const rules = []
 
   tree.walkRules(rule => {
     // Count declarations per rule
     let declarationsCount = 0
+
     rule.walkDecls(() => {
       declarationsCount += 1
     })
-    rules.push({
-      declarationsCount
-    })
+
+    // Count selectors per rule, but don't include the 'selectors'
+    // (from, 50%, to, etc.) inside @keyframes
+    const selectorsCount = isKeyframes(rule)
+      ? 0
+      : rule.selector.split(',').length
+
+    rules.push({declarationsCount, selectorsCount})
   })
 
   return rules
diff --git a/src/parser/selectors.js b/src/parser/selectors.js
index a2fd24d..2d2072f 100644
--- a/src/parser/selectors.js
+++ b/src/parser/selectors.js
@@ -1,20 +1,16 @@
+const {isKeyframes} = require('./atrules')
+
 module.exports = tree => {
   const selectors = []
 
   tree.walkRules(rule => {
     // Don't include the 'selectors' (from, 50%, to, etc.) inside @keyframes
-    if (rule.parent &&
-      rule.parent.type === 'atrule' &&
-      rule.parent.name === 'keyframes') {
+    if (isKeyframes(rule)) {
       return
     }
 
     // Get selectors: flatten the list, split each by ',' and trim the results
-    selectors.push(
-      ...rule.selector
-        .split(',')
-        .map(s => s.trim())
-    )
+    selectors.push(...rule.selector.split(',').map(s => s.trim()))
   })
 
   return selectors


Test patch:
diff --git a/test/analyzer/index.js b/test/analyzer/index.js
index 0b27c33..fe0311a 100644
--- a/test/analyzer/index.js
+++ b/test/analyzer/index.js
@@ -67,6 +67,9 @@ test('Returns the correct analysis object structure', async t => {
     'properties.unique': [],
     'rules.total': 1,
     'rules.empty.total': 1,
+    'rules.selectors.max': 1,
+    'rules.selectors.min': 1,
+    'rules.selectors.average': 1,
     'selectors.accessibility.total': 0,
     'selectors.accessibility.totalUnique': 0,
     'selectors.accessibility.unique': [],
diff --git a/test/analyzer/rules/index.js b/test/analyzer/rules/index.js
index 7fc7f20..bc03663 100644
--- a/test/analyzer/rules/index.js
+++ b/test/analyzer/rules/index.js
@@ -8,6 +8,11 @@ test('it responds with the correct structure', t => {
     total: 0,
     empty: {
       total: 0
+    },
+    selectors: {
+      min: 0,
+      max: 0,
+      average: 0
     }
   })
 })
@@ -18,6 +23,36 @@ test('it counts basic rules', t => {
 })
 
 test('it counts empty rules', t => {
-  const actual = analyze([{declarationsCount: 1}, {declarationsCount: 0}])
-  t.is(actual.empty.total, 1)
+  const {
+    empty: {total}
+  } = analyze([{declarationsCount: 1}, {declarationsCount: 0}])
+  const expected = 1
+  t.is(total, expected)
+})
+
+test('it counts the average selectors per rule', t => {
+  const {
+    selectors: {average}
+  } = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
+  const expected = 2.5
+
+  t.is(average, expected)
+})
+
+test('it counts the min selectors per rule', t => {
+  const {
+    selectors: {min}
+  } = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
+  const expected = 1
+
+  t.is(min, expected)
+})
+
+test('it counts the max selectors per rule', t => {
+  const {
+    selectors: {max}
+  } = analyze([{selectorsCount: 1}, {selectorsCount: 4}])
+  const expected = 4
+
+  t.is(max, expected)
 })
diff --git a/test/parser/rules.js b/test/parser/rules.js
index 2e8a42d..f39e626 100644
--- a/test/parser/rules.js
+++ b/test/parser/rules.js
@@ -4,7 +4,10 @@ const parser = require('../../src/parser')
 test('basic rules are parsed', async t => {
   const fixture = 'html {color:red} @media screen { html {} }'
   const actual = await parser(fixture)
-  const expected = [{declarationsCount: 1}, {declarationsCount: 0}]
+  const expected = [
+    {declarationsCount: 1, selectorsCount: 1},
+    {declarationsCount: 0, selectorsCount: 1}
+  ]
 
   t.deepEqual(actual.rules, expected)
 })
@@ -12,7 +15,10 @@ test('basic rules are parsed', async t => {
 test('declarations per rule are counted', async t => {
   const fixture = 'html, body {color:red; font-size : 12px} .foo {color: red;}'
   const actual = await parser(fixture)
-  const expected = [2, 1].map(num => ({declarationsCount: num}))
+  const expected = [
+    {declarationsCount: 2, selectorsCount: 2},
+    {declarationsCount: 1, selectorsCount: 1}
+  ]
   t.deepEqual(actual.rules, expected)
 })
 
@@ -29,6 +35,6 @@ test('heavily nested rules are parsed', async t => {
     }
   `
   const actual = await parser(fixture)
-  const expected = [{declarationsCount: 1}]
+  const expected = [{declarationsCount: 1, selectorsCount: 1}]
   t.deepEqual(actual.rules, expected)
 })