Repository: vsiakka/gherkin-lint
Pull request #87 (2017-10-11 18:00:12)

Check the indentation of tags


Patch:
diff --git a/README.md b/README.md
index da1a6c2..83f83a0 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ Or check this:
 | `no-trailing-spaces`                        | Disallows trailing spaces                                                                |
 | `no-unnamed-features`                       | Disallows empty Feature name                                                             |
 | `no-unnamed-scenarios`                      | Disallows empty Scenario name                                                            |
+| `one-space-between-tags`                    | Tags on the same time must be separated by a single space                                |
 | `use-and`                                   | Disallows repeated step names requiring use of And instead                               |
 
 \* These rules cannot be turned off because they detect undocumented cucumber functionality that causes the [gherkin](https://github.com/cucumber/gherkin-javascript) parser to crash.
@@ -84,11 +85,13 @@ You can override the defaults for `indentation` like this:
       "when": 2,
       "then": 2,
       "and": 2,
-      "but": 2
+      "but": 2,
+      "feature tag": 0,
+      "scenario tag": 0
     }
   ]
 }
-There is no need to override all the defaults, as is done above, instead they can be overriden only where required.  `Step` will be used as a fallback if the keyword of the step, eg. 'given', is not specified.
+There is no need to override all the defaults, as is done above, instead they can be overriden only where required.  `Step` will be used as a fallback if the keyword of the step, eg. 'given', is not specified.  If `feature tag` is not set then `Feature` is used as a fallback, and if `scenario tag` is not set then `Scenario` is used as a fallback.
 
 This feature is able to handle all localizations of the gherkin steps.
 ```
diff --git a/package.json b/package.json
index 4042492..85db55a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "gherkin-lint",
-  "version": "2.6.0",
+  "version": "2.7.0",
   "description": "A Gherkin linter/validator written in javascript",
   "author": "Vasiliki Siakka",
   "contributors": [
diff --git a/src/rules/indentation.js b/src/rules/indentation.js
index 9055699..f606beb 100644
--- a/src/rules/indentation.js
+++ b/src/rules/indentation.js
@@ -2,7 +2,7 @@ var _ = require('lodash');
 var languageMapping = require('gherkin').DIALECTS;
 var rule = 'indentation';
 
-var availableConfigs = {
+var defaultConfig = {
   'Feature': 0,
   'Background': 0,
   'Scenario': 0,
@@ -16,60 +16,86 @@ var availableConfigs = {
   'but': 2
 };
 
-var errors = [];
+var availableConfigs = _.merge({}, defaultConfig, {
+  // The values here are unused by the config parsing logic.
+  'feature tag': -1,
+  'scenario tag': -1
+});
 
-function test(parsedLocation, configuration, type) {
-    // location.column is 1 index based so, when we compare with the expected indentation we need to subtract 1
-  if (--parsedLocation.column !== configuration[type]) {
-    errors.push({message: 'Wrong indentation for "' + type +
-                          '", expected indentation level of ' + configuration[type] +
-                          ', but got ' + parsedLocation.column,
-                 rule   : rule,
-                 line   : parsedLocation.line});
+function mergeConfiguration(configuration) {
+  var mergedConfiguration = _.merge({}, defaultConfig, configuration);
+  if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'feature tag')) {
+    mergedConfiguration['feature tag'] = mergedConfiguration['Feature'];
   }
+  if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'scenario tag')) {
+    mergedConfiguration['scenario tag'] = mergedConfiguration['Scenario'];
+  }
+  return mergedConfiguration;
 }
 
-function testStep(step, language, configuration, mergedConfiguration) {
-  var keyword = step.keyword;
-  var stepType = _.findKey(language, function(values) {
-    return values instanceof Array && values.indexOf(keyword) !== -1;
-  });
-  stepType = stepType in configuration ? stepType : 'Step';
-  test(step.location, mergedConfiguration, stepType);
-}
+function testFeature(feature, configuration, mergedConfiguration) {
+  var errors = [];
+
+  function test(parsedLocation, type) {
+    // location.column is 1 index based so, when we compare with the expected
+    // indentation we need to subtract 1
+    if (--parsedLocation.column !== mergedConfiguration[type]) {
+      errors.push({message: 'Wrong indentation for "' + type +
+                            '", expected indentation level of ' + mergedConfiguration[type] +
+                            ', but got ' + parsedLocation.column,
+                   rule   : rule,
+                   line   : parsedLocation.line});
+    }
+  }
 
-function testScenarioOutline(scenarioOutline, mergedConfiguration) {
-  test(scenarioOutline.location, mergedConfiguration, 'Scenario');
-  scenarioOutline.examples.forEach(function(examples) {
-    test(examples.location, mergedConfiguration, 'Examples');
-    test(examples.tableHeader.location, mergedConfiguration, 'example');
-    examples.tableBody.forEach(function(row) {
-      test(row.location, mergedConfiguration, 'example');
+  function testStep(step) {
+    var keyword = step.keyword;
+    var stepType = _.findKey(languageMapping[feature.language], function(values) {
+      return values instanceof Array && values.indexOf(keyword) !== -1;
     });
-  });
-}
+    stepType = stepType in configuration ? stepType : 'Step';
+    test(step.location, stepType);
+  }
 
-function indentation(feature, unused, configuration) {
-  if (!feature || Object.keys(feature).length === 0) {
-    return;
+  function testScenarioOutline(scenarioOutline) {
+    test(scenarioOutline.location, 'Scenario');
+    scenarioOutline.examples.forEach(function(examples) {
+      test(examples.location, 'Examples');
+      test(examples.tableHeader.location, 'example');
+      examples.tableBody.forEach(function(row) {
+        test(row.location, 'example');
+      });
+    });
+  }
+
+  function testTags(tags, type) {
+    _(tags).map(function(tag) {
+      return tag.location;
+    }).groupBy(function(tagLocation) {
+      return tagLocation.line;
+    }).forEach(function(locationsPerLine) {
+      var firstLocation = locationsPerLine.sort(function(location) {
+        return -location.column;
+      })[0];
+      test(firstLocation, type);
+    });
   }
-  var language = languageMapping[feature.language];
-  var mergedConfiguration = _.merge(availableConfigs, configuration);
-  errors = [];
 
-  // Check Feature indentation
-  test(feature.location, mergedConfiguration, 'Feature');
+  test(feature.location, 'Feature');
+  testTags(feature.tags, 'feature tag');
 
   feature.children.forEach(function(child) {
     switch(child.type) {
     case 'Background':
-      test(child.location, mergedConfiguration, 'Background');
+      test(child.location, 'Background');
       break;
     case 'Scenario':
-      test(child.location, mergedConfiguration, 'Scenario');
+      test(child.location, 'Scenario');
+      testTags(child.tags, 'scenario tag');
       break;
     case 'ScenarioOutline':
-      testScenarioOutline(child, mergedConfiguration);
+      testScenarioOutline(child);
+      testTags(child.tags, 'scenario tag');
       break;
     default:
       errors.push({message: 'Unknown gherkin node type ' + child.type,
@@ -78,17 +104,23 @@ function indentation(feature, unused, configuration) {
       break;
     }
 
-    child.steps.forEach(function(step) {
-      // Check Step indentation
-      testStep(step, language, configuration, mergedConfiguration);
-    });
+    child.steps.forEach(testStep);
   });
 
   return errors;
 }
 
+function run(feature, unused, configuration) {
+  if (!feature || Object.keys(feature).length === 0) {
+    return;
+  }
+  var mergedConfiguration = mergeConfiguration(configuration);
+
+  return testFeature(feature, configuration, mergedConfiguration);
+}
+
 module.exports = {
   name: rule,
-  run: indentation,
+  run: run,
   availableConfigs: availableConfigs
 };
diff --git a/src/rules/one-space-between-tags.js b/src/rules/one-space-between-tags.js
new file mode 100644
index 0000000..0770160
--- /dev/null
+++ b/src/rules/one-space-between-tags.js
@@ -0,0 +1,43 @@
+const _ = require('lodash');
+
+var rule = 'one-space-between-tags';
+
+function run(feature) {
+  if (!feature || Object.keys(feature).length === 0) {
+    return [];
+  }
+  var errors = [];
+
+  function testTags(allTags) {
+    _(allTags).sort(function(tag) {
+      return -tag.location.column;
+    }).groupBy(function(tag) {
+      return tag.location.line;
+    }).forEach(function(tags) {
+      _.range(tags.length - 1).map(function(i) {
+        if (tags[i].location.column + tags[i].name.length < tags[i + 1].location.column - 1) {
+          errors.push({
+            line: tags[i].location.line,
+            rule: rule,
+            message: 'There is more than one space between the tags ' +
+                      tags[i].name + ' and ' + tags[i + 1].name
+          });
+        }
+      });
+    });
+  }
+
+  testTags(feature.tags);
+  feature.children.forEach(function(child) {
+    if (child.type === 'Scenario' || child.type === 'ScenarioOutline') {
+      testTags(child.tags);
+    }
+  });
+
+  return errors;
+}
+
+module.exports = {
+  run: run,
+  name: rule
+};


Test patch:
diff --git a/test-data-wip/.gherkin-lintrc b/test-data-wip/.gherkin-lintrc
index 5260b96..64f24da 100644
--- a/test-data-wip/.gherkin-lintrc
+++ b/test-data-wip/.gherkin-lintrc
@@ -16,5 +16,6 @@
   "use-and": "on",
   "no-duplicate-tags": "on",
   "no-superfluous-tags": "on",
-  "no-homogenous-tags": "on"
+  "no-homogenous-tags": "on",
+  "one-space-between-tags": "on"
 }
diff --git a/test/rules/indentation/CorrectIndentationSpaces.feature b/test/rules/indentation/CorrectIndentationSpaces.feature
index e981a35..92e6577 100644
--- a/test/rules/indentation/CorrectIndentationSpaces.feature
+++ b/test/rules/indentation/CorrectIndentationSpaces.feature
@@ -1,11 +1,16 @@
+@featureTag1 @featureTag2 @featureTag3
 Feature: Test for indentation - spaces
 
 Background:
   Given I have a Feature file with great indentation
 
+@scenarioTag1 @scenarioTag2
+@scenarioTag3
 Scenario: This is a Scenario with correct indentation - spaces
   Then I should not see an indentation error
 
+@scenarioTag1 @scenarioTag2
+@scenarioTag3
 Scenario Outline: This is a Scenario Outline with correct indentation - spaces
   Then I should not see an indentation error
 Examples:
diff --git a/test/rules/indentation/CorrectIndentationTabs.feature b/test/rules/indentation/CorrectIndentationTabs.feature
index 4251621..7cd216f 100644
--- a/test/rules/indentation/CorrectIndentationTabs.feature
+++ b/test/rules/indentation/CorrectIndentationTabs.feature
@@ -1,11 +1,16 @@
+@featureTag1 @featureTag2 @featuretag3
 Feature: Test for correct indentation - tabs
 
 Background:
 		Given I have a Feature file with great indentation
 
+@scenarioTag1 @scenarioTag2
+@scenarioTag3
 Scenario: This is a Scenario with correct indentation - tabs
 		Then I should not see an indentation error
 
+@scenarioTag1 @scenarioTag2
+@scenarioTag3
 Scenario Outline: This is a Scenario Outline with correct indentation - tabs
 		Then I should not see an indentation error
 Examples:
diff --git a/test/rules/indentation/CorrectIndentationWithFeatureAndScenarioOverrides.feature b/test/rules/indentation/CorrectIndentationWithFeatureAndScenarioOverrides.feature
new file mode 100644
index 0000000..a5d344d
--- /dev/null
+++ b/test/rules/indentation/CorrectIndentationWithFeatureAndScenarioOverrides.feature
@@ -0,0 +1,19 @@
+ @featureTag1 @featureTag2
+ Feature: Test for indentation
+
+Background:
+  Given I have a Feature file with great indentation
+
+   @scenarioTag1 @scenarioTag2
+   @scenarioTag3
+   Scenario: This is a Scenario with correct indentation
+  Then I should not see an indentation error
+
+   @scenarioTag1 @scenarioTag2
+   @scenarioTag3
+   Scenario Outline: This is a Scenario Outline with correct indentation
+  Then I should not see an indentation error
+Examples:
+  | foo | bar |
+  | bar | foo |
+  | har | har |
diff --git a/test/rules/indentation/CorrectIndentationWithScenarioTagOverrides.feature b/test/rules/indentation/CorrectIndentationWithScenarioTagOverrides.feature
new file mode 100644
index 0000000..bd710c7
--- /dev/null
+++ b/test/rules/indentation/CorrectIndentationWithScenarioTagOverrides.feature
@@ -0,0 +1,19 @@
+@featureTag1 @featureTag2
+Feature: Test for indentation
+
+Background:
+  Given I have a Feature file with great indentation
+
+   @scenarioTag1 @scenarioTag2
+   @scenarioTag3
+Scenario: This is a Scenario with correct indentation
+  Then I should not see an indentation error
+
+   @scenarioTag1 @scenarioTag2
+   @scenarioTag3
+Scenario Outline: This is a Scenario Outline with correct indentation
+  Then I should not see an indentation error
+Examples:
+  | foo | bar |
+  | bar | foo |
+  | har | har |
diff --git a/test/rules/indentation/WrongIndentationDifferentLanguage.feature b/test/rules/indentation/WrongIndentationDifferentLanguage.feature
index e50c124..b2614f7 100644
--- a/test/rules/indentation/WrongIndentationDifferentLanguage.feature
+++ b/test/rules/indentation/WrongIndentationDifferentLanguage.feature
@@ -1,12 +1,17 @@
 #language: de
+				@featureTag1 @featureTag2
     Funktionalität: Test for indentation - with German language and spaces
 
     Grundlage:
 Angenommen I have a Feature file with indentation all over the place
 
+    @scenarioTag1 @scenarioTag2
+ @scenarioTag3
     Szenario: This is a Scenario for indentation - German + spaces
             Dann I should see an indentation error
 
+    @scenarioTag1 @scenarioTag2
+ @scenarioTag3
             Szenariogrundriss: This is a Scenario Outline for indentation - German + spaces
            Dann I should see an indentation error
        Beispiele:
diff --git a/test/rules/indentation/WrongIndentationSpaces.feature b/test/rules/indentation/WrongIndentationSpaces.feature
index a753443..eca3380 100644
--- a/test/rules/indentation/WrongIndentationSpaces.feature
+++ b/test/rules/indentation/WrongIndentationSpaces.feature
@@ -1,11 +1,16 @@
+ @featureTag1 @featureTag2
  Feature: Test for indentation - spaces
 
     Background:
 Given I have a Feature file with indentation all over the place
 
+ @scenarioTag1 @scenarioTag2
+ @scenarioTag3
  Scenario: This is a Scenario for indentation - spaces
    Then I should see an indentation error
 
+   @scenarioTag1 @scenarioTag2
+    @scenarioTag3
    Scenario Outline: This is a Scenario Outline for indentation - spaces
    Then I should see an indentation error
   Examples:
diff --git a/test/rules/indentation/WrongIndentationTabs.feature b/test/rules/indentation/WrongIndentationTabs.feature
index 00f69e3..a2649d0 100644
--- a/test/rules/indentation/WrongIndentationTabs.feature
+++ b/test/rules/indentation/WrongIndentationTabs.feature
@@ -1,11 +1,16 @@
+	@featureTag1 @featureTag2
 	Feature: Test for indentation - tabs
 
 				Background:
 Given I have a Feature file with indentation all over the place
 
+	@scenarioTag1 @scenarioTag2
+	@scenarioTag3
 	Scenario: This is a Scenario for indentation - tabs
 			Then I should see an indentation error
 
+			@scenarioTag1 @scenarioTag2
+				@scenarioTag3
 			Scenario Outline: This is a Scenario Outline for indentation - tabs
 		 Then I should see an indentation error
 		Examples:
diff --git a/test/rules/indentation/indentation.js b/test/rules/indentation/indentation.js
index b518049..520c8f6 100644
--- a/test/rules/indentation/indentation.js
+++ b/test/rules/indentation/indentation.js
@@ -5,34 +5,49 @@ var runTest = ruleTestBase.createRuleTest(rule,
 
 var wrongIndenatationErrors = [{
   messageElements: {element: 'Feature', expected: 0, actual: 1},
+  line: 2
+},{
+  messageElements: {element: 'feature tag', expected: 0, actual: 1},
   line: 1
 },{
   messageElements: {element: 'Background', expected: 0, actual: 4},
-  line: 3
+  line: 4
 },{
   messageElements: {element: 'Step', expected: 2, actual: 0},
-  line: 4
+  line: 5
 },{
   messageElements: {element: 'Scenario', expected: 0, actual: 1},
-  line: 6
+  line: 9
 },{
-  messageElements: {element: 'Step', expected: 2, actual: 3},
+  messageElements: {element: 'scenario tag', expected: 0, actual: 1},
   line: 7
+},{
+  messageElements: {element: 'scenario tag', expected: 0, actual: 1},
+  line: 8
+},{
+  messageElements: {element: 'Step', expected: 2, actual: 3},
+  line: 10
 },{
   messageElements: {element: 'Scenario', expected: 0, actual: 3},
-  line: 9
+  line: 14
 },{
   messageElements: {element: 'Examples', expected: 0, actual: 2},
-  line: 11
+  line: 16
+},{
+  messageElements: {element: 'example', expected: 2, actual: 4},
+  line: 17
+},{
+  messageElements: {element: 'example', expected: 2, actual: 4},
+  line: 18
 },{
-  messageElements: {element: 'example', expected: 2, actual:4},
+  messageElements: {element: 'scenario tag', expected: 0, actual: 3},
   line: 12
 },{
-  messageElements: {element: 'example', expected: 2, actual:4},
+  messageElements: {element: 'scenario tag', expected: 0, actual: 4},
   line: 13
 },{
   messageElements: {element: 'Step', expected: 2, actual: 3},
-  line: 10
+  line: 15
 }];
 
 describe('Indentation rule', function() {
@@ -55,36 +70,64 @@ describe('Indentation rule', function() {
   it('detects errors for features, backgrounds, scenarios, scenario outlines and steps in other languages', function() {
     runTest('indentation/WrongIndentationDifferentLanguage.feature', {}, [{
       messageElements: {element: 'Feature', expected: 0, actual: 4},
+      line: 3
+    },{
+      messageElements: {element: 'feature tag', expected: 0, actual: 4},
       line: 2
     },{
       messageElements: {element: 'Background', expected: 0, actual: 4},
-      line: 4
+      line: 5
     },{
       messageElements: {element: 'Step', expected: 2, actual: 0},
-      line: 5
+      line: 6
     },{
       messageElements: {element: 'Scenario', expected: 0, actual: 4},
-      line: 7
+      line: 10
     },{
-      messageElements: {element: 'Step', expected: 2, actual: 12},
+      messageElements: {element: 'scenario tag', expected: 0, actual: 4},
       line: 8
+    },{
+      messageElements: {element: 'scenario tag', expected: 0, actual: 1},
+      line: 9
+    },{
+      messageElements: {element: 'Step', expected: 2, actual: 12},
+      line: 11
     },{
       messageElements: {element: 'Scenario', expected: 0, actual: 12},
-      line: 10
+      line: 15
     },{
       messageElements: {element: 'Examples', expected: 0, actual: 7},
-      line: 12
+      line: 17
     },{
       messageElements: {element: 'example', expected: 2, actual: 15},
-      line: 13
+      line: 18
     },{
       messageElements: {element: 'example', expected: 2, actual: 15},
+      line: 19
+    },{
+      messageElements: {element: 'scenario tag', expected: 0, actual: 4},
+      line: 13
+    },{
+      messageElements: {element: 'scenario tag', expected: 0, actual: 1},
       line: 14
     },{
       messageElements: {element: 'Step', expected: 2, actual: 11},
-      line: 11
+      line: 16
     }]);
   });
 
+  it('defaults the tag indentation settings when they are not set', function() {
+    runTest('indentation/CorrectIndentationWithFeatureAndScenarioOverrides.feature', {
+      'Feature': 1,
+      'Scenario': 3
+    }, []);
+  });
+
+  it('observe tag indentation settings when they are overriden', function() {
+    runTest('indentation/CorrectIndentationWithScenarioTagOverrides.feature', {
+      'scenario tag': 3
+    }, []);
+  });
+
   // TODO: add tests for partial configurations and fallbacks (eg rule for Step is used for Given, Then etc is rule for Given, Then, etc has not been specified)
 });
diff --git a/test/rules/one-space-between-tags/NoViolations.feature b/test/rules/one-space-between-tags/NoViolations.feature
new file mode 100644
index 0000000..6db0e8c
--- /dev/null
+++ b/test/rules/one-space-between-tags/NoViolations.feature
@@ -0,0 +1,18 @@
+@featuretag1 @featuretag2
+@featuretag3
+Feature: Feature with good tag spacing
+
+Background:
+  Given I have a Background
+
+@scenariotag1 @scenariotag2
+@scenariotag3 @scenariotag4 @scenariotag5
+Scenario: This is a Scenario with good tag spacing
+  Then this is a then step
+
+@scenariotag5 @scenariotag6
+Scenario Outline: This is a Scenario Outline with good tag spacing
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/one-space-between-tags/Violations.feature b/test/rules/one-space-between-tags/Violations.feature
new file mode 100644
index 0000000..83c33a3
--- /dev/null
+++ b/test/rules/one-space-between-tags/Violations.feature
@@ -0,0 +1,18 @@
+@featuretag1  @featuretag2
+@featuretag3
+Feature: Feature with good tag spacing
+
+Background:
+  Given I have a Background
+
+@scenariotag1 @scenariotag2
+@scenariotag3   @scenariotag4  @scenariotag5
+Scenario: This is a Scenario with bad tag spacing
+  Then this is a then step
+
+@scenariotag5   @scenariotag6
+Scenario Outline: This is a Scenario Outline with bad tag spacing
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/one-space-between-tags/one-space-between-tags.js b/test/rules/one-space-between-tags/one-space-between-tags.js
new file mode 100644
index 0000000..aa512a7
--- /dev/null
+++ b/test/rules/one-space-between-tags/one-space-between-tags.js
@@ -0,0 +1,26 @@
+var ruleTestBase = require('../rule-test-base');
+var rule = require('../../../src/rules/one-space-between-tags.js');
+var runTest = ruleTestBase.createRuleTest(rule,
+  'There is more than one space between the tags <%= left %> and <%= right %>');
+
+describe('One space between tags rule', function() {
+  it('doesn\'t raise errors when there are no violations', function() {
+    runTest('one-space-between-tags/NoViolations.feature', {}, []);
+  });
+
+  it('detects errors for tags on features, scenarios, and scenario outlines', function() {
+    runTest('one-space-between-tags/Violations.feature', {}, [{
+      line: 1,
+      messageElements: {left: '@featuretag1', right: '@featuretag2'}
+    }, {
+      line: 9,
+      messageElements: {left: '@scenariotag3', right: '@scenariotag4'}
+    }, {
+      line: 9,
+      messageElements: {left: '@scenariotag4', right: '@scenariotag5'}
+    }, {
+      line: 13,
+      messageElements: {left: '@scenariotag5', right: '@scenariotag6'}
+    }]);
+  });
+});