@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,30 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+ const rule = 'no-scenario-outlines-without-examples';
4
+
5
+ function run(feature) {
6
+ if (!feature) {
7
+ return [];
8
+ }
9
+
10
+ let errors = [];
11
+ feature.children.forEach(child => {
12
+ if (child.scenario) {
13
+ const scenario = child.scenario;
14
+ const nodeType = gherkinUtils.getNodeType(scenario, feature.language);
15
+ if (nodeType === 'Scenario Outline' && (!_.find(scenario.examples, 'tableBody') || !_.find(scenario.examples, 'tableBody')['tableBody'].length)) {
16
+ errors.push({
17
+ message: 'Scenario Outline does not have any Examples',
18
+ rule : rule,
19
+ line : scenario.location.line
20
+ });
21
+ }
22
+ }
23
+ });
24
+ return errors;
25
+ }
26
+
27
+ module.exports = {
28
+ name: rule,
29
+ run: run
30
+ };
@@ -0,0 +1,44 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'no-superfluous-tags';
5
+
6
+ function run(feature) {
7
+ if (!feature) {
8
+ return [];
9
+ }
10
+
11
+ let errors = [];
12
+
13
+ feature.children.forEach(child => {
14
+ const node = child.rule || child.background || child.scenario;
15
+ checkTags(node, feature, feature.language, errors);
16
+
17
+ if (node.examples) {
18
+ node.examples.forEach(example => {
19
+ checkTags(example, feature, feature.language, errors);
20
+ checkTags(example, node, feature.language, errors);
21
+ });
22
+ }
23
+ });
24
+ return errors;
25
+ }
26
+
27
+ function checkTags(child, parent, language, errors) {
28
+ const superfluousTags = _.intersectionBy(child.tags, parent.tags, 'name');
29
+ const childType = gherkinUtils.getNodeType(child, language);
30
+ const parentType = gherkinUtils.getNodeType(parent, language);
31
+
32
+ superfluousTags.forEach(tag => {
33
+ errors.push({
34
+ message: `Tag duplication between ${childType} and its corresponding ${parentType}: ${tag.name}`,
35
+ rule : rule,
36
+ line : tag.location.line
37
+ });
38
+ });
39
+ }
40
+
41
+ module.exports = {
42
+ name: rule,
43
+ run: run
44
+ };
@@ -0,0 +1,22 @@
1
+ const rule = 'no-trailing-spaces';
2
+
3
+ function run(unused, file) {
4
+ let errors = [];
5
+ let lineNo = 1;
6
+ file.lines.forEach(line => {
7
+ if (/[\t ]+$/.test(line)) {
8
+ errors.push({message: 'Trailing spaces are not allowed',
9
+ rule : rule,
10
+ line : lineNo});
11
+ }
12
+
13
+ lineNo++;
14
+ });
15
+
16
+ return errors;
17
+ }
18
+
19
+ module.exports = {
20
+ name: rule,
21
+ run: run
22
+ };
@@ -0,0 +1,20 @@
1
+ const rule = 'no-unnamed-features';
2
+
3
+ function run(feature) {
4
+ let errors = [];
5
+
6
+ if (!feature || !feature.name) {
7
+ const location = feature ? feature.location.line : 0;
8
+ errors.push({
9
+ message: 'Missing Feature name',
10
+ rule : rule,
11
+ line : location
12
+ });
13
+ }
14
+ return errors;
15
+ }
16
+
17
+ module.exports = {
18
+ name: rule,
19
+ run: run
20
+ };
@@ -0,0 +1,23 @@
1
+ const rule = 'no-unnamed-scenarios';
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ function run(feature) {
5
+ if (!feature) {
6
+ return [];
7
+ }
8
+ let errors = [];
9
+ gherkinUtils.getScenarios(feature).forEach(scenario => {
10
+ if (!scenario.name) {
11
+ errors.push({
12
+ message: 'Missing Scenario name',
13
+ rule : rule,
14
+ line : scenario.location.line});
15
+ }
16
+ });
17
+ return errors;
18
+ }
19
+
20
+ module.exports = {
21
+ name: rule,
22
+ run: run
23
+ };
@@ -0,0 +1,108 @@
1
+ const rule = 'no-unused-variables';
2
+
3
+ function run(feature) {
4
+ if(!feature) {
5
+ return [];
6
+ }
7
+
8
+ let errors = [];
9
+ const stepVariableRegex = /<([^>]*)>/gu;
10
+
11
+ feature.children.forEach(child => {
12
+ if (!child.scenario) {
13
+ // Variables are a feature of Scenarios (as of Gherkin 9?) and Scenario Outlines only
14
+ return;
15
+ }
16
+
17
+ const examples = child.scenario.examples;
18
+
19
+ if (!examples.length) {
20
+ // If there is no examples table, the rule doesn't apply
21
+ return;
22
+ }
23
+
24
+ // Maps of variableName -> lineNo
25
+ const examplesVariables = {};
26
+ const scenarioVariables = {};
27
+ let match;
28
+
29
+ // Collect all the entries of the examples table
30
+ examples.forEach(example => {
31
+ if (example.tableHeader && example.tableHeader.cells) {
32
+ example.tableHeader.cells.forEach(cell => {
33
+ if (cell.value) {
34
+ examplesVariables[cell.value] = cell.location.line;
35
+ }
36
+ });
37
+ }
38
+ });
39
+
40
+
41
+ // Collect the variables used in the scenario outline
42
+
43
+ // Scenario names can include variables
44
+ while ((match = stepVariableRegex.exec(child.scenario.name)) != null) {
45
+ scenarioVariables[match[1]] = child.scenario.location.line;
46
+ }
47
+
48
+
49
+ child.scenario.steps.forEach(step => {
50
+
51
+ // Steps can take arguments and their argument can include variables.
52
+ // The arguments can be of type:
53
+ // - DocString
54
+ // - DataTable
55
+ // For more details, see https://docs.cucumber.io/gherkin/reference/#step-arguments
56
+
57
+ // Collect variables from step arguments
58
+ if (step.dataTable) {
59
+ step.dataTable.rows.forEach(row => {
60
+ row.cells.forEach(cell => {
61
+ if (cell.value) {
62
+ while ((match = stepVariableRegex.exec(cell.value)) != null) {
63
+ scenarioVariables[match[1]] = cell.location.line;
64
+ }
65
+ }
66
+ });
67
+ });
68
+ } else if (step.docString) {
69
+ while ((match = stepVariableRegex.exec(step.docString.content)) != null) {
70
+ scenarioVariables[match[1]] = step.location.line;
71
+ }
72
+ }
73
+
74
+ // Collect variables from the steps themselves
75
+ while ((match = stepVariableRegex.exec(step.text)) != null) {
76
+ scenarioVariables[match[1]] = step.location.line;
77
+ }
78
+ });
79
+
80
+
81
+ for (const exampleVariable in examplesVariables) {
82
+ if (!scenarioVariables[exampleVariable]) {
83
+ errors.push({
84
+ message: 'Examples table variable "' + exampleVariable + '" is not used in any step',
85
+ rule : rule,
86
+ line : examplesVariables[exampleVariable]
87
+ });
88
+ }
89
+ }
90
+
91
+ for (const scenarioVariable in scenarioVariables) {
92
+ if (!examplesVariables[scenarioVariable]) {
93
+ errors.push({
94
+ message: 'Step variable "' + scenarioVariable + '" does not exist in the examples table',
95
+ rule : rule,
96
+ line : scenarioVariables[scenarioVariable]
97
+ });
98
+ }
99
+ }
100
+ });
101
+
102
+ return errors;
103
+ }
104
+
105
+ module.exports = {
106
+ name: rule,
107
+ run: run
108
+ };
@@ -0,0 +1,41 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'one-space-between-tags';
5
+
6
+ function run(feature) {
7
+ if (!feature) {
8
+ return;
9
+ }
10
+ let errors = [];
11
+
12
+ gherkinUtils.getTaggableNodes(feature).forEach(node => {
13
+ testTags(node, errors);
14
+ });
15
+
16
+ return errors;
17
+ }
18
+
19
+ function testTags(node, errors) {
20
+ _(node.tags)
21
+ .groupBy('location.line')
22
+ .sortBy('location.column')
23
+ .forEach(tags => {
24
+ _.range(tags.length - 1)
25
+ .map(i => {
26
+ if (tags[i].location.column + tags[i].name.length < tags[i + 1].location.column - 1) {
27
+ errors.push({
28
+ line: tags[i].location.line,
29
+ rule: rule,
30
+ message: 'There is more than one space between the tags ' +
31
+ tags[i].name + ' and ' + tags[i + 1].name
32
+ });
33
+ }
34
+ });
35
+ });
36
+ }
37
+
38
+ module.exports = {
39
+ run: run,
40
+ name: rule
41
+ };
@@ -0,0 +1,68 @@
1
+ const gherkinUtils = require('./utils/gherkin.js');
2
+
3
+ const rule = 'only-one-when';
4
+
5
+ function run(feature) {
6
+ if (!feature) {
7
+ return [];
8
+ }
9
+
10
+ let errors = [];
11
+
12
+ function getScenarios(featureChildren) {
13
+ let simpleScenarios = featureChildren
14
+ .filter(child => child.scenario)
15
+ .map(child => child.scenario);
16
+
17
+ let ruleScenarios = featureChildren
18
+ .filter(child => child.rule)
19
+ .map(child => child.rule.children)
20
+ .reduce((flattened, element) => flattened.concat(element), [])
21
+ .filter(grandchild => grandchild.scenario)
22
+ .map(grandchild => grandchild.scenario);
23
+
24
+ return simpleScenarios.concat(ruleScenarios);
25
+ }
26
+
27
+ let scenarios = getScenarios(feature.children);
28
+
29
+ scenarios.forEach(scenario => {
30
+ let lastRealKeyword = '';
31
+ let whenCount = 0;
32
+ let firstViolationLine = 0;
33
+
34
+ scenario.steps.forEach(step => {
35
+ let keyword = gherkinUtils.getLanguageInsitiveKeyword(step, feature.language);
36
+ if (keyword === 'when' || (keyword === 'and' && lastRealKeyword === 'when')) {
37
+ lastRealKeyword = 'when';
38
+ whenCount++;
39
+ if (whenCount > 1 && firstViolationLine === 0) {
40
+ firstViolationLine = step.location.line;
41
+ }
42
+
43
+ } else {
44
+ lastRealKeyword = keyword;
45
+ }
46
+ });
47
+
48
+ if (whenCount > 1) {
49
+ errors.push(createError(scenario, whenCount, firstViolationLine));
50
+ }
51
+ });
52
+
53
+ return errors;
54
+ }
55
+
56
+
57
+ function createError(scenario, whenCount, lineNumber) {
58
+ return {
59
+ message: `Scenario "${scenario.name}" contains ${whenCount} When statements (max 1)`,
60
+ rule: rule,
61
+ line: lineNumber,
62
+ };
63
+ }
64
+
65
+ module.exports = {
66
+ name: rule,
67
+ run: run,
68
+ };
@@ -0,0 +1,56 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'required-tags';
5
+ const availableConfigs = {
6
+ tags: [],
7
+ ignoreUntagged: true
8
+ };
9
+
10
+
11
+ function checkTagExists(requiredTag, ignoreUntagged, scenarioTags, scenarioType, scenarioLine) {
12
+ const result = (ignoreUntagged && scenarioTags.length == 0)
13
+ || scenarioTags.some((tagObj) => RegExp(requiredTag).test(tagObj.name));
14
+ if (!result) {
15
+ return {
16
+ message: `No tag found matching ${requiredTag} for ${scenarioType}`,
17
+ rule,
18
+ line: scenarioLine
19
+ };
20
+ }
21
+ return result;
22
+ }
23
+
24
+ function run(feature, unused, config) {
25
+ if (!feature) {
26
+ return [];
27
+ }
28
+
29
+ const mergedConfig = _.merge({}, availableConfigs, config);
30
+
31
+ let errors = [];
32
+ feature.children.forEach((child) => {
33
+ if (child.scenario) {
34
+ const type = gherkinUtils.getNodeType(child.scenario, feature.language);
35
+ const line = child.scenario.location.line;
36
+
37
+ // Check each Scenario for the required tags
38
+ const requiredTagErrors = mergedConfig.tags
39
+ .map((requiredTag) => checkTagExists(requiredTag, mergedConfig.ignoreUntagged, child.scenario.tags || [], type, line))
40
+ .filter((item) =>
41
+ typeof item === 'object' && item.message
42
+ );
43
+
44
+ // Update errors
45
+ errors = errors.concat(requiredTagErrors);
46
+ }
47
+ });
48
+
49
+ return errors;
50
+ }
51
+
52
+ module.exports = {
53
+ name: rule,
54
+ run: run,
55
+ availableConfigs: availableConfigs
56
+ };
@@ -0,0 +1,46 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'scenario-size';
5
+ const availableConfigs = {
6
+ 'steps-length': {
7
+ 'Rule': 15,
8
+ 'Background': 15,
9
+ 'Scenario': 15
10
+ }
11
+ };
12
+
13
+ function run(feature, unused, configuration) {
14
+ if (!feature) {
15
+ return;
16
+ }
17
+
18
+ if (_.isEmpty(configuration)) {
19
+ configuration = availableConfigs;
20
+ }
21
+
22
+ let errors = [];
23
+ feature.children.forEach((child) => {
24
+ const node = child.rule || child.background || child.scenario;
25
+ const nodeType = gherkinUtils.getNodeType(node, feature.language);
26
+ const configKey = child.rule ? 'Rule' : child.background ? 'Background' : 'Scenario';
27
+ const maxSize = configuration['steps-length'][configKey];
28
+
29
+ if (maxSize && node.steps.length > maxSize) {
30
+ errors.push({
31
+ message: `Element ${nodeType} too long: actual ${node.steps.length}, expected ${maxSize}`,
32
+ rule : 'scenario-size',
33
+ line : node.location.line
34
+ });
35
+ }
36
+ });
37
+
38
+ return errors;
39
+ }
40
+
41
+
42
+ module.exports = {
43
+ name: rule,
44
+ run: run,
45
+ availableConfigs: availableConfigs
46
+ };
@@ -0,0 +1,41 @@
1
+ const gherkinUtils = require('./utils/gherkin.js');
2
+
3
+ const rule = 'use-and';
4
+
5
+ function run(feature) {
6
+ if (!feature) {
7
+ return [];
8
+ }
9
+
10
+ let errors = [];
11
+
12
+ gherkinUtils.getStepContainers(feature).forEach(node => {
13
+ let previousKeyword = undefined;
14
+ node.steps.forEach(step => {
15
+ const keyword = gherkinUtils.getLanguageInsitiveKeyword(step, feature.language);
16
+
17
+ if (keyword === 'and') {
18
+ return;
19
+ }
20
+ if (keyword === previousKeyword) {
21
+ errors.push(createError(step));
22
+ }
23
+
24
+ previousKeyword = keyword;
25
+ });
26
+ });
27
+ return errors;
28
+ }
29
+
30
+ function createError(step) {
31
+ return {
32
+ message: 'Step "' + step.keyword + step.text + '" should use And instead of ' + step.keyword,
33
+ rule : rule,
34
+ line : step.location.line
35
+ };
36
+ }
37
+
38
+ module.exports = {
39
+ name: rule,
40
+ run: run
41
+ };
@@ -0,0 +1,91 @@
1
+ const _ = require('lodash');
2
+ const {dialects} = require('@cucumber/gherkin');
3
+
4
+ // We use the node's keyword to determine the node's type
5
+ // because it's the only way to distinguish a scenario with a scenario outline
6
+ function getNodeType(node, language) {
7
+ const key = getLanguageInsitiveKeyword(node, language).toLowerCase();
8
+ const stepKeys = [
9
+ 'given',
10
+ 'when',
11
+ 'then',
12
+ 'and',
13
+ 'but',
14
+ ];
15
+
16
+ if (key === 'feature') {
17
+ return 'Feature';
18
+ } else if (key === 'rule') {
19
+ return 'Rule';
20
+ } else if (key === 'background') {
21
+ return 'Background';
22
+ } else if (key === 'scenario') {
23
+ return 'Scenario';
24
+ } else if (key === 'scenariooutline') {
25
+ return 'Scenario Outline';
26
+ } else if (key === 'examples') {
27
+ return 'Examples';
28
+ } else if (stepKeys.includes(key)) {
29
+ return 'Step';
30
+ }
31
+ return '';
32
+ }
33
+
34
+
35
+ function getLanguageInsitiveKeyword(node, language) {
36
+ const languageMapping = dialects[language];
37
+
38
+ return _.findKey(languageMapping, values => values instanceof Array && values.includes(node.keyword));
39
+ }
40
+
41
+ function getRuleChildren(feature) {
42
+ return (feature.children || [])
43
+ .filter(child => child.rule)
44
+ .reduce((children, child) => children.concat(child.rule.children || []), []);
45
+ }
46
+
47
+ function getAllChildren(feature) {
48
+ return (feature.children || []).concat(getRuleChildren(feature));
49
+ }
50
+
51
+ function getRules(feature) {
52
+ return (feature.children || [])
53
+ .filter(child => child.rule)
54
+ .map(child => child.rule);
55
+ }
56
+
57
+ function getScenarios(feature) {
58
+ return getAllChildren(feature)
59
+ .filter(child => child.scenario)
60
+ .map(child => child.scenario);
61
+ }
62
+
63
+ function getBackgrounds(feature) {
64
+ return getAllChildren(feature)
65
+ .filter(child => child.background)
66
+ .map(child => child.background);
67
+ }
68
+
69
+ function getStepContainers(feature) {
70
+ return getBackgrounds(feature).concat(getScenarios(feature));
71
+ }
72
+
73
+ function getTaggableNodes(feature) {
74
+ const nodes = [feature].concat(getRules(feature), getScenarios(feature));
75
+ getScenarios(feature).forEach(scenario => {
76
+ nodes.push(...(scenario.examples || []));
77
+ });
78
+ return nodes;
79
+ }
80
+
81
+
82
+ module.exports = {
83
+ getNodeType: getNodeType,
84
+ getLanguageInsitiveKeyword: getLanguageInsitiveKeyword,
85
+ getAllChildren: getAllChildren,
86
+ getRules: getRules,
87
+ getScenarios: getScenarios,
88
+ getBackgrounds: getBackgrounds,
89
+ getStepContainers: getStepContainers,
90
+ getTaggableNodes: getTaggableNodes,
91
+ };
package/src/rules.js ADDED
@@ -0,0 +1,60 @@
1
+ // Operations on rules
2
+ const glob = require('glob');
3
+ const path = require('path');
4
+
5
+ function getAllRules(additionalRulesDirs) {
6
+ let rules = {};
7
+
8
+ const rulesDirs = [
9
+ path.join(__dirname, 'rules')
10
+ ].concat(additionalRulesDirs || []);
11
+
12
+ rulesDirs.forEach(rulesDir => {
13
+ rulesDir = path.resolve(rulesDir);
14
+ glob.sync(`${rulesDir}/*.js`).forEach(file => {
15
+ const rule = require(file);
16
+ rules[rule.name] = rule;
17
+ });
18
+ });
19
+ return rules;
20
+ }
21
+
22
+ function getRule(rule, additionalRulesDirs) {
23
+ return getAllRules(additionalRulesDirs)[rule];
24
+ }
25
+
26
+ function doesRuleExist(rule, additionalRulesDirs) {
27
+ return getRule(rule, additionalRulesDirs) !== undefined;
28
+ }
29
+
30
+ function isRuleEnabled(ruleConfig) {
31
+ if (Array.isArray(ruleConfig)) {
32
+ return ruleConfig[0] === 'on';
33
+ }
34
+ return ruleConfig === 'on';
35
+ }
36
+
37
+ function runAllEnabledRules(feature, file, configuration, additionalRulesDirs) {
38
+ let errors = [];
39
+ const rules = getAllRules(additionalRulesDirs);
40
+ Object.keys(rules).forEach(ruleName => {
41
+ let rule = rules[ruleName];
42
+ if (isRuleEnabled(configuration[rule.name])) {
43
+ const ruleConfig = Array.isArray(configuration[rule.name]) ? configuration[rule.name][1] : {};
44
+ const error = rule.run(feature, file, ruleConfig);
45
+ if (error) {
46
+ errors = errors.concat(error);
47
+ }
48
+ }
49
+ });
50
+ return errors;
51
+ }
52
+
53
+
54
+ module.exports = {
55
+ doesRuleExist: doesRuleExist,
56
+ isRuleEnabled: isRuleEnabled,
57
+ runAllEnabledRules: runAllEnabledRules,
58
+ getRule: getRule,
59
+ getAllRules: getAllRules
60
+ };