@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.
- package/LICENSE +13 -0
- package/README.md +299 -0
- package/package.json +157 -0
- package/src/config-parser.js +38 -0
- package/src/config-verifier.js +54 -0
- package/src/feature-finder.js +74 -0
- package/src/formatters/json.js +9 -0
- package/src/formatters/stylish.js +81 -0
- package/src/formatters/xunit.js +36 -0
- package/src/linter.js +140 -0
- package/src/logger.js +14 -0
- package/src/main.js +59 -0
- package/src/rules/allowed-tags.js +55 -0
- package/src/rules/file-name.js +40 -0
- package/src/rules/indentation.js +106 -0
- package/src/rules/keywords-in-logical-order.js +65 -0
- package/src/rules/max-scenarios-per-file.js +46 -0
- package/src/rules/name-length.js +57 -0
- package/src/rules/new-line-at-eof.js +40 -0
- package/src/rules/no-background-only-scenario.js +33 -0
- package/src/rules/no-dupe-feature-names.js +27 -0
- package/src/rules/no-dupe-scenario-names.js +60 -0
- package/src/rules/no-duplicate-tags.js +38 -0
- package/src/rules/no-empty-background.js +31 -0
- package/src/rules/no-empty-file.js +19 -0
- package/src/rules/no-examples-in-scenarios.js +28 -0
- package/src/rules/no-files-without-scenarios.js +29 -0
- package/src/rules/no-homogenous-tags.js +60 -0
- package/src/rules/no-multiple-empty-lines.js +18 -0
- package/src/rules/no-partially-commented-tag-lines.js +39 -0
- package/src/rules/no-restricted-patterns.js +125 -0
- package/src/rules/no-restricted-tags.js +58 -0
- package/src/rules/no-scenario-outlines-without-examples.js +30 -0
- package/src/rules/no-superfluous-tags.js +44 -0
- package/src/rules/no-trailing-spaces.js +22 -0
- package/src/rules/no-unnamed-features.js +20 -0
- package/src/rules/no-unnamed-scenarios.js +23 -0
- package/src/rules/no-unused-variables.js +108 -0
- package/src/rules/one-space-between-tags.js +41 -0
- package/src/rules/only-one-when.js +68 -0
- package/src/rules/required-tags.js +56 -0
- package/src/rules/scenario-size.js +46 -0
- package/src/rules/use-and.js +41 -0
- package/src/rules/utils/gherkin.js +91 -0
- 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
|
+
};
|