Repository: vsiakka/gherkin-lint
Pull request #76 (2017-08-15 17:25:55)

Add no-duplicate-tags rule
I needed this custom rule, but it seemed generic enough that you might want it.  It prevents duplicate tags on a feature or scenario.

I have a couple more slightly less generic rules which I can raise PRs for as well, if you'd like them?  They are:
1. Failing if the same tag is specified on a feature and on one of it's scenarios (no-unnecessary-tags?)
2. Failing if the same tag is specified on every scenario in a feature (it should be feature level instead).

Simon  


Patch:
diff --git a/README.md b/README.md
index 36c27d2..7aadef3 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ Or check this:
 | [`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                             |
@@ -65,8 +66,8 @@ will turn on the `no-unnamed-features` rule.
 - Expected indentation for Feature, Background, Scenario: 0 spaces
 - Expected indentation for Steps: 2 spaces
 
-You can override the defaults for `indentation` like this:  
-`Step` will be used as a fallback if the keyword of the step is not specified.  
+You can override the defaults for `indentation` like this:
+`Step` will be used as a fallback if the keyword of the step is not specified.
 This feature is able to handle all localizations of the gherkin steps.
 ```
 {
@@ -112,7 +113,7 @@ The default is 70 characters for each of these:
 ```
 
 ## Configuration File
-The default name for the configuration file is `.gherkin-lintrc` and it's expected to be in your working directory.     
+The default name for the configuration file is `.gherkin-lintrc` and it's expected to be in your working directory.
 
 If you are using a file with a different name or a file in a different folder, you will need to specify the `-c` or `--config` option and pass in the relative path to your configuration file. Eg: `gherkin-lint -c path/to/configuration/file.extention`
 
diff --git a/src/rules/no-duplicate-tags.js b/src/rules/no-duplicate-tags.js
new file mode 100644
index 0000000..e91d499
--- /dev/null
+++ b/src/rules/no-duplicate-tags.js
@@ -0,0 +1,40 @@
+var _ = require('lodash');
+
+var rule = 'no-duplicate-tags';
+
+function noDuplicateTags(feature) {
+  var errors = [];
+  errors = errors.concat(verifyTags(feature.tags, feature.location));
+  if(feature.children !== undefined) {
+    _.forEach(feature.children, function(child) {
+      errors = errors.concat(verifyTags(child.tags, child.location));
+    });
+  }
+  return errors;
+}
+
+function verifyTags(tags, location) {
+  var errors = [],
+    failedTagNames = [],
+    uniqueTagNames = [];
+  if (tags !== undefined && location !== undefined) {
+    _.forEach(tags, function(tag) {
+      if (!_.includes(failedTagNames, tag.name)) {
+        if (_.includes(uniqueTagNames, tag.name)) {
+          errors.push({message: 'Duplicate tags are not allowed: ' + tag.name,
+                       rule   : rule,
+                       line   : tag.location.line});
+          failedTagNames.push(tag.name);
+        } else  {
+          uniqueTagNames.push(tag.name);
+        }
+      }
+    });
+  }
+  return errors;
+}
+
+module.exports = {
+  name: rule,
+  run: noDuplicateTags
+};


Test patch:
diff --git a/test-data-wip/.gherkin-lintrc b/test-data-wip/.gherkin-lintrc
index 8063258..b833b50 100644
--- a/test-data-wip/.gherkin-lintrc
+++ b/test-data-wip/.gherkin-lintrc
@@ -13,5 +13,6 @@
   "no-scenario-outlines-without-examples": "on",
   "name-length": ["on", {"Feature": 50}],
   "no-restricted-tags": ["on", {"tags": ["@watch", "@wip"]}],
-  "use-and": "on"
+  "use-and": "on",
+  "no-duplicate-tags": "on"
 }
diff --git a/test-data-wip/DuplicateTags.feature b/test-data-wip/DuplicateTags.feature
new file mode 100644
index 0000000..d954235
--- /dev/null
+++ b/test-data-wip/DuplicateTags.feature
@@ -0,0 +1,6 @@
+@featuretag @featuretag
+Feature: Test for duplicate tags
+
+@scenariotag @scenariotag
+Scenario: This is a Scenario with duplicate tags
+  Then I should see a no-duplicate-tags
diff --git a/test/rules/no-duplicate-tags/test-data/NoViolations.feature b/test/rules/no-duplicate-tags/test-data/NoViolations.feature
new file mode 100644
index 0000000..ec64348
--- /dev/null
+++ b/test/rules/no-duplicate-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-duplicate-tags/test-data/Violations.feature b/test/rules/no-duplicate-tags/test-data/Violations.feature
new file mode 100644
index 0000000..63cc53c
--- /dev/null
+++ b/test/rules/no-duplicate-tags/test-data/Violations.feature
@@ -0,0 +1,16 @@
+@featuretag @featuretag @anothertag
+Feature: Feature with multiple duplicate tags
+
+Background:
+  Given I have a Background
+
+@scenariotag @scenariotag @scenariotag @anothertag
+Scenario: This is a Scenario with three duplicate tags
+  Then this is a then step
+
+@scenariotag @scenariotag
+Scenario Outline: This is a Scenario Outline with two duplicate tags
+  Then this is a then step
+Examples:
+  | foo |
+  | bar |
diff --git a/test/rules/no-duplicate-tags/test-results/results.js b/test/rules/no-duplicate-tags/test-results/results.js
new file mode 100644
index 0000000..3ab792b
--- /dev/null
+++ b/test/rules/no-duplicate-tags/test-results/results.js
@@ -0,0 +1,16 @@
+module.exports = {
+  'wrongLength':[
+    { message: 'Duplicate tags are not allowed: @featuretag',
+      rule: 'no-duplicate-tags',
+      line: 1
+    },
+    { message: 'Duplicate tags are not allowed: @scenariotag',
+      rule: 'no-duplicate-tags',
+      line: 7
+    },
+    { message: 'Duplicate tags are not allowed: @scenariotag',
+      rule: 'no-duplicate-tags',
+      line: 11
+    }
+  ]
+};
diff --git a/test/rules/no-duplicate-tags/test.js b/test/rules/no-duplicate-tags/test.js
new file mode 100644
index 0000000..1f9dc81
--- /dev/null
+++ b/test/rules/no-duplicate-tags/test.js
@@ -0,0 +1,33 @@
+var assert = require('chai').assert;
+var expect = require('chai').expect;
+var rule = require('../../../src/rules/no-duplicate-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-duplicate-tags/test-data/' + fileName, 'utf8');
+  var parsedFile = parser.parse(file).feature;
+  return rule.run(parsedFile, undefined, configuration);
+}
+
+describe('No Duplicate 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 features, scenarios, and scenario outlines', function() {
+    var configuration = ['on'];
+    var fileName = 'Violations.feature';
+    var errors = getErrors(fileName, configuration);
+    assert.sameDeepMembers(errors, expectedResults.wrongLength);
+  });
+
+});