@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,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
|
+
};
|