Repository: sasstools/sass-lint
Pull request #323 (2015-10-17 18:53:51)

Refactor of force nested rules to remove duplication
PR removes a lot of duplicated functionality from the three force nested rules (`force-pseudo-nesting`, `force-element-nesting` and `force-attribute-nesting`). 

It's decreased the amount of code in each rule by 50% through the creation of 2 new helper functions (`isNestable` and `constructSelector`) and a slight refactor of the rule code.

~~It adds 2 other helper functions (`capitalize` and `camelCaseToHyphens`) to assist with improving the formatting of the warning messages from these three rules.~~
**Update:** 2 string helper functions to improve the formatting of warning messages have been removed and replaced with underscore.string library.

DCO 1.1 Signed-off-by: Ben Griffith gt11687@gmail.com


Patch:
diff --git a/lib/helpers.js b/lib/helpers.js
index ba4e9fe..8587c75 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -216,4 +216,111 @@ helpers.mapDelims = function (val) {
   return false;
 };
 
+/**
+ * Constructs a syntax complete selector
+ * @param {object} node - the current node / part of our selector
+ * @returns {object} constructedSelector - The constructed selector
+ * @returns {string} constructedSelector.content - The selector string
+ * @returns {string} constructedSelector.type - The type of the selector
+ */
+helpers.constructSelector = function (node) {
+  var content = node.content,
+      type = '';
+
+  if (node.type === 'id') {
+    content = '#' + node.content;
+    type = 'id';
+  }
+
+  else if (node.type === 'class') {
+    content = '.' + node.content;
+    type = 'class';
+  }
+
+  else if (node.type === 'ident') {
+    content = node.content;
+    type = 'selector';
+  }
+
+  else if (node.type === 'attribute') {
+    var selector = '[';
+
+    node.forEach(function (attrib) {
+      var selectorPiece = helpers.constructSelector(attrib);
+
+      selector += selectorPiece.content;
+    });
+
+    content = selector + ']';
+    type = 'attribute';
+  }
+
+  else if (node.type === 'pseudoClass') {
+    content = ':' + node.content;
+    type = 'pseudoClass';
+  }
+
+  else if (node.type === 'pseudoElement') {
+    content = '::' + node.content;
+    type = 'pseudoElement';
+  }
+
+  else if (node.type === 'nth') {
+    content = '(' + node.content + ')';
+    type = 'nth';
+  }
+
+  else if (node.type === 'nthSelector') {
+    var nthSelector = ':';
+
+    node.forEach(function (attrib) {
+      var selectorPiece = helpers.constructSelector(attrib);
+
+      nthSelector += selectorPiece.content;
+    });
+
+    content = nthSelector;
+    type = 'nthSelector';
+  }
+
+  else if (node.type === 'space') {
+    content = ' ';
+  }
+
+  else if (node.type === 'parentSelector') {
+    content = node.content;
+    type = 'parentSelector';
+  }
+
+  else if (node.type === 'combinator') {
+    content = node.content;
+    type = 'combinator';
+  }
+
+  return {
+    content: content,
+    type: type
+  };
+};
+
+/**
+ * Checks the current selector value against the previous selector value and assesses whether they are
+ * a) currently an enforced selector type for nesting (user specified - all true by default)
+ * b) whether they should be nested
+ * @param {object} currentVal - the current node / part of our selector
+ * @param {object} previousVal - the previous node / part of our selector
+ * @param {array} elements - a complete array of nestable selector types
+ * @param {array} nestable - an array of the types of selector to nest
+ * @returns {object} Returns whether we or we should nest and the previous val
+ */
+helpers.isNestable = function (currentVal, previousVal, elements, nestable) {
+  // check if they are nestable by checking the previous element against one
+  // of the user specified selector types
+  if (elements.indexOf(previousVal) !== -1 && nestable.indexOf(currentVal) !== -1) {
+    return true;
+  }
+
+  return false;
+};
+
 module.exports = helpers;
diff --git a/lib/rules/force-attribute-nesting.js b/lib/rules/force-attribute-nesting.js
index e18d4e8..861e752 100644
--- a/lib/rules/force-attribute-nesting.js
+++ b/lib/rules/force-attribute-nesting.js
@@ -1,147 +1,40 @@
 'use strict';
 
-var helpers = require('../helpers');
+var helpers = require('../helpers'),
+    capitalize = require('lodash.capitalize'),
+    kebabcase = require('lodash.kebabcase');
 
 // Our nestable selector types, separated by type for ease of use with rules
 // we replace ident with 'selector' for readability'
 var nestableElements = ['selector', 'class', 'id'],
     nestableAttributes = ['attribute'],
-    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'],
-    // we need to keep track of the current and previous selector type
-    prevVal = '',
-    curVal = '',
-
-    /**
-     * constructSelector
-     * Constructs a syntax complete selector for our selector matching and warning output
-     * @param {object} val - the current node / part of our selector
-     * @returns {string} content - The current node with correct syntax e.g. class my-class = '.my-class'
-     */
-
-    constructSelector = function (val) {
-
-      var content = val.content;
-
-      if (val.is('id')) {
-        content = '#' + val.content;
-        curVal = 'id';
-      }
-
-      else if (val.is('class')) {
-        content = '.' + val.content;
-        curVal = 'class';
-      }
-
-      else if (val.is('ident')) {
-        content = val.content;
-        curVal = 'selector';
-      }
-
-      else if (val.is('attribute')) {
-        var selector = '[';
-        val.forEach( function (attrib) {
-          selector += constructSelector(attrib);
-        });
-        content = selector + ']';
-        curVal = 'attribute';
-      }
-
-      else if (val.is('pseudoClass')) {
-        content = ':' + val.content;
-        curVal = 'pseudoClass';
-      }
-
-      else if (val.is('pseudoElement')) {
-        content = '::' + val.content;
-        curVal = 'pseudoElement';
-      }
-
-      else if (val.is('nth')) {
-        content = '(' + val.content + ')';
-        curVal = 'nth';
-      }
-
-      else if (val.is('nthSelector')) {
-        var nthSelector = ':';
-        val.forEach( function (attrib) {
-          nthSelector += constructSelector(attrib);
-        });
-        content = nthSelector;
-        curVal = 'nthSelector';
-      }
-
-      else if (val.is('space')) {
-        content = ' ';
-      }
-
-      else if (val.is('parentSelector')) {
-        content = val.content;
-        curVal = 'parentSelector';
-      }
-
-      else if (val.is('combinator')) {
-        content = val.content;
-        curVal = 'combinator';
-      }
-
-      return content;
-
-    },
-
-    /**
-     * resets the current value and previous value markers to default
-     */
-
-    resetNesting = function () {
-      curVal = '';
-      prevVal = '';
-    },
-
-    /**
-     * checkNesting
-     * Checks the current selector value against the previous selector value and assesses whether they are
-     * a) currently an enforced selector type for nesting (user specified - all true by default)
-     * b) whether they should be nested
-     * @param {object} val - the current node / part of our selector
-     * @param {string} curSelector - the current selector string
-     * @param {object} parser - and instance of our parser
-     * @param {array} result - an array of result objects
-     */
-
-    checkNesting = function (val, parser, result) {
-      var elements = nestableElements.concat(nestableAttributes, nestablePseudo);
-      // check if they are nestable by checking the previous element against one of the user specified
-      // selector types
-      if (elements.indexOf(prevVal) !== -1 && nestableAttributes.indexOf(curVal) !== -1) {
-        helpers.addUnique(result, {
-          'ruleId': parser.rule.name,
-          'line': val.start.line,
-          'column': val.start.column,
-          'message': curVal + ' `' + constructSelector(val) + '` should be nested within its parent ' + prevVal,
-          'severity': parser.severity
-        });
-      }
-
-      if (curVal !== '') {
-        prevVal = curVal;
-        curVal = '';
-      }
-    };
+    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'];
+
+/**
+ * Formats a string from camelCase to hyphens and capitalizes
+ * @param {string} str - The string to be formatted
+ * @returns {string} A hyphenated and capitalized string
+ */
+var formatOutput = function (str) {
+  return capitalize(kebabcase(str));
+};
 
 module.exports = {
   'name': 'force-attribute-nesting',
   'defaults': {},
   'detect': function (ast, parser) {
-    var result = [];
+    var result = [],
+        elements = nestableElements.concat(nestableAttributes, nestablePseudo);
 
     ast.traverseByType('ruleset', function (ruleset) {
       var delimOrder = [];
 
       ruleset.forEach('selector', function (selector) {
+        // Where we'll store the previous value
+        var previousVal;
 
-        resetNesting();
         // Keep track of the order of elements & delimeters
-        selector.forEach( function (el) {
+        selector.forEach(function (el) {
           var curNode = helpers.mapDelims(el);
 
           if (curNode !== false) {
@@ -150,35 +43,49 @@ module.exports = {
         });
 
         selector.forEach('simpleSelector', function (simpleSelector) {
-
           // check if the next selector is proceeded by a delimiter
           // if it is add it to the curSelector output
-
           var nextType = delimOrder[0];
 
           if (nextType === 'd') {
-            resetNesting();
+            // Empty the previous value store
+            previousVal = null;
 
-            // remove the next delim and selector from our order as we are only looping over selectors
+            // remove the next delim and selector from our order as we are only
+            // looping over selectors
             delimOrder.splice(0, 2);
           }
           else {
-
-            // if no delim then just remove the current selectors marker in the order array
+            // if no delim then just remove the current selectors marker in the
+            // order array
             delimOrder.splice(0, 1);
           }
 
-          simpleSelector.forEach(function (val) {
-
-            // construct our selector from its content parts
-            constructSelector(val);
-
-            checkNesting(val, parser, result);
-
+          // Loop each selector while checking if it should be nested
+          simpleSelector.forEach(function (node) {
+            // Construct the selector and store the current selector value
+            var constructedSelector = helpers.constructSelector(node),
+                currentVal = constructedSelector.type;
+
+            if (helpers.isNestable(currentVal, previousVal, elements, nestableAttributes)) {
+              helpers.addUnique(result, {
+                'ruleId': parser.rule.name,
+                'line': node.start.line,
+                'column': node.start.column,
+                'message': formatOutput(currentVal) + ' `' + constructedSelector.content + '` should be nested within its parent ' + previousVal,
+                'severity': parser.severity
+              });
+            }
+
+            if (currentVal) {
+              // Store current value as previous and continue with the loop
+              previousVal = currentVal;
+            }
           });
         });
       });
     });
+
     return result;
   }
 };
diff --git a/lib/rules/force-element-nesting.js b/lib/rules/force-element-nesting.js
index eae7972..7e9692b 100644
--- a/lib/rules/force-element-nesting.js
+++ b/lib/rules/force-element-nesting.js
@@ -1,147 +1,40 @@
 'use strict';
 
-var helpers = require('../helpers');
+var helpers = require('../helpers'),
+    capitalize = require('lodash.capitalize'),
+    kebabcase = require('lodash.kebabcase');
 
 // Our nestable selector types, separated by type for ease of use with rules
 // we replace ident with 'selector' for readability'
 var nestableElements = ['selector', 'class', 'id'],
     nestableAttributes = ['attribute'],
-    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'],
-    // we need to keep track of the current and previous selector type
-    prevVal = '',
-    curVal = '',
-
-    /**
-     * constructSelector
-     * Constructs a syntax complete selector for our selector matching and warning output
-     * @param {object} val - the current node / part of our selector
-     * @returns {string} content - The current node with correct syntax e.g. class my-class = '.my-class'
-     */
-
-    constructSelector = function (val) {
-
-      var content = val.content;
-
-      if (val.is('id')) {
-        content = '#' + val.content;
-        curVal = 'id';
-      }
-
-      else if (val.is('class')) {
-        content = '.' + val.content;
-        curVal = 'class';
-      }
-
-      else if (val.is('ident')) {
-        content = val.content;
-        curVal = 'selector';
-      }
-
-      else if (val.is('attribute')) {
-        var selector = '[';
-        val.forEach( function (attrib) {
-          selector += constructSelector(attrib);
-        });
-        content = selector + ']';
-        curVal = 'attribute';
-      }
-
-      else if (val.is('pseudoClass')) {
-        content = ':' + val.content;
-        curVal = 'pseudoClass';
-      }
-
-      else if (val.is('pseudoElement')) {
-        content = '::' + val.content;
-        curVal = 'pseudoElement';
-      }
-
-      else if (val.is('nth')) {
-        content = '(' + val.content + ')';
-        curVal = 'nth';
-      }
-
-      else if (val.is('nthSelector')) {
-        var nthSelector = ':';
-        val.forEach( function (attrib) {
-          nthSelector += constructSelector(attrib);
-        });
-        content = nthSelector;
-        curVal = 'nthSelector';
-      }
-
-      else if (val.is('space')) {
-        content = ' ';
-      }
-
-      else if (val.is('parentSelector')) {
-        content = val.content;
-        curVal = 'parentSelector';
-      }
-
-      else if (val.is('combinator')) {
-        content = val.content;
-        curVal = 'combinator';
-      }
-
-      return content;
-
-    },
-
-    /**
-     * resets the current value and previous value markers to default
-     */
-
-    resetNesting = function () {
-      curVal = '';
-      prevVal = '';
-    },
-
-    /**
-     * checkNesting
-     * Checks the current selector value against the previous selector value and assesses whether they are
-     * a) currently an enforced selector type for nesting (user specified - all true by default)
-     * b) whether they should be nested
-     * @param {object} val - the current node / part of our selector
-     * @param {string} curSelector - the current selector string
-     * @param {object} parser - and instance of our parser
-     * @param {array} result - an array of result objects
-     */
-
-    checkNesting = function (val, parser, result) {
-      var elements = nestableElements.concat(nestableAttributes, nestablePseudo);
-      // check if they are nestable by checking the previous element against one of the user specified
-      // selector types
-      if (elements.indexOf(prevVal) !== -1 && nestableElements.indexOf(curVal) !== -1) {
-        helpers.addUnique(result, {
-          'ruleId': parser.rule.name,
-          'line': val.start.line,
-          'column': val.start.column,
-          'message': curVal + ' `' + constructSelector(val) + '` should be nested within its parent ' + prevVal,
-          'severity': parser.severity
-        });
-      }
-
-      if (curVal !== '') {
-        prevVal = curVal;
-        curVal = '';
-      }
-    };
+    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'];
+
+/**
+ * Formats a string from camelCase to hyphens and capitalizes
+ * @param {string} str - The string to be formatted
+ * @returns {string} A hyphenated and capitalized string
+ */
+var formatOutput = function (str) {
+  return capitalize(kebabcase(str));
+};
 
 module.exports = {
   'name': 'force-element-nesting',
   'defaults': {},
   'detect': function (ast, parser) {
-    var result = [];
+    var result = [],
+        elements = nestableElements.concat(nestableAttributes, nestablePseudo);
 
     ast.traverseByType('ruleset', function (ruleset) {
       var delimOrder = [];
 
       ruleset.forEach('selector', function (selector) {
+        // Where we'll store the previous value
+        var previousVal;
 
-        resetNesting();
         // Keep track of the order of elements & delimeters
-        selector.forEach( function (el) {
+        selector.forEach(function (el) {
           var curNode = helpers.mapDelims(el);
 
           if (curNode !== false) {
@@ -150,35 +43,49 @@ module.exports = {
         });
 
         selector.forEach('simpleSelector', function (simpleSelector) {
-
           // check if the next selector is proceeded by a delimiter
           // if it is add it to the curSelector output
-
           var nextType = delimOrder[0];
 
           if (nextType === 'd') {
-            resetNesting();
+            // Empty the previous value store
+            previousVal = null;
 
-            // remove the next delim and selector from our order as we are only looping over selectors
+            // remove the next delim and selector from our order as we are only
+            // looping over selectors
             delimOrder.splice(0, 2);
           }
           else {
-
-            // if no delim then just remove the current selectors marker in the order array
+            // if no delim then just remove the current selectors marker in the
+            // order array
             delimOrder.splice(0, 1);
           }
 
-          simpleSelector.forEach(function (val) {
-
-            // construct our selector from its content parts
-            constructSelector(val);
-
-            checkNesting(val, parser, result);
-
+          // Loop each selector while checking if it should be nested
+          simpleSelector.forEach(function (node) {
+            // Construct the selector and store the current selector value
+            var constructedSelector = helpers.constructSelector(node),
+                currentVal = constructedSelector.type;
+
+            if (helpers.isNestable(currentVal, previousVal, elements, nestableElements)) {
+              helpers.addUnique(result, {
+                'ruleId': parser.rule.name,
+                'line': node.start.line,
+                'column': node.start.column,
+                'message': formatOutput(currentVal) + ' `' + constructedSelector.content + '` should be nested within its parent ' + previousVal,
+                'severity': parser.severity
+              });
+            }
+
+            if (currentVal) {
+              // Store current value as previous and continue with the loop
+              previousVal = currentVal;
+            }
           });
         });
       });
     });
+
     return result;
   }
 };
diff --git a/lib/rules/force-pseudo-nesting.js b/lib/rules/force-pseudo-nesting.js
index 5832a46..3f1b2d7 100644
--- a/lib/rules/force-pseudo-nesting.js
+++ b/lib/rules/force-pseudo-nesting.js
@@ -1,147 +1,47 @@
 'use strict';
 
-var helpers = require('../helpers');
+var helpers = require('../helpers'),
+    capitalize = require('lodash.capitalize'),
+    kebabcase = require('lodash.kebabcase');
 
 // Our nestable selector types, separated by type for ease of use with rules
 // we replace ident with 'selector' for readability'
 var nestableElements = ['selector', 'class', 'id'],
     nestableAttributes = ['attribute'],
-    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'],
-    // we need to keep track of the current and previous selector type
-    prevVal = '',
-    curVal = '',
-
-    /**
-     * constructSelector
-     * Constructs a syntax complete selector for our selector matching and warning output
-     * @param {object} val - the current node / part of our selector
-     * @returns {string} content - The current node with correct syntax e.g. class my-class = '.my-class'
-     */
-
-    constructSelector = function (val) {
-
-      var content = val.content;
-
-      if (val.is('id')) {
-        content = '#' + val.content;
-        curVal = 'id';
-      }
-
-      else if (val.is('class')) {
-        content = '.' + val.content;
-        curVal = 'class';
-      }
-
-      else if (val.is('ident')) {
-        content = val.content;
-        curVal = 'selector';
-      }
-
-      else if (val.is('attribute')) {
-        var selector = '[';
-        val.forEach( function (attrib) {
-          selector += constructSelector(attrib);
-        });
-        content = selector + ']';
-        curVal = 'attribute';
-      }
-
-      else if (val.is('pseudoClass')) {
-        content = ':' + val.content;
-        curVal = 'pseudoClass';
-      }
-
-      else if (val.is('pseudoElement')) {
-        content = '::' + val.content;
-        curVal = 'pseudoElement';
-      }
-
-      else if (val.is('nth')) {
-        content = '(' + val.content + ')';
-        curVal = 'nth';
-      }
-
-      else if (val.is('nthSelector')) {
-        var nthSelector = ':';
-        val.forEach( function (attrib) {
-          nthSelector += constructSelector(attrib);
-        });
-        content = nthSelector;
-        curVal = 'nthSelector';
-      }
-
-      else if (val.is('space')) {
-        content = ' ';
-      }
-
-      else if (val.is('parentSelector')) {
-        content = val.content;
-        curVal = 'parentSelector';
-      }
-
-      else if (val.is('combinator')) {
-        content = val.content;
-        curVal = 'combinator';
-      }
-
-      return content;
-
-    },
-
-    /**
-     * resets the current value and previous value markers to default
-     */
-
-    resetNesting = function () {
-      curVal = '';
-      prevVal = '';
-    },
-
-    /**
-     * checkNesting
-     * Checks the current selector value against the previous selector value and assesses whether they are
-     * a) currently an enforced selector type for nesting (user specified - all true by default)
-     * b) whether they should be nested
-     * @param {object} val - the current node / part of our selector
-     * @param {string} curSelector - the current selector string
-     * @param {object} parser - and instance of our parser
-     * @param {array} result - an array of result objects
-     */
-
-    checkNesting = function (val, parser, result) {
-      var elements = nestableElements.concat(nestableAttributes, nestablePseudo);
-      // check if they are nestable by checking the previous element against one of the user specified
-      // selector types
-      if (elements.indexOf(prevVal) !== -1 && nestablePseudo.indexOf(curVal) !== -1) {
-        helpers.addUnique(result, {
-          'ruleId': parser.rule.name,
-          'line': val.start.line,
-          'column': val.start.column,
-          'message': curVal + ' `' + constructSelector(val) + '` should be nested within its parent ' + prevVal,
-          'severity': parser.severity
-        });
-      }
+    nestablePseudo = ['pseudoClass', 'pseudoElement', 'nth', 'nthSelector'];
+
+/**
+ * Formats a string from camelCase to hyphens and capitalizes when not
+ * nth related
+ * @param {string} str - The string to be formatted
+ * @returns {string} A hyphenated (and possibly capitalized) string
+ */
+var formatOutput = function (str) {
+  str = kebabcase(str);
+
+  if (str.indexOf('nth') === -1) {
+    str = capitalize(str);
+  }
 
-      if (curVal !== '') {
-        prevVal = curVal;
-        curVal = '';
-      }
-    };
+  return str;
+};
 
 module.exports = {
   'name': 'force-pseudo-nesting',
   'defaults': {},
   'detect': function (ast, parser) {
-    var result = [];
+    var result = [],
+        elements = nestableElements.concat(nestableAttributes, nestablePseudo);
 
     ast.traverseByType('ruleset', function (ruleset) {
       var delimOrder = [];
 
       ruleset.forEach('selector', function (selector) {
+        // Where we'll store the previous value
+        var previousVal;
 
-        resetNesting();
         // Keep track of the order of elements & delimeters
-        selector.forEach( function (el) {
+        selector.forEach(function (el) {
           var curNode = helpers.mapDelims(el);
 
           if (curNode !== false) {
@@ -150,35 +50,49 @@ module.exports = {
         });
 
         selector.forEach('simpleSelector', function (simpleSelector) {
-
           // check if the next selector is proceeded by a delimiter
           // if it is add it to the curSelector output
-
           var nextType = delimOrder[0];
 
           if (nextType === 'd') {
-            resetNesting();
+            // Empty the previous value store
+            previousVal = null;
 
-            // remove the next delim and selector from our order as we are only looping over selectors
+            // remove the next delim and selector from our order as we are only
+            // looping over selectors
             delimOrder.splice(0, 2);
           }
           else {
-
-            // if no delim then just remove the current selectors marker in the order array
+            // if no delim then just remove the current selectors marker in the
+            // order array
             delimOrder.splice(0, 1);
           }
 
-          simpleSelector.forEach(function (val) {
-
-            // construct our selector from its content parts
-            constructSelector(val);
-
-            checkNesting(val, parser, result);
-
+          // Loop each selector while checking if it should be nested
+          simpleSelector.forEach(function (node) {
+            // Construct the selector and store the current selector value
+            var constructedSelector = helpers.constructSelector(node),
+                currentVal = constructedSelector.type;
+
+            if (helpers.isNestable(currentVal, previousVal, elements, nestablePseudo)) {
+              helpers.addUnique(result, {
+                'ruleId': parser.rule.name,
+                'line': node.start.line,
+                'column': node.start.column,
+                'message': formatOutput(currentVal) + ' `' + constructedSelector.content + '` should be nested within its parent ' + previousVal,
+                'severity': parser.severity
+              });
+            }
+
+            if (currentVal) {
+              // Store current value as previous and continue with the loop
+              previousVal = currentVal;
+            }
           });
         });
       });
     });
+
     return result;
   }
 };
diff --git a/package.json b/package.json
index 67f2c59..19d05f6 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,8 @@
     "glob": "^5.0.15",
     "gonzales-pe": "3.0.0-31",
     "js-yaml": "^3.2.6",
+    "lodash.capitalize": "^3.0.0",
+    "lodash.kebabcase": "^3.0.1",
     "merge": "^1.2.0",
     "util": "^0.10.3"
   },


Test patch:
diff --git a/tests/helpers.js b/tests/helpers.js
index 1ed628b..28517eb 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -1,7 +1,8 @@
 'use strict';
 
 var assert = require('assert'),
-    helpers = require('../lib/helpers');
+    helpers = require('../lib/helpers'),
+    gonzales = require('gonzales-pe');
 
 var haystack = [
     {
@@ -14,6 +15,281 @@ var haystack = [
     }
 ];
 
+var idNode = gonzales.createNode({
+  type: 'id',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'header',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 2
+      },
+      end: {
+        line: 1,
+        column: 7
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 1
+  },
+  end: {
+    line: 1,
+    column: 7
+  }
+});
+
+
+var identNode = gonzales.createNode({
+  type: 'ident',
+  content: 'input',
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 1
+  },
+  end: {
+    line: 1,
+    column: 5
+  }
+});
+
+
+var classNode = gonzales.createNode({
+  type: 'class',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'header',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 2
+      },
+      end: {
+        line: 1,
+        column: 7
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 1
+  },
+  end: {
+    line: 1,
+    column: 7
+  }
+});
+
+
+var attributeNode = gonzales.createNode({
+  type: 'attribute',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'type',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 7
+      },
+      end: {
+        line: 1,
+        column: 10
+      }
+    }),
+    gonzales.createNode({
+      type: 'attributeSelector',
+      content: '=',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 11
+      },
+      end: {
+        line: 1,
+        column: 11
+      }
+    }),
+    gonzales.createNode({
+      type: 'ident',
+      content: 'radio',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 12
+      },
+      end: {
+        line: 1,
+        column: 16
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 6
+  },
+  end: {
+    line: 1,
+    column: 17
+  }
+});
+
+
+var pseudoNode = gonzales.createNode({
+  type: 'pseudoClass',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'last-child',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 3
+      },
+      end: {
+        line: 1,
+        column: 12
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 2
+  },
+  end: {
+    line: 1,
+    column: 12
+  }
+});
+
+
+var pseudoElementNode = gonzales.createNode({
+  type: 'pseudoElement',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'first-line',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 3
+      },
+      end: {
+        line: 1,
+        column: 12
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 2
+  },
+  end: {
+    line: 1,
+    column: 12
+  }
+});
+
+
+
+var nthSelectorNode = gonzales.createNode({
+  type: 'nthSelector',
+  content: [
+    gonzales.createNode({
+      type: 'ident',
+      content: 'nth-of-type',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 3
+      },
+      end: {
+        line: 1,
+        column: 13
+      }
+    }),
+    gonzales.createNode({
+      type: 'nth',
+      content: '2',
+      syntax: 'scss',
+      start: {
+        line: 1,
+        column: 15
+      },
+      end: {
+        line: 1,
+        column: 15
+      }
+    })
+  ],
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 2
+  },
+  end: {
+    line: 1,
+    column: 16
+  }
+});
+
+
+var spaceNode = gonzales.createNode({
+  type: 'space',
+  content: ' ',
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 1
+  },
+  end: {
+    line: 1,
+    column: 1
+  }
+});
+
+
+var parentSelectorNode = gonzales.createNode({
+  type: 'parentSelector',
+  content: '&',
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 1
+  },
+  end: {
+    line: 1,
+    column: 1
+  }
+});
+
+
+var combinatorNode = gonzales.createNode({
+  type: 'combinator',
+  content: '+',
+  syntax: 'scss',
+  start: {
+    line: 1,
+    column: 6
+  },
+  end: {
+    line: 1,
+    column: 6
+  }
+});
+
+
+
 var classBlock =
     {
       type: 'class',
@@ -888,4 +1164,204 @@ describe('helpers', function () {
     done();
   });
 
+  //////////////////////////////
+  // isNestable
+  //////////////////////////////
+
+  it('isNestable - nest attribute in selector', function (done) {
+    var elements = ['selector', 'class', 'id', 'attribute'],
+        nestable = ['class', 'selector', 'attribute'],
+        previous = 'selector',
+        current = 'attribute';
+
+    var result = helpers.isNestable(current, previous, elements, nestable);
+
+    assert.equal(true, result);
+    done();
+  });
+
+  it('isNestable - nest id in class', function (done) {
+    var elements = ['selector', 'class', 'id', 'attribute'],
+        nestable = ['class', 'selector', 'attribute'],
+        previous = 'class',
+        current = 'id';
+
+    var result = helpers.isNestable(current, previous, elements, nestable);
+
+    assert.equal(false, result);
+    done();
+  });
+
+  //////////////////////////////
+  // constructSelector
+  //////////////////////////////
+
+  it('constructSelector - id node - [#header]', function (done) {
+    var result = helpers.constructSelector(idNode),
+        expect = '#header';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - id node - [id]', function (done) {
+    var result = helpers.constructSelector(idNode),
+        expect = 'id';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - class node - [.header]', function (done) {
+    var result = helpers.constructSelector(classNode),
+        expect = '.header';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - class node - [class]', function (done) {
+    var result = helpers.constructSelector(classNode),
+        expect = 'class';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - ident node - [input]', function (done) {
+    var result = helpers.constructSelector(identNode),
+        expect = 'input';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - ident node - [selector]', function (done) {
+    var result = helpers.constructSelector(identNode),
+        expect = 'selector';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - attribute node - [type=radio]', function (done) {
+    var result = helpers.constructSelector(attributeNode),
+        expect = '[type=radio]';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - attribute node - [attribute]', function (done) {
+    var result = helpers.constructSelector(attributeNode),
+        expect = 'attribute';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - pseudo node - [:last-child]', function (done) {
+    var result = helpers.constructSelector(pseudoNode),
+        expect = ':last-child';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - pseudo node - [pseudoClass]', function (done) {
+    var result = helpers.constructSelector(pseudoNode),
+        expect = 'pseudoClass';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - pseudo element node - [::first-line]', function (done) {
+    var result = helpers.constructSelector(pseudoElementNode),
+        expect = '::first-line';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - pseudo element node - [pseudoElement]', function (done) {
+    var result = helpers.constructSelector(pseudoElementNode),
+        expect = 'pseudoElement';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - nthSelector node - [:nth-of-type(2)]', function (done) {
+    var result = helpers.constructSelector(nthSelectorNode),
+        expect = ':nth-of-type(2)';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - nthSelector node - [nthSelector]', function (done) {
+    var result = helpers.constructSelector(nthSelectorNode),
+        expect = 'nthSelector';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - space node - [ ]', function (done) {
+    var result = helpers.constructSelector(spaceNode),
+        expect = ' ';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - space node - []', function (done) {
+    var result = helpers.constructSelector(spaceNode),
+        expect = '';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - parent selector node - [&]', function (done) {
+    var result = helpers.constructSelector(parentSelectorNode),
+        expect = '&';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - parent selector node - [parentSelector]', function (done) {
+    var result = helpers.constructSelector(parentSelectorNode),
+        expect = 'parentSelector';
+
+    assert.equal(expect, result.type);
+    done();
+  });
+
+
+  it('constructSelector - combinator node - [+]', function (done) {
+    var result = helpers.constructSelector(combinatorNode),
+        expect = '+';
+
+    assert.equal(expect, result.content);
+    done();
+  });
+
+  it('constructSelector - combinator node - [combinator]', function (done) {
+    var result = helpers.constructSelector(combinatorNode),
+        expect = 'combinator';
+
+    assert.equal(expect, result.type);
+    done();
+  });
 });