@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,81 @@
1
+ /*eslint no-console: "off"*/
2
+ require('core-js/stable/string');
3
+
4
+ const style = {
5
+ gray: function(text) {
6
+ return '\x1b[38;5;243m' + text + '\x1b[0m';
7
+ },
8
+
9
+ underline: function(text) {
10
+ return '\x1b[0;4m' + text + '\x1b[24m';
11
+ }
12
+
13
+ };
14
+
15
+ function stylizeFilePath(filePath) {
16
+ return style.underline(filePath);
17
+ }
18
+
19
+ function stylizeError(error, maxLineLength, maxMessageLength, addColors) {
20
+ const indent = ' '; // indent 2 spaces so it looks pretty
21
+ const padding = ' '; //padding of 4 spaces, will be used between line numbers, error msgs and rule names, for readability
22
+ const errorLinePadded = error.line.toString().padEnd(maxLineLength);
23
+ const errorLineStylized = addColors ? style.gray(errorLinePadded) : errorLinePadded;
24
+
25
+ const errorRuleStylized = addColors ? style.gray(error.rule) : error.rule;
26
+ return indent + errorLineStylized + padding + error.message.padEnd(maxMessageLength) + padding + errorRuleStylized;
27
+ }
28
+
29
+ function getMaxLineLength(result) {
30
+ let length = 0;
31
+ result.errors.forEach(error => {
32
+ const errorStr = error.line.toString();
33
+ if (errorStr.length > length) {
34
+ length = errorStr.length;
35
+ }
36
+ });
37
+ return length;
38
+ }
39
+
40
+ function getMaxMessageLength(result, maxLineLength, consoleWidth) {
41
+ let length = 0;
42
+ result.errors.forEach(error => {
43
+ const errorStr = error.message.toString();
44
+
45
+ // Get the length of the formatted error message when no extra padding is applied
46
+ // If the formatted message is longer than the console width, we will ignore its length
47
+ const expandedErrorStrLength = stylizeError(error, maxLineLength, 0, false).length;
48
+
49
+ if (errorStr.length > length && expandedErrorStrLength < consoleWidth) {
50
+ length = errorStr.length;
51
+ }
52
+ });
53
+
54
+ return length;
55
+ }
56
+
57
+ function printResults(results) {
58
+ // If the console is tty, get its width and use it to ensure we don't try to write messages longer
59
+ // than the console width when possible
60
+ let consoleWidth = Infinity;
61
+ if (process.stdout.isTTY) {
62
+ consoleWidth = process.stdout.columns;
63
+ }
64
+
65
+ results.forEach(result => {
66
+ if (result.errors.length > 0) {
67
+ const maxLineLength = getMaxLineLength(result);
68
+ const maxMessageLength = getMaxMessageLength(result, maxLineLength, consoleWidth);
69
+ console.error(stylizeFilePath(result.filePath));
70
+
71
+ result.errors.forEach(error => {
72
+ console.error(stylizeError(error, maxLineLength, maxMessageLength, true));
73
+ });
74
+ console.error('\n');
75
+ }
76
+ });
77
+ }
78
+
79
+ module.exports = {
80
+ printResults: printResults
81
+ };
@@ -0,0 +1,36 @@
1
+ function printResults(results) {
2
+ const testCases = results.map(result => ({
3
+ _attributes: {
4
+ name : result.filePath
5
+ },
6
+ error: result.errors.map(error => ({
7
+ _attributes: {
8
+ message: error.message,
9
+ type: 'gherkin-lint-error'
10
+ },
11
+ _cdata: `${result.filePath}:${error.line} (${error.rule}) ${error.message}`
12
+ }))
13
+ }));
14
+ const testSuiteReport = {
15
+ _declaration:{
16
+ _attributes: {
17
+ version: '1.0',
18
+ encoding: 'utf-8'
19
+ }
20
+ },
21
+ testsuite : {
22
+ _attributes: {
23
+ name: 'gherkin-lint',
24
+ },
25
+ testcase : testCases
26
+ }
27
+ };
28
+ let convert = require('xml-js');
29
+ const xunitXml = convert.js2xml(testSuiteReport, {compact: true, spaces: 4});
30
+ /*eslint no-console: "off"*/
31
+ console.error(xunitXml);
32
+ }
33
+
34
+ module.exports = {
35
+ printResults: printResults
36
+ };
package/src/linter.js ADDED
@@ -0,0 +1,140 @@
1
+ const _ = require('lodash');
2
+ const {generateMessages} = require('@cucumber/gherkin');
3
+ const {IdGenerator, SourceMediaType} = require('@cucumber/messages');
4
+ const fs = require('fs');
5
+ const rules = require('./rules.js');
6
+ const logger = require('./logger.js');
7
+
8
+ function readAndParseFile(filePath) {
9
+ return new Promise((resolve, reject) => {
10
+ const source = fs.readFileSync(filePath, 'utf8');
11
+ const options = {
12
+ includeGherkinDocument: true,
13
+ includePickles: false,
14
+ includeSource: false,
15
+ newId: IdGenerator.incrementing(),
16
+ };
17
+
18
+ try {
19
+ const envelopes = generateMessages(
20
+ source,
21
+ filePath,
22
+ SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN,
23
+ options
24
+ );
25
+ const parsingErrors = envelopes
26
+ .filter(envelope => envelope.parseError)
27
+ .map(envelope => envelope.parseError);
28
+
29
+ if (parsingErrors.length) {
30
+ reject(processFatalErrors(parsingErrors));
31
+ return;
32
+ }
33
+
34
+ const documentEnvelope = envelopes.find(envelope => envelope.gherkinDocument);
35
+ const file = {
36
+ relativePath: filePath,
37
+ lines: source.split(/\r\n|\r|\n/),
38
+ };
39
+ resolve({
40
+ feature: documentEnvelope ? documentEnvelope.gherkinDocument.feature : undefined,
41
+ file,
42
+ });
43
+ } catch (error) {
44
+ logger.error(`Gherkin emitted an error while parsing ${filePath}: ${error}`);
45
+ reject(processFatalErrors([error]));
46
+ }
47
+ });
48
+ }
49
+
50
+
51
+ function lint(files, configuration, additionalRulesDirs) {
52
+ let results = [];
53
+
54
+ return Promise.all(files.map((f) => {
55
+ let perFileErrors = [];
56
+
57
+ return readAndParseFile(f)
58
+ .then(
59
+ // Handle Promise.resolve
60
+ ({feature, file}) => {
61
+ perFileErrors = rules.runAllEnabledRules(feature, file, configuration, additionalRulesDirs);
62
+ },
63
+ // Handle Promise.reject
64
+ (parsingErrors) => {
65
+ perFileErrors = parsingErrors;
66
+ })
67
+ .finally(()=> {
68
+ let fileBlob = {
69
+ filePath: fs.realpathSync(f),
70
+ errors: _.sortBy(perFileErrors, 'line')
71
+ };
72
+
73
+ results.push(fileBlob);
74
+ });
75
+ })).then(() => results);
76
+ }
77
+
78
+ function processFatalErrors(errors) {
79
+ const firstError = errors[0];
80
+ const firstMessage = getErrorMessage(firstError);
81
+
82
+ // A tag before a Background causes cascading parser errors. Report the root
83
+ // cause once instead of emitting every recovery error that follows it.
84
+ if (firstMessage.includes('got \'Background') &&
85
+ firstMessage.includes('expected: #TagLine, #RuleLine')) {
86
+ return [{
87
+ message: 'Tags on Backgrounds are disallowed',
88
+ rule: 'no-tags-on-backgrounds',
89
+ line: getErrorLine(firstError)
90
+ }];
91
+ }
92
+
93
+ return errors.map(getFormattedFatalError);
94
+ }
95
+
96
+
97
+ /*eslint no-console: "off"*/
98
+ function getFormattedFatalError(error) {
99
+ const errorData = getErrorMessage(error);
100
+ const errorLine = getErrorLine(error);
101
+ let errorMsg;
102
+ let rule;
103
+ if (errorData.includes('got \'Background')) {
104
+ errorMsg = 'Multiple "Background" definitions in the same Feature or Rule are disallowed';
105
+ rule = 'up-to-one-background-per-scope';
106
+ } else if(errorData.includes('got \'Feature')) {
107
+ errorMsg = 'Multiple "Feature" definitions in the same file are disallowed';
108
+ rule = 'one-feature-per-file';
109
+ } else if (
110
+ errorData.includes('expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got') ||
111
+ errorData.includes('expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ExamplesLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got')
112
+ ) {
113
+ errorMsg = 'Steps should begin with "Given", "When", "Then", "And" or "But". Multiline steps are disallowed';
114
+ rule = 'no-multiline-steps';
115
+
116
+ } else {
117
+ errorMsg = errorData;
118
+ rule = 'unexpected-error';
119
+ }
120
+ return {message: errorMsg,
121
+ rule : rule,
122
+ line : errorLine};
123
+ }
124
+
125
+ function getErrorMessage(error) {
126
+ return error.message || error.data || String(error);
127
+ }
128
+
129
+ function getErrorLine(error) {
130
+ if (error.source && error.source.location) {
131
+ return error.source.location.line;
132
+ }
133
+ const match = getErrorMessage(error).match(/\((\d+):/);
134
+ return match ? Number(match[1]) : 0;
135
+ }
136
+
137
+ module.exports = {
138
+ lint: lint,
139
+ readAndParseFile: readAndParseFile
140
+ };
package/src/logger.js ADDED
@@ -0,0 +1,14 @@
1
+ /* eslint-disable no-console */
2
+
3
+ function boldError(msg) {
4
+ console.error(`\x1b[31m\x1b[1m${msg}\x1b[0m`);
5
+ }
6
+
7
+ function error(msg) {
8
+ console.error(`\x1b[31m${msg}\x1b[0m`);
9
+ }
10
+
11
+ module.exports = {
12
+ error: error,
13
+ boldError: boldError
14
+ };
package/src/main.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ const program = require('commander');
4
+ const linter = require('./linter.js');
5
+ const featureFinder = require('./feature-finder.js');
6
+ const configParser = require('./config-parser.js');
7
+ const logger = require('./logger.js');
8
+
9
+ function list(val) {
10
+ return val.split(',');
11
+ }
12
+
13
+ function collect(val, memo) {
14
+ memo.push(val);
15
+ return memo;
16
+ }
17
+
18
+ program
19
+ .usage('[options] <feature-files>')
20
+ .option('-f, --format [format]', 'output format. Possible values: json, stylish, xunit. Defaults to stylish')
21
+ .option('-i, --ignore <...>', 'comma seperated list of files/glob patterns that the linter should ignore, overrides ' + featureFinder.defaultIgnoreFileName + ' file', list)
22
+ .option('-c, --config [config]', 'configuration file, defaults to ' + configParser.defaultConfigFileName)
23
+ .option('-r, --rulesdir <...>', 'additional rule directories', collect, [])
24
+ .parse(process.argv);
25
+
26
+ const options = program.opts();
27
+ const additionalRulesDirs = options.rulesdir;
28
+ const files = featureFinder.getFeatureFiles(program.args, options.ignore);
29
+ const config = configParser.getConfiguration(options.config, additionalRulesDirs);
30
+ linter.lint(files, config, additionalRulesDirs)
31
+ .then((results) => {
32
+ printResults(results, options.format);
33
+ process.exit(getExitCode(results));
34
+ });
35
+
36
+ function getExitCode(results) {
37
+ let exitCode = 0;
38
+ results.forEach(result => {
39
+ if (result.errors.length > 0) {
40
+ exitCode = 1;
41
+ }
42
+ });
43
+ return exitCode;
44
+ }
45
+
46
+ function printResults(results, format) {
47
+ let formatter;
48
+ if (format === 'json') {
49
+ formatter = require('./formatters/json.js');
50
+ } else if (format === 'xunit') {
51
+ formatter = require('./formatters/xunit.js');
52
+ } else if (!format || format === 'stylish') {
53
+ formatter = require('./formatters/stylish.js');
54
+ } else {
55
+ logger.boldError('Unsupported format. The supported formats are json, xunit and stylish.');
56
+ process.exit(1);
57
+ }
58
+ formatter.printResults(results);
59
+ }
@@ -0,0 +1,55 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+ const rule = 'allowed-tags';
4
+
5
+ const availableConfigs = {
6
+ 'tags': [],
7
+ 'patterns': []
8
+ };
9
+
10
+ function run(feature, unused, configuration) {
11
+ if (!feature) {
12
+ return [];
13
+ }
14
+
15
+ let errors = [];
16
+ const allowedTags = configuration.tags;
17
+ const allowedPatterns = getAllowedPatterns(configuration);
18
+
19
+ gherkinUtils.getTaggableNodes(feature).forEach(node => {
20
+ checkTags(node, allowedTags, allowedPatterns, errors);
21
+ });
22
+
23
+ return errors;
24
+ }
25
+
26
+ function getAllowedPatterns(configuration) {
27
+ return (configuration.patterns || []).map((pattern) => new RegExp(pattern));
28
+ }
29
+
30
+ function checkTags(node, allowedTags, allowedPatterns, errors) {
31
+ return (node.tags || [])
32
+ .filter(tag => !isAllowed(tag, allowedTags, allowedPatterns))
33
+ .forEach(tag => {
34
+ errors.push(createError(node, tag));
35
+ });
36
+ }
37
+
38
+ function isAllowed(tag, allowedTags, allowedPatterns) {
39
+ return _.includes(allowedTags, tag.name)
40
+ || allowedPatterns.some((pattern) => pattern.test(tag.name));
41
+ }
42
+
43
+ function createError(node, tag) {
44
+ return {
45
+ message: 'Not allowed tag ' + tag.name + ' on ' + node.keyword,
46
+ rule : rule,
47
+ line : tag.location.line
48
+ };
49
+ }
50
+
51
+ module.exports = {
52
+ name: rule,
53
+ run: run,
54
+ availableConfigs: availableConfigs
55
+ };
@@ -0,0 +1,40 @@
1
+ const path = require('path');
2
+ const _ = require('lodash');
3
+
4
+ const rule = 'file-name';
5
+ const availableConfigs = {
6
+ 'style': 'PascalCase'
7
+ };
8
+ const checkers = {
9
+ 'PascalCase': filename => _.startCase(filename).replace(/ /g, ''),
10
+ 'Title Case': filename => _.startCase(filename),
11
+ 'camelCase': filename => _.camelCase(filename),
12
+ 'kebab-case': filename => _.kebabCase(filename),
13
+ 'snake_case': filename => _.snakeCase(filename)
14
+ };
15
+
16
+ function run(feature, file, configuration) {
17
+ if (!file) {
18
+ return [];
19
+ }
20
+ const {style} = _.merge(availableConfigs, configuration);
21
+ const filename = path.basename(file.relativePath, '.feature');
22
+ if (!checkers[style]) {
23
+ throw new Error('style "' + style + '" not supported for file-name rule');
24
+ }
25
+ const expected = checkers[style](filename);
26
+ if (filename === expected) {
27
+ return [];
28
+ }
29
+ return [{
30
+ message: `File names should be written in ${style} e.g. "${expected}.feature"`,
31
+ rule: rule,
32
+ line: 0
33
+ }];
34
+ }
35
+
36
+ module.exports = {
37
+ name: rule,
38
+ run: run,
39
+ availableConfigs: availableConfigs
40
+ };
@@ -0,0 +1,106 @@
1
+ const _ = require('lodash');
2
+ const gherkinUtils = require('./utils/gherkin.js');
3
+
4
+ const rule = 'indentation';
5
+ const defaultConfig = {
6
+ 'Feature': 0,
7
+ 'Background': 0,
8
+ 'Rule': 0,
9
+ 'Scenario': 0,
10
+ 'Step': 2,
11
+ 'Examples': 0,
12
+ 'example': 2,
13
+ 'given': 2,
14
+ 'when': 2,
15
+ 'then': 2,
16
+ 'and': 2,
17
+ 'but': 2
18
+ };
19
+
20
+ const availableConfigs = _.merge({}, defaultConfig, {
21
+ // The values here are unused by the config parsing logic.
22
+ 'feature tag': -1,
23
+ 'scenario tag': -1
24
+ });
25
+
26
+ function mergeConfiguration(configuration) {
27
+ let mergedConfiguration = _.merge({}, defaultConfig, configuration);
28
+ if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'feature tag')) {
29
+ mergedConfiguration['feature tag'] = mergedConfiguration['Feature'];
30
+ }
31
+ if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'scenario tag')) {
32
+ mergedConfiguration['scenario tag'] = mergedConfiguration['Scenario'];
33
+ }
34
+ return mergedConfiguration;
35
+ }
36
+
37
+ function run(feature, unused, configuration) {
38
+ if (!feature) {
39
+ return [];
40
+ }
41
+
42
+ let errors = [];
43
+ const mergedConfiguration = mergeConfiguration(configuration);
44
+
45
+ function test(parsedLocation, type) {
46
+ // location.column is 1 index based so, when we compare with the expected
47
+ // indentation we need to subtract 1
48
+ if (parsedLocation.column - 1 !== mergedConfiguration[type]) {
49
+ errors.push({
50
+ message: 'Wrong indentation for "' + type +
51
+ '", expected indentation level of ' + mergedConfiguration[type] +
52
+ ', but got ' + (parsedLocation.column - 1),
53
+ rule : rule,
54
+ line : parsedLocation.line
55
+ });
56
+ }
57
+ }
58
+
59
+ function testStep(step) {
60
+ let stepType = gherkinUtils.getLanguageInsitiveKeyword(step, feature.language);
61
+ stepType = stepType in configuration ? stepType : 'Step';
62
+ test(step.location, stepType);
63
+ }
64
+
65
+ function testTags(tags, type) {
66
+ _(tags).groupBy('location.line').forEach(tagLocationGroup => {
67
+ const firstTag = _(tagLocationGroup).sortBy('location.column').head();
68
+ test(firstTag.location, type);
69
+ });
70
+ }
71
+
72
+ test(feature.location, 'Feature');
73
+ testTags(feature.tags, 'feature tag');
74
+
75
+ feature.children.forEach(child => {
76
+ if (child.rule) {
77
+ test(child.rule.location, 'Rule');
78
+ } else if (child.background) {
79
+ test(child.background.location, 'Background');
80
+ child.background.steps.forEach(testStep);
81
+ } else {
82
+ test(child.scenario.location, 'Scenario');
83
+ testTags(child.scenario.tags, 'scenario tag');
84
+ child.scenario.steps.forEach(testStep);
85
+
86
+ child.scenario.examples.forEach(examples => {
87
+ test(examples.location, 'Examples');
88
+
89
+ if (examples.tableHeader) {
90
+ test(examples.tableHeader.location, 'example');
91
+ examples.tableBody.forEach(row => {
92
+ test(row.location, 'example');
93
+ });
94
+ }
95
+ });
96
+ }
97
+ });
98
+
99
+ return errors;
100
+ }
101
+
102
+ module.exports = {
103
+ name: rule,
104
+ run: run,
105
+ availableConfigs: availableConfigs
106
+ };
@@ -0,0 +1,65 @@
1
+ const gherkinUtils = require('./utils/gherkin.js');
2
+
3
+ const rule = 'keywords-in-logical-order';
4
+
5
+ function run(feature) {
6
+ if (!feature) {
7
+ return [];
8
+ }
9
+
10
+ let errors = [];
11
+
12
+ let simpleStepContainers = feature.children
13
+ .filter(child => child.background || child.scenario);
14
+
15
+ let ruleStepContainers = feature.children
16
+ .filter(child => child.rule)
17
+ .map(child => child.rule.children)
18
+ // .flat(); NOTE: flat not available until nodejs 11, which this project does not require
19
+ // using reduce() equivalent instead
20
+ // When revisiting wth nodejs 11, consider using flatmap()
21
+ .reduce((flattened, element) => flattened.concat(element), []);
22
+
23
+ let allStepContainers = simpleStepContainers.concat(ruleStepContainers);
24
+
25
+ allStepContainers.forEach(child => {
26
+ const node = child.background || child.scenario;
27
+
28
+ const keywordList = ['given', 'when', 'then'];
29
+
30
+ let maxKeywordPosition = undefined;
31
+
32
+ node.steps.forEach((step) => {
33
+ const keyword = gherkinUtils.getLanguageInsitiveKeyword(step, feature.language);
34
+ let keywordPosition = keywordList.indexOf(keyword);
35
+
36
+ if (keywordPosition === -1) {
37
+ // not found
38
+ return;
39
+ }
40
+
41
+ if (keywordPosition < maxKeywordPosition) {
42
+ let maxKeyword = keywordList[maxKeywordPosition];
43
+ errors.push(createError(step, maxKeyword));
44
+ }
45
+
46
+ maxKeywordPosition =
47
+ Math.max(maxKeywordPosition, keywordPosition) || keywordPosition;
48
+ });
49
+ });
50
+
51
+ return errors;
52
+ }
53
+
54
+ function createError(step, maxKeyword) {
55
+ return {
56
+ message: `Step "${step.keyword}${step.text}" should not appear after step using keyword ${maxKeyword}`,
57
+ rule: rule,
58
+ line: step.location.line,
59
+ };
60
+ }
61
+
62
+ module.exports = {
63
+ name: rule,
64
+ run: run,
65
+ };
@@ -0,0 +1,46 @@
1
+ const _ = require('lodash');
2
+ const rule = 'max-scenarios-per-file';
3
+
4
+ const defaultConfig = {
5
+ 'maxScenarios': 10,
6
+ 'countOutlineExamples': true
7
+ };
8
+
9
+ function run(feature, unused, config) {
10
+ if (!feature) {
11
+ return [];
12
+ }
13
+ let errors = [];
14
+ const mergedConfiguration = _.merge({}, defaultConfig, config);
15
+ const maxScenarios = mergedConfiguration.maxScenarios;
16
+ let count = feature.children.length;
17
+
18
+ feature.children.forEach(child => {
19
+ if (child.background) {
20
+ count = count - 1;
21
+ } else if (child.scenario.examples.length && mergedConfiguration.countOutlineExamples) {
22
+ count = count - 1;
23
+ child.scenario.examples.forEach(example => {
24
+ if (example.tableBody) {
25
+ count = count + example.tableBody.length;
26
+ }
27
+ });
28
+ }
29
+ });
30
+
31
+ if (count > maxScenarios) {
32
+ errors.push({
33
+ message: 'Number of scenarios exceeds maximum: ' + count + '/' + maxScenarios,
34
+ rule,
35
+ line: 0
36
+ });
37
+ }
38
+
39
+ return errors;
40
+ }
41
+
42
+ module.exports = {
43
+ name: rule,
44
+ run: run,
45
+ availableConfigs: defaultConfig
46
+ };