Repository: vsiakka/gherkin-lint
Pull request #79 (2017-08-24 15:30:53)

Add no-superfluous-tags and no-homogenous-tags rules
As promised, two new rules for tags.

I'm not sure about the no-homogenous-tags name - but I can't think of anything better - unless you've got a good idea?


Patch:
diff --git a/README.md b/README.md
index 7aadef3..8aad378 100644
--- a/README.md
+++ b/README.md
@@ -24,28 +24,31 @@ Or check this:
 
 ## Available rules
 
-| Name                                        | Functionality                                              |
-|---------------------------------------------|------------------------------------------------------------|
-| `no-tags-on-backgrounds` *                  | Disallows tags on Background                               |
-| `one-feature-per-file` *                    | Disallows multiple Feature definitions in the same file    |
-| `up-to-one-background-per-file` *           | Disallows multiple Background definition in the same file  |
-| &nbsp;                                      |                                                            |
-| [`indentation`](#indentation)               | Allows the user to specify indentation rules               |
-| [`name-length`](#name-length)               | Allows restricting length of Feature/Scenario/Step names   |
-| [`new-line-at-eof`](#new-line-at-eof)       | Disallows/enforces new line at EOF                         |
-| `no-dupe-feature-names`                     | Disallows duplicate Feature names                          |
-| `no-dupe-scenario-names`                    | Disallows duplicate Scenario names                         |
-| `no-duplicate-tags`                         | Disallows duplicate tags on the same Feature or Scenario   |
-| `no-empty-file`                             | Disallows empty feature files                              |
-| `no-files-without-scenarios`                | Disallows files with no scenarios                          |
-| `no-multiple-empty-lines`                   | Disallows multiple empty lines                             |
-| `no-partially-commented-tag-lines`          | Disallows partially commented tag lines                    |
-| [`no-restricted-tags`](#no-restricted-tags) | Disallow use of particular @tags                           |
-| `no-scenario-outlines-without-examples`     | Disallows scenario outlines without examples               |
-| `no-trailing-spaces`                        | Disallows trailing spaces                                  |
-| `no-unnamed-features`                       | Disallows empty Feature name                               |
-| `no-unnamed-scenarios`                      | Disallows empty Scenario name                              |
-| `use-and`                                   | Disallows repeated step names requiring use of And instead |
+
+| Name                                        | Functionality                                                                            |
+|---------------------------------------------|------------------------------------------------------------------------------------------|
+| `no-tags-on-backgrounds` *                  | Disallows tags on Background                                                             |
+| `one-feature-per-file` *                    | Disallows multiple Feature definitions in the same file                                  |
+| `up-to-one-background-per-file` *           | Disallows multiple Background definition in the same file                                |
+| &nbsp;                                      |                                                                                          |
+| [`indentation`](#indentation)               | Allows the user to specify indentation rules                                             |
+| [`name-length`](#name-length)               | Allows restricting length of Feature/Scenario/Step names                                 |
+| [`new-line-at-eof`](#new-line-at-eof)       | Disallows/enforces new line at EOF                                                       |
+| `no-dupe-feature-names`                     | Disallows duplicate Feature names                                                        |
+| `no-dupe-scenario-names`                    | Disallows duplicate Scenario names                                                       |
+| `no-duplicate-tags`                         | Disallows duplicate tags on the same Feature or Scenario                                 |
+| `no-empty-file`                             | Disallows empty feature files                                                            |
+| `no-files-without-scenarios`                | Disallows files with no scenarios                                                        |
+| `no-homogenous-tags`                        | Disallows tags present on every Scenario in a Feature, rather than on the Feature itself |
+| `no-multiple-empty-lines`                   | Disallows multiple empty lines                                                           |
+| `no-partially-commented-tag-lines`          | Disallows partially commented tag lines                                                  |
+| [`no-restricted-tags`](#no-restricted-tags) | Disallow use of particular @tags                                                         |
+| `no-scenario-outlines-without-examples`     | Disallows scenario outlines without examples                                             |
+| `no-superfluous-tags`                       | Disallows tags present on a Feature and a Scenario in that Feature                       |
+| `no-trailing-spaces`                        | Disallows trailing spaces                                                                |
+| `no-unnamed-features`                       | Disallows empty Feature name                                                             |
+| `no-unnamed-scenarios`                      | Disallows empty Scenario name                                                            |
+| `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.
 
diff --git a/package.json b/package.json
index b810b59..3809f56 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
     "lodash": "4.13.1"
   },
   "devDependencies": {
-    "chai": "3.5.0",
+    "chai": "4.1.1",
     "eslint": "2.13.1",
     "mocha": "2.5.3",
     "mocha-sinon": "1.1.5",
diff --git a/src/rules/no-homogenous-tags.js b/src/rules/no-homogenous-tags.js
new file mode 100644
index 0000000..86ede30
--- /dev/null
+++ b/src/rules/no-homogenous-tags.js
@@ -0,0 +1,36 @@
+var _ = require('lodash');
+
+var rule = 'no-homogenous-tags';
+
+function noHomogenousTags(feature) {
+  var errors = [];
+  if(feature.children !== undefined) {
+    var tagNames = _.flatten(_.map(feature.children, function(child) {
+      if ((child.type === 'Scenario' || child.type === 'ScenarioOutline') &&
+          child.tags !== undefined) {
+        return [_.map(child.tags, function(tag) {
+          return tag.name;
+        })];
+      } else {
+        return [];
+      }
+    }));
+
+    var homogenousTags = _.intersection.apply(_, tagNames);
+    if (homogenousTags.length !== 0) {
+      // You could argue that the line number should be the first instance of a
+      // bad tag, but I think this is really a problem with the whole feature.
+      errors = [{message: 'All Scenarios on this Feature have the same tag(s), ' +
+                            'they should be defined on the Feature instead: ' +
+                            _.join(homogenousTags, ', '),
+                   rule   : rule,
+                   line   : feature.location.line}];
+    }
+  }
+  return errors;
+}
+
+module.exports = {
+  name: rule,
+  run: noHomogenousTags
+};
diff --git a/src/rules/no-superfluous-tags.js b/src/rules/no-superfluous-tags.js
new file mode 100644
index 0000000..42b4605
--- /dev/null
+++ b/src/rules/no-superfluous-tags.js
@@ -0,0 +1,31 @@
+var _ = require('lodash');
+
+var rule = 'no-superfluous-tags';
+
+function noSuperfluousTags(feature) {
+  var errors = [];
+  if(feature.tags !== undefined && feature.children !== undefined) {
+    _.forEach(feature.children, function(child) {
+      if (child.tags !== undefined) {
+        var superfluousTags = _.intersectionWith(child.tags, feature.tags, function(lhs, rhs) {
+          return lhs.name === rhs.name;
+        });
+        if (superfluousTags.length !== 0) {
+          var superfluousTagNames = _.map(superfluousTags, function(tag) {
+            return tag.name;
+          });
+          errors.push({message: 'Tag(s) duplicated on a Feature and a Scenario in that Feature: ' +
+                                _.join(superfluousTagNames, ', '),
+                       rule   : rule,
+                       line   : superfluousTags[0].location.line});
+        }
+      }
+    });
+  }
+  return errors;
+}
+
+module.exports = {
+  name: rule,
+  run: noSuperfluousTags
+};


Test patch:
diff --git a/test-data-wip/.gherkin-lintrc b/test-data-wip/.gherkin-lintrc
index b833b50..5260b96 100644
--- a/test-data-wip/.gherkin-lintrc
+++ b/test-data-wip/.gherkin-lintrc
@@ -14,5 +14,7 @@
   "name-length": ["on", {"Feature": 50}],
   "no-restricted-tags": ["on", {"tags": ["@watch", "@wip"]}],
   "use-and": "on",
-  "no-duplicate-tags": "on"
+  "no-duplicate-tags": "on",
+  "no-superfluous-tags": "on",
+  "no-homogenous-tags": "on"
 }
diff --git a/test-data-wip/NoHomogenousTags.feature b/test-data-wip/NoHomogenousTags.feature
new file mode 100644
index 0000000..eef034e
--- /dev/null
+++ b/test-data-wip/NoHomogenousTags.feature
@@ -0,0 +1,16 @@
+Feature: Test for the no-homogenous-tags
+
+Background:
+  Given I have a Feature file with homogenous tags
+
+@homogenousTag
+Scenario: This is a Scenario with some tags
+  Then this is a then step
+
+@homogenousTag
+Scenario Outline: This is a Scenario with the same tags
+  Then this is a then step
+
+  Examples:
+  | Example |
+  | Another Example |
diff --git a/test-data-wip/NoSuperfluousTags.feature b/test-data-wip/NoSuperfluousTags.feature
new file mode 100644
index 0000000..cef8278
--- /dev/null
+++ b/test-data-wip/NoSuperfluousTags.feature
@@ -0,0 +1,9 @@
+@superfluousTag
+Feature: Test for the no-superfluous-tags
+
+Background:
+  Given I have a Feature file with superfluous tags
+
+@superfluousTag
+Scenario: This is a Scenario with the same tag as a feature
+  Then I should see a no-superfluous-tags
diff --git a/test/rules/no-homogenous-tags/test-data/NoViolations.feature b/test/rules/no-homogenous-tags/test-data/NoViolations.feature
new file mode 100644
index 0000000..e0aaa28
--- /dev/null
+++ b/test/rules/no-homogenous-tags/test-data/NoViolations.feature
@@ -0,0 +1,16 @@
+@featuretag1 @featuretag2
+Feature: Feature with multiple tags
+
+Background:
+  Given I have a Background
+
+@tag1 @tag2 @featuretag1
+Scenario: This is a Scenario with multiple tags
+  Then this is a then step
+
+@tag3 @tag4
+Scenario Outline: This is a Scenario Outline with multiple tags
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/no-homogenous-tags/test-data/Violations.feature b/test/rules/no-homogenous-tags/test-data/Violations.feature
new file mode 100644
index 0000000..06a1143
--- /dev/null
+++ b/test/rules/no-homogenous-tags/test-data/Violations.feature
@@ -0,0 +1,15 @@
+Feature: Feature with homogenous tags
+
+Background:
+  Given I have a Background
+
+@tag1 @tag2 @tag3
+Scenario: This is a Scenario with some tags
+  Then this is a then step
+
+@tag1 @tag2 @tag4
+Scenario Outline: This is a Scenario Outline with the same tags
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/no-homogenous-tags/test-results/results.js b/test/rules/no-homogenous-tags/test-results/results.js
new file mode 100644
index 0000000..903947d
--- /dev/null
+++ b/test/rules/no-homogenous-tags/test-results/results.js
@@ -0,0 +1,8 @@
+module.exports = {
+  'noHomogenousTags':[
+    { line: 1,
+      message: 'All Scenarios on this Feature have the same tag(s), they should be defined on the Feature instead: @tag1, @tag2',
+      rule: 'no-homogenous-tags'
+    }
+  ]
+};
diff --git a/test/rules/no-homogenous-tags/test.js b/test/rules/no-homogenous-tags/test.js
new file mode 100644
index 0000000..04c9984
--- /dev/null
+++ b/test/rules/no-homogenous-tags/test.js
@@ -0,0 +1,33 @@
+var assert = require('chai').assert;
+var expect = require('chai').expect;
+var rule = require('../../../src/rules/no-homogenous-tags.js');
+var Gherkin = require('gherkin');
+var parser = new Gherkin.Parser();
+var fs = require('fs');
+
+var expectedResults = require('./test-results/results.js');
+
+require('mocha-sinon');
+
+function getErrors(fileName, configuration) {
+  var file = fs.readFileSync('test/rules/no-homogenous-tags/test-data/' + fileName, 'utf8');
+  var parsedFile = parser.parse(file).feature;
+  return rule.run(parsedFile, undefined, configuration);
+}
+
+describe('No Homogenous Tags Rule', function() {
+  it('doesn\'t raise errors when there are no violations', function() {
+    var configuration = ['on'];
+    var fileName = 'NoViolations.feature';
+    var errors = getErrors(fileName, configuration);
+    expect(errors).to.be.empty;
+  });
+
+  it('detects errors for scenarios, and scenario outlines', function() {
+    var configuration = ['on'];
+    var fileName = 'Violations.feature';
+    var errors = getErrors(fileName, configuration);
+    assert.sameDeepMembers(errors, expectedResults.noHomogenousTags);
+  });
+
+});
diff --git a/test/rules/no-superfluous-tags/test-data/NoViolations.feature b/test/rules/no-superfluous-tags/test-data/NoViolations.feature
new file mode 100644
index 0000000..ec64348
--- /dev/null
+++ b/test/rules/no-superfluous-tags/test-data/NoViolations.feature
@@ -0,0 +1,16 @@
+@featuretag1 @featuretag2
+Feature: Feature with multiple tags
+
+Background:
+  Given I have a Background
+
+@scenariotag1 @scenariotag2
+Scenario: This is a Scenario with multiple tags
+  Then this is a then step
+
+@scenariotag1 @scenariotag3
+Scenario Outline: This is a Scenario Outline with multiple tags
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/no-superfluous-tags/test-data/Violations.feature b/test/rules/no-superfluous-tags/test-data/Violations.feature
new file mode 100644
index 0000000..d802a33
--- /dev/null
+++ b/test/rules/no-superfluous-tags/test-data/Violations.feature
@@ -0,0 +1,16 @@
+@superfluoustag1 @superfluoustag2 @featuretag
+Feature: Feature with multiple superfluous tags
+
+Background:
+  Given I have a Background
+
+@superfluoustag1 @scenariotag1 @scenariotag2
+Scenario: This is a Scenario with superfluous tags
+  Then this is a then step
+
+@superfluoustag1 @superfluoustag2 @scenariotag1 @scenariotag3
+Scenario Outline: This is a Scenario Outline with superfluous tags
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/no-superfluous-tags/test-results/results.js b/test/rules/no-superfluous-tags/test-results/results.js
new file mode 100644
index 0000000..e73cdb1
--- /dev/null
+++ b/test/rules/no-superfluous-tags/test-results/results.js
@@ -0,0 +1,12 @@
+module.exports = {
+  'noSuperfluousTags':[
+    { line: 7,
+      message: 'Tag(s) duplicated on a Feature and a Scenario in that Feature: @superfluoustag1',
+      rule: 'no-superfluous-tags'
+    },
+    { line: 11,
+      message: 'Tag(s) duplicated on a Feature and a Scenario in that Feature: @superfluoustag1, @superfluoustag2',
+      rule: 'no-superfluous-tags'
+    }
+  ]
+};
diff --git a/test/rules/no-superfluous-tags/test.js b/test/rules/no-superfluous-tags/test.js
new file mode 100644
index 0000000..ecea2b0
--- /dev/null
+++ b/test/rules/no-superfluous-tags/test.js
@@ -0,0 +1,33 @@
+var assert = require('chai').assert;
+var expect = require('chai').expect;
+var rule = require('../../../src/rules/no-superfluous-tags.js');
+var Gherkin = require('gherkin');
+var parser = new Gherkin.Parser();
+var fs = require('fs');
+
+var expectedResults = require('./test-results/results.js');
+
+require('mocha-sinon');
+
+function getErrors(fileName, configuration) {
+  var file = fs.readFileSync('test/rules/no-superfluous-tags/test-data/' + fileName, 'utf8');
+  var parsedFile = parser.parse(file).feature;
+  return rule.run(parsedFile, undefined, configuration);
+}
+
+describe('No Superfluous Tags Rule', function() {
+  it('doesn\'t raise errors when there are no violations', function() {
+    var configuration = ['on'];
+    var fileName = 'NoViolations.feature';
+    var errors = getErrors(fileName, configuration);
+    expect(errors).to.be.empty;
+  });
+
+  it('detects errors for scenarios, and scenario outlines', function() {
+    var configuration = ['on'];
+    var fileName = 'Violations.feature';
+    var errors = getErrors(fileName, configuration);
+    assert.sameDeepMembers(errors, expectedResults.noSuperfluousTags);
+  });
+
+});