@ohlori/gherkin-lint 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +299 -0
  3. package/package.json +157 -0
  4. package/src/config-parser.js +38 -0
  5. package/src/config-verifier.js +54 -0
  6. package/src/feature-finder.js +74 -0
  7. package/src/formatters/json.js +9 -0
  8. package/src/formatters/stylish.js +81 -0
  9. package/src/formatters/xunit.js +36 -0
  10. package/src/linter.js +140 -0
  11. package/src/logger.js +14 -0
  12. package/src/main.js +59 -0
  13. package/src/rules/allowed-tags.js +55 -0
  14. package/src/rules/file-name.js +40 -0
  15. package/src/rules/indentation.js +106 -0
  16. package/src/rules/keywords-in-logical-order.js +65 -0
  17. package/src/rules/max-scenarios-per-file.js +46 -0
  18. package/src/rules/name-length.js +57 -0
  19. package/src/rules/new-line-at-eof.js +40 -0
  20. package/src/rules/no-background-only-scenario.js +33 -0
  21. package/src/rules/no-dupe-feature-names.js +27 -0
  22. package/src/rules/no-dupe-scenario-names.js +60 -0
  23. package/src/rules/no-duplicate-tags.js +38 -0
  24. package/src/rules/no-empty-background.js +31 -0
  25. package/src/rules/no-empty-file.js +19 -0
  26. package/src/rules/no-examples-in-scenarios.js +28 -0
  27. package/src/rules/no-files-without-scenarios.js +29 -0
  28. package/src/rules/no-homogenous-tags.js +60 -0
  29. package/src/rules/no-multiple-empty-lines.js +18 -0
  30. package/src/rules/no-partially-commented-tag-lines.js +39 -0
  31. package/src/rules/no-restricted-patterns.js +125 -0
  32. package/src/rules/no-restricted-tags.js +58 -0
  33. package/src/rules/no-scenario-outlines-without-examples.js +30 -0
  34. package/src/rules/no-superfluous-tags.js +44 -0
  35. package/src/rules/no-trailing-spaces.js +22 -0
  36. package/src/rules/no-unnamed-features.js +20 -0
  37. package/src/rules/no-unnamed-scenarios.js +23 -0
  38. package/src/rules/no-unused-variables.js +108 -0
  39. package/src/rules/one-space-between-tags.js +41 -0
  40. package/src/rules/only-one-when.js +68 -0
  41. package/src/rules/required-tags.js +56 -0
  42. package/src/rules/scenario-size.js +46 -0
  43. package/src/rules/use-and.js +41 -0
  44. package/src/rules/utils/gherkin.js +91 -0
  45. package/src/rules.js +60 -0
@@ -0,0 +1,57 @@
1
+ const _ = require('lodash');
2
+ const rule = 'name-length';
3
+
4
+ const availableConfigs = {
5
+ 'Feature': 70,
6
+ 'Rule': 70,
7
+ 'Step': 70,
8
+ 'Scenario': 70
9
+ };
10
+
11
+ let errors = [];
12
+
13
+ function test(name, location, configuration, type) {
14
+ if (name && (name.length > configuration[type])) {
15
+ errors.push({message: type + ' name is too long. Length of ' + name.length + ' is longer than the maximum allowed: ' + configuration[type],
16
+ rule : rule,
17
+ line : location.line});
18
+ }
19
+ }
20
+
21
+ function testSteps(node, mergedConfiguration) {
22
+ node.steps.forEach(step => {
23
+ // Check Step name length
24
+ test(step.text, step.location, mergedConfiguration, 'Step');
25
+ });
26
+ }
27
+
28
+ function run(feature, unused, configuration) {
29
+ if (!feature) {
30
+ return [];
31
+ }
32
+
33
+ errors = [];
34
+ const mergedConfiguration = _.merge(availableConfigs, configuration);
35
+
36
+ // Check Feature name length
37
+ test(feature.name, feature.location, mergedConfiguration, 'Feature');
38
+
39
+ feature.children.forEach(child => {
40
+ if (child.rule) {
41
+ test(child.rule.name, child.rule.location, mergedConfiguration, 'Rule');
42
+ } else if (child.background) {
43
+ testSteps(child.background, mergedConfiguration);
44
+ } else {
45
+ test(child.scenario.name, child.scenario.location, mergedConfiguration, 'Scenario');
46
+ testSteps(child.scenario, mergedConfiguration);
47
+ }
48
+ });
49
+
50
+ return errors;
51
+ }
52
+
53
+ module.exports = {
54
+ name: rule,
55
+ run: run,
56
+ availableConfigs: availableConfigs
57
+ };
@@ -0,0 +1,40 @@
1
+ const _ = require('lodash');
2
+ const logger = require('./../logger.js');
3
+
4
+ const rule = 'new-line-at-eof';
5
+ const availableConfigs = [
6
+ 'yes',
7
+ 'no'
8
+ ];
9
+
10
+ function run(unused, file, configuration) {
11
+ let errors = [];
12
+ if (_.indexOf(availableConfigs, configuration) === -1) {
13
+ logger.boldError(rule + ' requires an extra configuration value.\nAvailable configurations: ' + availableConfigs.join(', ') + '\nFor syntax please look at the documentation.');
14
+ process.exit(1);
15
+ }
16
+
17
+ const hasNewLineAtEOF = _.last(file.lines) === '';
18
+ let errormsg = '';
19
+ if (hasNewLineAtEOF && configuration === 'no') {
20
+ errormsg = 'New line at EOF(end of file) is not allowed';
21
+ } else if (!hasNewLineAtEOF && configuration === 'yes') {
22
+ errormsg = 'New line at EOF(end of file) is required';
23
+ }
24
+
25
+ if (errormsg !== '') {
26
+ errors.push({
27
+ message: errormsg,
28
+ rule : rule,
29
+ line : file.lines.length
30
+ });
31
+ }
32
+
33
+ return errors;
34
+ }
35
+
36
+ module.exports = {
37
+ name: rule,
38
+ run: run,
39
+ availableConfigs: availableConfigs
40
+ };
@@ -0,0 +1,33 @@
1
+ const rule = 'no-background-only-scenario';
2
+
3
+ function run(feature) {
4
+ if (!feature) {
5
+ return [];
6
+ }
7
+
8
+ let errors = [];
9
+
10
+ feature.children.forEach(child => {
11
+ if (child.background) {
12
+ if (feature.children.length <= 2) {
13
+ // as just one background is allowed, if there is a background in the feature,
14
+ // there must be at least, three elements in the feature to have, more than
15
+ // one scenario
16
+ errors.push(createError(child.background));
17
+ }
18
+ }
19
+ });
20
+ return errors;
21
+ }
22
+
23
+ function createError(background) {
24
+ return {
25
+ message: 'Backgrounds are not allowed when there is just one scenario.',
26
+ rule : rule,
27
+ line : background.location.line};
28
+ }
29
+
30
+ module.exports = {
31
+ name: rule,
32
+ run: run
33
+ };
@@ -0,0 +1,27 @@
1
+ const rule = 'no-dupe-feature-names';
2
+ const features = [];
3
+
4
+ function run(feature, file) {
5
+ if (!feature) {
6
+ return [];
7
+ }
8
+ let errors = [];
9
+ if (feature.name in features) {
10
+ const dupes = features[feature.name].files.join(', ');
11
+ features[feature.name].files.push(file.relativePath);
12
+ errors.push({
13
+ message: 'Feature name is already used in: ' + dupes,
14
+ rule : rule,
15
+ line : feature.location.line
16
+ });
17
+ } else {
18
+ features[feature.name] = {files: [file.relativePath]};
19
+ }
20
+
21
+ return errors;
22
+ }
23
+
24
+ module.exports = {
25
+ name: rule,
26
+ run: run
27
+ };
@@ -0,0 +1,60 @@
1
+ const rule = 'no-dupe-scenario-names';
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+ const availableConfigs = [
4
+ 'anywhere',
5
+ 'in-feature'
6
+ ];
7
+
8
+ let scenarios = [];
9
+
10
+ function run(feature, file, configuration) {
11
+ if (!feature) {
12
+ return [];
13
+ }
14
+
15
+ let errors = [];
16
+ if(configuration === 'in-feature') {
17
+ scenarios = [];
18
+ }
19
+
20
+ gherkinUtils.getScenarios(feature).forEach(scenario => {
21
+ if (scenario.name in scenarios) {
22
+ const dupes = getFileLinePairsAsStr(scenarios[scenario.name].locations);
23
+
24
+ scenarios[scenario.name].locations.push({
25
+ file: file.relativePath,
26
+ line: scenario.location.line
27
+ });
28
+
29
+ errors.push({
30
+ message: 'Scenario name is already used in: ' + dupes,
31
+ rule : rule,
32
+ line : scenario.location.line});
33
+ } else {
34
+ scenarios[scenario.name] = {
35
+ locations: [
36
+ {
37
+ file: file.relativePath,
38
+ line: scenario.location.line
39
+ }
40
+ ]
41
+ };
42
+ }
43
+ });
44
+
45
+ return errors;
46
+ }
47
+
48
+ function getFileLinePairsAsStr(objects) {
49
+ let strings = [];
50
+ objects.forEach(object => {
51
+ strings.push(object.file + ':' + object.line);
52
+ });
53
+ return strings.join(', ');
54
+ }
55
+
56
+ module.exports = {
57
+ name: rule,
58
+ run: run,
59
+ availableConfigs: availableConfigs
60
+ };
@@ -0,0 +1,38 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'no-duplicate-tags';
5
+
6
+ function run(feature) {
7
+ if (!feature) {
8
+ return [];
9
+ }
10
+ let errors = [];
11
+
12
+ gherkinUtils.getTaggableNodes(feature).forEach(node => {
13
+ verifyTags(node, errors);
14
+ });
15
+ return errors;
16
+ }
17
+
18
+ function verifyTags(node, errors) {
19
+ const failedTagNames = [];
20
+ const uniqueTagNames = [];
21
+ node.tags.forEach(tag => {
22
+ if (!_.includes(failedTagNames, tag.name)) {
23
+ if (_.includes(uniqueTagNames, tag.name)) {
24
+ errors.push({message: 'Duplicate tags are not allowed: ' + tag.name,
25
+ rule : rule,
26
+ line : tag.location.line});
27
+ failedTagNames.push(tag.name);
28
+ } else {
29
+ uniqueTagNames.push(tag.name);
30
+ }
31
+ }
32
+ });
33
+ }
34
+
35
+ module.exports = {
36
+ name: rule,
37
+ run: run
38
+ };
@@ -0,0 +1,31 @@
1
+ const rule = 'no-empty-background';
2
+
3
+ function run(feature) {
4
+ if (!feature) {
5
+ return [];
6
+ }
7
+
8
+ let errors = [];
9
+
10
+ feature.children.forEach(child => {
11
+ if (child.background) {
12
+ if (child.background.steps.length === 0) {
13
+ errors.push(createError(child.background));
14
+ }
15
+ }
16
+ });
17
+ return errors;
18
+ }
19
+
20
+ function createError(background) {
21
+ return {
22
+ message: 'Empty backgrounds are not allowed.',
23
+ rule : rule,
24
+ line : background.location.line
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ name: rule,
30
+ run: run
31
+ };
@@ -0,0 +1,19 @@
1
+ const _ = require('lodash');
2
+ const rule = 'no-empty-file';
3
+
4
+ function run(feature) {
5
+ let errors = [];
6
+ if (_.isEmpty(feature)) {
7
+ errors.push({
8
+ message: 'Empty feature files are disallowed',
9
+ rule : rule,
10
+ line : 1
11
+ });
12
+ }
13
+ return errors;
14
+ }
15
+
16
+ module.exports = {
17
+ name: rule,
18
+ run: run
19
+ };
@@ -0,0 +1,28 @@
1
+ const gherkinUtils = require('./utils/gherkin.js');
2
+ const rule = 'no-examples-in-scenarios';
3
+
4
+ function run(feature) {
5
+ if (!feature) {
6
+ return [];
7
+ }
8
+ let errors = [];
9
+ feature.children.forEach((child) => {
10
+ if (child.scenario) {
11
+ const nodeType = gherkinUtils.getNodeType(child.scenario, feature.language);
12
+
13
+ if (nodeType == 'Scenario' && child.scenario.examples.length) {
14
+ errors.push({
15
+ message: 'Cannot use "Examples" in a "Scenario", use a "Scenario Outline" instead',
16
+ rule : rule,
17
+ line : child.scenario.location.line,
18
+ });
19
+ }
20
+ }
21
+ });
22
+ return errors;
23
+ }
24
+
25
+ module.exports = {
26
+ name: rule,
27
+ run: run,
28
+ };
@@ -0,0 +1,29 @@
1
+ const rule = 'no-files-without-scenarios';
2
+
3
+ function filterScenarios(child) {
4
+ if (child.scenario != undefined) return true;
5
+
6
+ if (child.rule == undefined) return false;
7
+
8
+ return child.rule.children.some(filterScenarios);
9
+ }
10
+
11
+ function run(feature) {
12
+ if (!feature) {
13
+ return [];
14
+ }
15
+ let errors = [];
16
+ if (!feature.children.some(filterScenarios)) {
17
+ errors.push({
18
+ message: 'Feature file does not have any Scenarios',
19
+ rule: rule,
20
+ line: 1
21
+ });
22
+ }
23
+ return errors;
24
+ }
25
+
26
+ module.exports = {
27
+ name: rule,
28
+ run: run
29
+ };
@@ -0,0 +1,60 @@
1
+ const _ = require('lodash');
2
+
3
+ const rule = 'no-homogenous-tags';
4
+
5
+ function run(feature) {
6
+ if (!feature) {
7
+ return [];
8
+ }
9
+ let errors = [];
10
+
11
+ // Tags that exist in every scenario and scenario outline
12
+ // should be applied on a feature level
13
+ let childrenTags = [];
14
+
15
+ feature.children.forEach(child => {
16
+ if (child.scenario) {
17
+ let scenario = child.scenario;
18
+
19
+ childrenTags.push(getTagNames(scenario));
20
+
21
+ let exampleTags = [];
22
+ scenario.examples.forEach(example => {
23
+ exampleTags.push(getTagNames(example));
24
+ });
25
+
26
+ const homogenousExampleTags = _.intersection(...exampleTags);
27
+ if (homogenousExampleTags.length) {
28
+ errors.push({
29
+ message: 'All Examples of a Scenario Outline have the same tag(s), ' +
30
+ 'they should be defined on the Scenario Outline instead: ' +
31
+ homogenousExampleTags.join(', '),
32
+ rule: rule,
33
+ line: scenario.location.line
34
+ });
35
+ }
36
+ }
37
+ });
38
+
39
+ const homogenousTags = _.intersection(...childrenTags);
40
+ if (homogenousTags.length) {
41
+ errors.push({
42
+ message: 'All Scenarios on this Feature have the same tag(s), ' +
43
+ 'they should be defined on the Feature instead: ' +
44
+ homogenousTags.join(', '),
45
+ rule : rule,
46
+ line : feature.location.line
47
+ });
48
+ }
49
+
50
+ return errors;
51
+ }
52
+
53
+ function getTagNames(node) {
54
+ return _.map(node.tags, tag => tag.name);
55
+ }
56
+
57
+ module.exports = {
58
+ name: rule,
59
+ run: run
60
+ };
@@ -0,0 +1,18 @@
1
+ const rule = 'no-multiple-empty-lines';
2
+
3
+ function run(unused, file) {
4
+ let errors = [];
5
+ for (let i = 0; i < file.lines.length - 1; i++) {
6
+ if (file.lines[i].trim() === '' && file.lines[i + 1].trim() == '') {
7
+ errors.push({message: 'Multiple empty lines are not allowed',
8
+ rule : rule,
9
+ line : i + 2});
10
+ }
11
+ }
12
+ return errors;
13
+ }
14
+
15
+ module.exports = {
16
+ name: rule,
17
+ run: run
18
+ };
@@ -0,0 +1,39 @@
1
+ const rule = 'no-partially-commented-tag-lines';
2
+
3
+ function run(feature) {
4
+ if (!feature) {
5
+ return [];
6
+ }
7
+
8
+ let errors = [];
9
+
10
+ checkTags(feature, errors);
11
+ feature.children.forEach(child => {
12
+ if (child.scenario) {
13
+ checkTags(child.scenario, errors);
14
+
15
+ child.scenario.examples.forEach(example => {
16
+ checkTags(example, errors);
17
+ });
18
+ }
19
+ });
20
+
21
+ return errors;
22
+ }
23
+
24
+ function checkTags(node, errors) {
25
+ node.tags.forEach(tag => {
26
+ if (tag.name.indexOf('#') > 0) {
27
+ errors.push({
28
+ message: 'Partially commented tag lines not allowed',
29
+ rule : rule,
30
+ line : tag.location.line
31
+ });
32
+ }
33
+ });
34
+ }
35
+
36
+ module.exports = {
37
+ name: rule,
38
+ run: run
39
+ };
@@ -0,0 +1,125 @@
1
+ const gherkinUtils = require('./utils/gherkin.js');
2
+ const rule = 'no-restricted-patterns';
3
+
4
+ const availableConfigs = {
5
+ 'Global': [],
6
+ 'Scenario': [],
7
+ 'ScenarioOutline': [],
8
+ 'Background': [],
9
+ 'Feature': []
10
+ };
11
+
12
+
13
+ function run(feature, unused, configuration) {
14
+ if (!feature) {
15
+ return [];
16
+ }
17
+ let errors = [];
18
+ const restrictedPatterns = getRestrictedPatterns(configuration);
19
+ const language = feature.language;
20
+
21
+ // Check the feature itself
22
+ checkNameAndDescription(feature, restrictedPatterns, language, errors);
23
+
24
+ // Check the feature children
25
+ feature.children.forEach(child => {
26
+ let node = child.background || child.scenario;
27
+ checkNameAndDescription(node, restrictedPatterns, language, errors);
28
+
29
+ // And all the steps of each child
30
+ node.steps.forEach(step => {
31
+ checkStepNode(step, node, restrictedPatterns, language, errors);
32
+ });
33
+ });
34
+
35
+ return errors;
36
+ }
37
+
38
+
39
+ function getRestrictedPatterns(configuration) {
40
+ // Patterns applied to everything; feature, scenarios, etc.
41
+ let globalPatterns = (configuration.Global || []).map(pattern => new RegExp(pattern, 'i'));
42
+
43
+ let restrictedPatterns = {};
44
+ Object.keys(availableConfigs).forEach(key => {
45
+ const resolvedKey = key.toLowerCase().replace(/ /g, '');
46
+ const resolvedConfig = (configuration[key] || []);
47
+
48
+ restrictedPatterns[resolvedKey] = resolvedConfig.map(pattern => new RegExp(pattern, 'i'));
49
+
50
+ restrictedPatterns[resolvedKey] = restrictedPatterns[resolvedKey].concat(globalPatterns);
51
+ });
52
+
53
+ return restrictedPatterns;
54
+ }
55
+
56
+
57
+ function getRestrictedPatternsForNode(node, restrictedPatterns, language) {
58
+ let key = gherkinUtils.getLanguageInsitiveKeyword(node, language).toLowerCase();
59
+
60
+ return restrictedPatterns[key];
61
+ }
62
+
63
+
64
+ function checkNameAndDescription(node, restrictedPatterns, language, errors) {
65
+ getRestrictedPatternsForNode(node, restrictedPatterns, language)
66
+ .forEach(pattern => {
67
+ check(node, 'name', pattern, language, errors);
68
+ check(node, 'description', pattern, language, errors);
69
+ });
70
+ }
71
+
72
+
73
+ function checkStepNode(node, parentNode, restrictedPatterns, language, errors) {
74
+ // Use the node keyword of the parent to determine which rule configuration to use
75
+ getRestrictedPatternsForNode(parentNode, restrictedPatterns, language)
76
+ .forEach(pattern => {
77
+ check(node, 'text', pattern, language, errors);
78
+ });
79
+ }
80
+
81
+
82
+ function check(node, property, pattern, language, errors) {
83
+ if (!node[property]) {
84
+ return;
85
+ }
86
+
87
+ let strings = [node[property]];
88
+ const type = gherkinUtils.getNodeType(node, language);
89
+
90
+ if (property == 'description') {
91
+ // Descriptions can be multiline, in which case the description will contain escapted
92
+ // newline characters "\n". If a multiline description matches one of the restricted patterns
93
+ // when the error message gets printed in the console, it will break the message into multiple lines.
94
+ // So let's split the description on newline chars and test each line separately.
95
+
96
+ // To make sure we don't accidentally pick up a doubly escaped new line "\\n" which would appear
97
+ // if a user wrote the string "\n" in a description, let's replace all escaped new lines
98
+ // with a sentinel, split lines and then restore the doubly escaped new line
99
+ const escapedNewLineSentinel = '<!gherkin-lint new line sentinel!>';
100
+ const escapedNewLine = '\\n';
101
+ strings = node[property]
102
+ .replace(escapedNewLine, escapedNewLineSentinel)
103
+ .split('\n')
104
+ .map(string => string.replace(escapedNewLineSentinel, escapedNewLine));
105
+ }
106
+
107
+ for (let i = 0; i < strings.length; i++) {
108
+ // We use trim() on the examined string because names and descriptions can contain
109
+ // white space before and after, unlike steps
110
+ if (strings[i].trim().match(pattern)) {
111
+ errors.push({
112
+ message: `${type} ${property}: "${strings[i].trim()}" matches restricted pattern "${pattern}"`,
113
+ rule: rule,
114
+ line: node.location.line
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+
121
+ module.exports = {
122
+ name: rule,
123
+ run: run,
124
+ availableConfigs: availableConfigs
125
+ };
@@ -0,0 +1,58 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'no-restricted-tags';
5
+ const availableConfigs = {
6
+ 'tags': [],
7
+ 'patterns': []
8
+ };
9
+
10
+
11
+ function run(feature, unused, configuration) {
12
+ if (!feature) {
13
+ return [];
14
+ }
15
+
16
+ const forbiddenTags = configuration.tags;
17
+ const forbiddenPatterns = getForbiddenPatterns(configuration);
18
+ const language = feature.language;
19
+ let errors = [];
20
+
21
+ gherkinUtils.getTaggableNodes(feature).forEach(node => {
22
+ checkTags(node, language, forbiddenTags, forbiddenPatterns, errors);
23
+ });
24
+
25
+ return errors;
26
+ }
27
+
28
+
29
+ function getForbiddenPatterns(configuration) {
30
+ return (configuration.patterns || []).map((pattern) => new RegExp(pattern));
31
+ }
32
+
33
+
34
+ function checkTags(node, language, forbiddenTags, forbiddenPatterns, errors) {
35
+ const nodeType = gherkinUtils.getNodeType(node, language);
36
+ node.tags.forEach(tag => {
37
+ if (isForbidden(tag, forbiddenTags, forbiddenPatterns)) {
38
+ errors.push({
39
+ message: `Forbidden tag ${tag.name} on ${nodeType}`,
40
+ rule : rule,
41
+ line : tag.location.line
42
+ });
43
+ }
44
+ });
45
+ }
46
+
47
+
48
+ function isForbidden(tag, forbiddenTags, forbiddenPatterns) {
49
+ return _.includes(forbiddenTags, tag.name)
50
+ || forbiddenPatterns.some((pattern) => pattern.test(tag.name));
51
+ }
52
+
53
+
54
+ module.exports = {
55
+ name: rule,
56
+ run: run,
57
+ availableConfigs: availableConfigs
58
+ };