gherkin-lint
Advanced tools
| 'use strict'; | ||
| var fs = require('fs'); | ||
| var verifyConfig = require('./config-verifier.js'); | ||
| var defaultConfigFileName = '.gherkin-lintrc'; | ||
| function getConfiguration(configPath, additionalRulesDirs) { | ||
| if (configPath) { | ||
| if (!fs.existsSync(configPath)) { | ||
| throw new Error('Could not find specified config file "' + configPath + '"'); | ||
| } | ||
| } else { | ||
| if (!fs.existsSync(defaultConfigFileName)) { | ||
| throw new Error('Could not find default config file "' + defaultConfigFileName + '" in the working ' + 'directory. To use a custom name/location provide the config file using the "-c" arg'); | ||
| } | ||
| configPath = defaultConfigFileName; | ||
| } | ||
| var config = JSON.parse(fs.readFileSync(configPath)); | ||
| var errors = verifyConfig(config, additionalRulesDirs); | ||
| if (errors.length > 0) { | ||
| console.error('\x1b[31m\x1b[1mError(s) in configuration file:\x1b[0m'); // eslint-disable-line no-console | ||
| errors.forEach(function (error) { | ||
| console.error('\x1b[31m- ' + error + '\x1b[0m'); // eslint-disable-line no-console | ||
| }); | ||
| throw new Error('Configuration error(s)'); | ||
| } | ||
| return config; | ||
| } | ||
| module.exports = { | ||
| getConfiguration: getConfiguration, | ||
| defaultConfigFileName: defaultConfigFileName | ||
| }; |
| 'use strict'; | ||
| var rules = require('./rules.js'); | ||
| function verifyConfigurationFile(config, additionalRulesDirs) { | ||
| var errors = []; | ||
| for (var rule in config) { | ||
| if (!rules.doesRuleExist(rule, additionalRulesDirs)) { | ||
| errors.push('Rule "' + rule + '" does not exist'); | ||
| } else { | ||
| verifyRuleConfiguration(rule, config[rule], errors); | ||
| } | ||
| } | ||
| return errors; | ||
| } | ||
| function verifyRuleConfiguration(rule, ruleConfig, errors) { | ||
| var enablingSettings = ['on', 'off']; | ||
| var genericErrorMsg = 'Invalid rule configuration for "' + rule + '" - '; | ||
| if (Array.isArray(ruleConfig)) { | ||
| if (enablingSettings.indexOf(ruleConfig[0]) === -1) { | ||
| errors.push(genericErrorMsg + 'The first part of the config should be "on" or "off"'); | ||
| } | ||
| if (ruleConfig.length != 2) { | ||
| errors.push(genericErrorMsg + ' The config should only have 2 parts.'); | ||
| } | ||
| var ruleObj = rules.getRule(rule); | ||
| var isValidSubConfig; | ||
| if (typeof ruleConfig[1] === 'string') { | ||
| isValidSubConfig = function isValidSubConfig(availableConfigs, subConfig) { | ||
| return ruleObj.availableConfigs.indexOf(subConfig) > -1; | ||
| }; | ||
| testSubconfig(genericErrorMsg, rule, ruleConfig[1], isValidSubConfig, errors); | ||
| } else { | ||
| isValidSubConfig = function isValidSubConfig(availableConfigs, subConfig) { | ||
| return ruleObj.availableConfigs[subConfig] !== undefined; | ||
| }; | ||
| for (var subConfig in ruleConfig[1]) { | ||
| testSubconfig(genericErrorMsg, rule, subConfig, isValidSubConfig, errors); | ||
| } | ||
| } | ||
| } else { | ||
| if (enablingSettings.indexOf(ruleConfig) == -1) { | ||
| errors.push(genericErrorMsg + 'The the config should be "on" or "off"'); | ||
| } | ||
| } | ||
| } | ||
| function testSubconfig(genericErrorMsg, rule, subConfig, isValidSubConfig, errors) { | ||
| var ruleObj = rules.getRule(rule); | ||
| if (!isValidSubConfig(ruleObj.availableConfigs, subConfig)) { | ||
| errors.push(genericErrorMsg + ' The rule does not have the specified configuration option "' + subConfig + '"'); | ||
| } | ||
| } | ||
| module.exports = verifyConfigurationFile; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var glob = require('glob'); | ||
| var fs = require('fs'); | ||
| var path = require('path'); | ||
| var defaultIgnoreFileName = '.gherkin-lintignore'; | ||
| var defaultIgnoredFiles = 'node_modules/**'; // Ignore node_modules by default | ||
| function getFeatureFiles(args, ignoreArg) { | ||
| var files = []; | ||
| var patterns = args.length ? args : ['.']; | ||
| patterns.forEach(function (pattern) { | ||
| // First we need to fix up the pattern so that it only matches .feature files | ||
| // and it's in the format that glob expects it to be | ||
| var fixedPattern; | ||
| if (pattern == '.') { | ||
| fixedPattern = '**/*.feature'; | ||
| } else if (pattern.match(/.*\/\*\*/)) { | ||
| fixedPattern = pattern.slice(0, -1) + '.feature'; | ||
| } else if (pattern.match(/.*\.feature/)) { | ||
| fixedPattern = pattern; | ||
| } else { | ||
| try { | ||
| if (fs.statSync(pattern).isDirectory()) { | ||
| fixedPattern = path.join(pattern, '**/*.feature'); | ||
| } | ||
| } catch (e) { | ||
| // Don't show the fs callstack, we will print a custom error message bellow instead | ||
| } | ||
| } | ||
| if (!fixedPattern) { | ||
| throw new Error('Invalid format of the feature file path/pattern: "' + pattern + '".\nTo run the linter please specify an existing feature file, directory or glob.'); | ||
| } | ||
| var globOptions = { ignore: getIgnorePatterns(ignoreArg) }; | ||
| files = files.concat(glob.sync(fixedPattern, globOptions)); | ||
| }); | ||
| return _.uniq(files); | ||
| } | ||
| function getIgnorePatterns(ignoreArg) { | ||
| if (ignoreArg) { | ||
| return ignoreArg; | ||
| } else if (fs.existsSync(defaultIgnoreFileName)) { | ||
| // return an array where each element of the array is a line of the ignore file | ||
| return fs.readFileSync(defaultIgnoreFileName).toString().split(/[\n|\r]/).filter(function (i) { | ||
| // remove empty strings | ||
| if (i !== '') { | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| } else { | ||
| return defaultIgnoredFiles; | ||
| } | ||
| } | ||
| module.exports = { | ||
| getFeatureFiles: getFeatureFiles, | ||
| defaultIgnoreFileName: defaultIgnoreFileName | ||
| }; |
| "use strict"; | ||
| /*eslint no-console: "off"*/ | ||
| function printResults(results) { | ||
| console.error(JSON.stringify(results)); | ||
| } | ||
| module.exports = { | ||
| printResults: printResults | ||
| }; |
| 'use strict'; | ||
| /*eslint no-console: "off"*/ | ||
| var style = { | ||
| gray: function gray(text) { | ||
| return '\x1b[38;5;243m' + text + '\x1b[0m'; | ||
| }, | ||
| underline: function underline(text) { | ||
| return '\x1b[0;4m' + text + '\x1b[24m'; | ||
| } | ||
| }; | ||
| function stylizeError(error, maxErrorMsgLength, maxLineChars) { | ||
| var str = ' '; // indent 2 spaces so it looks pretty | ||
| var padding = ' '; //padding of 4 spaces, will be used between line numbers, error msgs and rule names | ||
| var line = error.line.toString(); | ||
| // add spaces until the line string is as long as our longest line string | ||
| while (line.length < maxLineChars) { | ||
| line += ' '; | ||
| } | ||
| // print the line number as gray | ||
| str += style.gray(line) + padding; | ||
| var errorMsg = error.message; | ||
| // add spaces until the message is as long as our longest error message | ||
| while (errorMsg.length < maxErrorMsgLength) { | ||
| errorMsg += ' '; | ||
| } | ||
| // print the error message in default color and add 2 spaces after it for readability | ||
| str += errorMsg + padding; | ||
| // print the rule name in gray | ||
| str += style.gray(error.rule); | ||
| return str; // lastly, return our stylish-est string and pretend that this code was never written | ||
| } | ||
| function stylizeFilePath(filePath) { | ||
| return style.underline(filePath); | ||
| } | ||
| function getMaxLengthOfField(results, field) { | ||
| var length = 0; | ||
| results.forEach(function (result) { | ||
| result.errors.forEach(function (error) { | ||
| var errorStr = error[field].toString(); | ||
| if (errorStr.length > length) { | ||
| length = errorStr.length; | ||
| } | ||
| }); | ||
| }); | ||
| return length; | ||
| } | ||
| function printResults(results) { | ||
| var maxErrorMsgLength = getMaxLengthOfField(results, 'message'); | ||
| var maxLineChars = getMaxLengthOfField(results, 'line'); | ||
| results.forEach(function (result) { | ||
| if (result.errors.length > 0) { | ||
| console.error(stylizeFilePath(result.filePath)); | ||
| result.errors.forEach(function (error) { | ||
| console.error(stylizeError(error, maxErrorMsgLength, maxLineChars)); | ||
| }); | ||
| console.error('\n'); | ||
| } | ||
| }); | ||
| } | ||
| module.exports = { | ||
| printResults: printResults | ||
| }; |
| 'use strict'; | ||
| var fs = require('fs'); | ||
| var _ = require('lodash'); | ||
| var Gherkin = require('gherkin'); | ||
| var parser = new Gherkin.Parser(); | ||
| var rules = require('./rules.js'); | ||
| function lint(files, configuration, additionalRulesDirs) { | ||
| var output = []; | ||
| files.forEach(function (fileName) { | ||
| var fileContent = fs.readFileSync(fileName, 'utf-8'); | ||
| var file = { | ||
| name: fileName, | ||
| lines: fileContent.split(/\r\n|\r|\n/) | ||
| }; | ||
| var errors = []; | ||
| try { | ||
| var feature = parser.parse(fileContent).feature || {}; | ||
| errors = rules.runAllEnabledRules(feature, file, configuration, additionalRulesDirs); | ||
| } catch (e) { | ||
| if (e.errors) { | ||
| errors = processFatalErrors(e.errors); | ||
| } else { | ||
| throw e; | ||
| } | ||
| } | ||
| var fileBlob = { filePath: fs.realpathSync(fileName), errors: _.sortBy(errors, 'line') }; | ||
| output.push(fileBlob); | ||
| }); | ||
| return output; | ||
| } | ||
| function processFatalErrors(errors) { | ||
| var errorMsgs = []; | ||
| if (errors.length > 1) { | ||
| var result = getFormatedTaggedBackgroundError(errors); | ||
| errors = result.errors; | ||
| errorMsgs = result.errorMsgs; | ||
| } | ||
| errors.forEach(function (error) { | ||
| errorMsgs.push(getFormattedFatalError(error)); | ||
| }); | ||
| return errorMsgs; | ||
| } | ||
| function getFormatedTaggedBackgroundError(errors) { | ||
| var errorMsgs = []; | ||
| var index = 0; | ||
| if (errors[0].message.indexOf('got \'Background') > -1 && errors[1].message.indexOf('expected: #TagLine, #ScenarioLine, #ScenarioOutlineLine, #Comment, #Empty') > -1) { | ||
| errorMsgs.push({ | ||
| message: 'Tags on Backgrounds are dissallowed', | ||
| rule: 'no-tags-on-backgrounds', | ||
| line: errors[0].message.match(/\((\d+):.*/)[1] | ||
| }); | ||
| index = 2; | ||
| for (var i = 2; i < errors.length; i++) { | ||
| if (errors[i].message.indexOf('expected: #TagLine, #ScenarioLine, #ScenarioOutlineLine, #Comment, #Empty') > -1) { | ||
| index = i; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| return { errors: errors.slice(index, errors.length), errorMsgs: errorMsgs }; | ||
| } | ||
| function getFormattedFatalError(error) { | ||
| var errorLine = error.message.match(/\((\d+):.*/)[1]; | ||
| var errorMsg; | ||
| var rule; | ||
| if (error.message.indexOf('got \'Background') > -1) { | ||
| errorMsg = 'Multiple "Background" definitions in the same file are disallowed'; | ||
| rule = 'up-to-one-background-per-file'; | ||
| } else if (error.message.indexOf('got \'Feature') > -1) { | ||
| errorMsg = 'Multiple "Feature" definitions in the same file are disallowed'; | ||
| rule = 'one-feature-per-file'; | ||
| } else { | ||
| errorMsg = error.message; | ||
| rule = 'unexpected-error'; | ||
| } | ||
| return { message: errorMsg, | ||
| rule: rule, | ||
| line: errorLine }; | ||
| } | ||
| module.exports = { | ||
| lint: lint | ||
| }; |
+47
| #!/usr/bin/env node | ||
| 'use strict'; | ||
| var program = require('commander'); | ||
| var linter = require('./linter.js'); | ||
| var featureFinder = require('./feature-finder.js'); | ||
| var configParser = require('./config-parser.js'); | ||
| function list(val) { | ||
| return val.split(','); | ||
| } | ||
| function collect(val, memo) { | ||
| memo.push(val); | ||
| return memo; | ||
| } | ||
| program.usage('[options] <feature-files>').option('-f, --format [format]', 'output format. Possible values: json, stylish. Defaults to stylish').option('-i, --ignore <...>', 'comma seperated list of files/glob patterns that the linter should ignore, overrides ' + featureFinder.defaultIgnoreFileName + ' file', list).option('-c, --config [config]', 'configuration file, defaults to ' + configParser.defaultConfigFileName).option('-r, --rulesdir <...>', 'additional rule directories', collect, []).parse(process.argv); | ||
| var additionalRulesDirs = program.rulesdir; | ||
| var files = featureFinder.getFeatureFiles(program.args, program.ignore); | ||
| var config = configParser.getConfiguration(program.config, additionalRulesDirs); | ||
| var results = linter.lint(files, config, additionalRulesDirs); | ||
| printResults(results, program.format); | ||
| process.exit(getExitCode(results)); | ||
| function getExitCode(results) { | ||
| var exitCode = 0; | ||
| results.forEach(function (result) { | ||
| if (result.errors.length > 0) { | ||
| exitCode = 1; | ||
| } | ||
| }); | ||
| return exitCode; | ||
| } | ||
| function printResults(results, format) { | ||
| var formatter; | ||
| if (format === 'json') { | ||
| formatter = require('./formatters/json.js'); | ||
| } else if (!format || format == 'stylish') { | ||
| formatter = require('./formatters/stylish.js'); | ||
| } else { | ||
| throw new Error('Unsupported format. The supported formats are json and stylish.'); | ||
| } | ||
| formatter.printResults(results); | ||
| } |
| 'use strict'; | ||
| // Operations on rules | ||
| var fs = require('fs'); | ||
| var path = require('path'); | ||
| function getAllRules(additionalRulesDirs) { | ||
| var rules = {}; | ||
| var rulesDirs = [path.join(__dirname, 'rules')].concat(additionalRulesDirs || []); | ||
| rulesDirs.forEach(function (rulesDir) { | ||
| rulesDir = path.resolve(rulesDir); | ||
| fs.readdirSync(rulesDir).forEach(function (file) { | ||
| var rule = require(path.join(rulesDir, file)); | ||
| rules[rule.name] = rule; | ||
| }); | ||
| }); | ||
| return rules; | ||
| } | ||
| function getRule(rule, additionalRulesDirs) { | ||
| return getAllRules(additionalRulesDirs)[rule]; | ||
| } | ||
| function doesRuleExist(rule, additionalRulesDirs) { | ||
| return getRule(rule, additionalRulesDirs) !== undefined; | ||
| } | ||
| function isRuleEnabled(ruleConfig) { | ||
| if (Array.isArray(ruleConfig)) { | ||
| return ruleConfig[0] === 'on'; | ||
| } | ||
| return ruleConfig === 'on'; | ||
| } | ||
| function runAllEnabledRules(feature, file, configuration, additionalRulesDirs) { | ||
| var errors = []; | ||
| var ignoreFutureErrors = false; | ||
| var rules = getAllRules(additionalRulesDirs); | ||
| Object.keys(rules).forEach(function (ruleName) { | ||
| var rule = rules[ruleName]; | ||
| if (isRuleEnabled(configuration[rule.name]) && !ignoreFutureErrors) { | ||
| var ruleConfig = Array.isArray(configuration[rule.name]) ? configuration[rule.name][1] : {}; | ||
| var error = rule.run(feature, file, ruleConfig); | ||
| if (error) { | ||
| if (rule.suppressOtherRules) { | ||
| errors = [error]; | ||
| ignoreFutureErrors = true; | ||
| } else { | ||
| errors = errors.concat(error); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| doesRuleExist: doesRuleExist, | ||
| isRuleEnabled: isRuleEnabled, | ||
| runAllEnabledRules: runAllEnabledRules, | ||
| getRule: getRule | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var languageMapping = require('gherkin').DIALECTS; | ||
| var rule = 'indentation'; | ||
| var availableConfigs = { | ||
| 'Feature': 0, | ||
| 'Background': 0, | ||
| 'Scenario': 0, | ||
| 'Step': 2, | ||
| 'Examples': 0, | ||
| 'example': 2, | ||
| 'given': 2, | ||
| 'when': 2, | ||
| 'then': 2, | ||
| 'and': 2, | ||
| 'but': 2 | ||
| }; | ||
| var errors = []; | ||
| function test(parsedLocation, configuration, type) { | ||
| // location.column is 1 index based so, when we compare with the expected indentation we need to subtract 1 | ||
| if (--parsedLocation.column !== configuration[type]) { | ||
| errors.push({ message: 'Wrong indentation for "' + type + '", expected indentation level of ' + configuration[type] + ', but got ' + parsedLocation.column, | ||
| rule: rule, | ||
| line: parsedLocation.line }); | ||
| } | ||
| } | ||
| function testStep(step, language, configuration, mergedConfiguration) { | ||
| var keyword = step.keyword; | ||
| var stepType = _.findKey(language, function (values) { | ||
| return values instanceof Array && values.indexOf(keyword) !== -1; | ||
| }); | ||
| stepType = stepType in configuration ? stepType : 'Step'; | ||
| test(step.location, mergedConfiguration, stepType); | ||
| } | ||
| function testScenarioOutline(scenarioOutline, mergedConfiguration) { | ||
| test(scenarioOutline.location, mergedConfiguration, 'Scenario'); | ||
| scenarioOutline.examples.forEach(function (examples) { | ||
| test(examples.location, mergedConfiguration, 'Examples'); | ||
| test(examples.tableHeader.location, mergedConfiguration, 'example'); | ||
| examples.tableBody.forEach(function (row) { | ||
| test(row.location, mergedConfiguration, 'example'); | ||
| }); | ||
| }); | ||
| } | ||
| function indentation(feature, unused, configuration) { | ||
| if (!feature || Object.keys(feature).length === 0) { | ||
| return; | ||
| } | ||
| var language = languageMapping[feature.language]; | ||
| var mergedConfiguration = _.merge(availableConfigs, configuration); | ||
| errors = []; | ||
| // Check Feature indentation | ||
| test(feature.location, mergedConfiguration, 'Feature'); | ||
| feature.children.forEach(function (child) { | ||
| switch (child.type) { | ||
| case 'Background': | ||
| test(child.location, mergedConfiguration, 'Background'); | ||
| break; | ||
| case 'Scenario': | ||
| test(child.location, mergedConfiguration, 'Scenario'); | ||
| break; | ||
| case 'ScenarioOutline': | ||
| testScenarioOutline(child, mergedConfiguration); | ||
| break; | ||
| default: | ||
| errors.push({ message: 'Unknown gherkin node type ' + child.type, | ||
| rule: rule, | ||
| line: child.location.line }); | ||
| break; | ||
| } | ||
| child.steps.forEach(function (step) { | ||
| // Check Step indentation | ||
| testStep(step, language, configuration, mergedConfiguration); | ||
| }); | ||
| }); | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: indentation, | ||
| availableConfigs: availableConfigs | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'name-length'; | ||
| var availableConfigs = { | ||
| 'Feature': 70, | ||
| 'Step': 70, | ||
| 'Scenario': 70 | ||
| }; | ||
| var errors = []; | ||
| function test(name, location, configuration, type) { | ||
| if (name && name.length > configuration[type]) { | ||
| errors.push({ message: type + ' name is too long. Length of ' + name.length + ' is longer than the maximum allowed: ' + configuration[type], | ||
| rule: rule, | ||
| line: location.line }); | ||
| } | ||
| } | ||
| function nameLength(feature, unused, configuration) { | ||
| if (!feature || Object.keys(feature).length === 0) { | ||
| return; | ||
| } | ||
| var mergedConfiguration = _.merge(availableConfigs, configuration); | ||
| errors = []; | ||
| // Check Feature name length | ||
| test(feature.name, feature.location, mergedConfiguration, 'Feature'); | ||
| feature.children.forEach(function (child) { | ||
| switch (child.type) { | ||
| case 'Scenario': | ||
| case 'ScenarioOutline': | ||
| // Check Scenario name length | ||
| test(child.name, child.location, mergedConfiguration, 'Scenario'); | ||
| break; | ||
| case 'Background': | ||
| break; | ||
| default: | ||
| errors.push({ message: 'Unknown gherkin node type ' + child.type, | ||
| rule: rule, | ||
| line: child.location.line }); | ||
| break; | ||
| } | ||
| child.steps.forEach(function (step) { | ||
| // Check Step name length | ||
| test(step.text, step.location, mergedConfiguration, 'Step'); | ||
| }); | ||
| }); | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: nameLength, | ||
| availableConfigs: availableConfigs | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'new-line-at-eof'; | ||
| var availableConfigs = ['yes', 'no']; | ||
| function newLineAtEOF(unused, file, configuration) { | ||
| if (_.indexOf(availableConfigs, configuration) === -1) { | ||
| throw new Error(rule + ' requires an extra configuration value.\nAvailable configurations: ' + availableConfigs.join(', ') + '\nFor syntax please look at the documentation.'); | ||
| } | ||
| var hasNewLineAtEOF = _.last(file.lines) === ''; | ||
| var errormsg = ''; | ||
| if (hasNewLineAtEOF && configuration === 'no') { | ||
| errormsg = 'New line at EOF(end of file) is not allowed'; | ||
| } else if (!hasNewLineAtEOF && configuration === 'yes') { | ||
| errormsg = 'New line at EOF(end of file) is required'; | ||
| } | ||
| if (errormsg !== '') { | ||
| return { message: errormsg, | ||
| rule: rule, | ||
| line: file.lines.length }; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: newLineAtEOF, | ||
| availableConfigs: availableConfigs | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-dupe-feature-names'; | ||
| var features = []; | ||
| function noDuplicateFeatureNames(feature, file) { | ||
| if (feature.name) { | ||
| if (feature.name in features) { | ||
| var dupes = features[feature.name].files.join(', '); | ||
| features[feature.name].files.push(file.name); | ||
| return { message: 'Feature name is already used in: ' + dupes, | ||
| rule: rule, | ||
| line: feature.location.line }; | ||
| } else { | ||
| features[feature.name] = { files: [file.name] }; | ||
| } | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noDuplicateFeatureNames | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-dupe-scenario-names'; | ||
| var scenarios = []; | ||
| function noDuplicateScenarioNames(feature, file) { | ||
| if (feature.children) { | ||
| var errors = []; | ||
| feature.children.forEach(function (scenario) { | ||
| if (scenario.name) { | ||
| if (scenario.name in scenarios) { | ||
| var dupes = getFileLinePairsAsStr(scenarios[scenario.name].locations); | ||
| scenarios[scenario.name].locations.push({ file: file.name, line: scenario.location.line }); | ||
| errors.push({ message: 'Scenario name is already used in: ' + dupes, | ||
| rule: rule, | ||
| line: scenario.location.line }); | ||
| } else { | ||
| scenarios[scenario.name] = { locations: [{ file: file.name, line: scenario.location.line }] }; | ||
| } | ||
| } | ||
| }); | ||
| return errors; | ||
| } | ||
| } | ||
| function getFileLinePairsAsStr(objects) { | ||
| var strings = []; | ||
| objects.forEach(function (object) { | ||
| strings.push(object.file + ':' + object.line); | ||
| }); | ||
| return strings.join(', '); | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noDuplicateScenarioNames | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'no-duplicate-tags'; | ||
| function noDuplicateTags(feature) { | ||
| var errors = []; | ||
| errors = errors.concat(verifyTags(feature.tags, feature.location)); | ||
| if (feature.children !== undefined) { | ||
| feature.children.forEach(function (child) { | ||
| errors = errors.concat(verifyTags(child.tags, child.location)); | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| function verifyTags(tags, location) { | ||
| var errors = [], | ||
| failedTagNames = [], | ||
| uniqueTagNames = []; | ||
| if (tags !== undefined && location !== undefined) { | ||
| tags.forEach(function (tag) { | ||
| if (!_.includes(failedTagNames, tag.name)) { | ||
| if (_.includes(uniqueTagNames, tag.name)) { | ||
| errors.push({ message: 'Duplicate tags are not allowed: ' + tag.name, | ||
| rule: rule, | ||
| line: tag.location.line }); | ||
| failedTagNames.push(tag.name); | ||
| } else { | ||
| uniqueTagNames.push(tag.name); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noDuplicateTags | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'no-empty-file'; | ||
| var suppressOtherRules = true; | ||
| function noEmptyFiles(feature) { | ||
| if (_.isEmpty(feature)) { | ||
| return { message: 'Empty feature files are disallowed', | ||
| rule: rule, | ||
| line: 1 }; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noEmptyFiles, | ||
| suppressOtherRules: suppressOtherRules | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-files-without-scenarios'; | ||
| function filterScenarios(child) { | ||
| return child.type === 'Scenario' || child.type === 'ScenarioOutline'; | ||
| } | ||
| function noFilesWithoutScenarios(feature) { | ||
| if (!feature.children || !feature.children.some(filterScenarios)) { | ||
| return { message: 'Feature file does not have any Scenarios', | ||
| rule: rule, | ||
| line: 1 }; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noFilesWithoutScenarios | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'no-homogenous-tags'; | ||
| function noHomogenousTags(feature) { | ||
| var errors = []; | ||
| if (feature.children !== undefined) { | ||
| var tagNames = _.flatten(_.map(feature.children, function (child) { | ||
| if ((child.type === 'Scenario' || child.type === 'ScenarioOutline') && child.tags !== undefined) { | ||
| return [_.map(child.tags, function (tag) { | ||
| return tag.name; | ||
| })]; | ||
| } else { | ||
| return []; | ||
| } | ||
| })); | ||
| var homogenousTags = _.intersection.apply(_, tagNames); | ||
| if (homogenousTags.length !== 0) { | ||
| // You could argue that the line number should be the first instance of a | ||
| // bad tag, but I think this is really a problem with the whole feature. | ||
| errors = [{ message: 'All Scenarios on this Feature have the same tag(s), ' + 'they should be defined on the Feature instead: ' + _.join(homogenousTags, ', '), | ||
| rule: rule, | ||
| line: feature.location.line }]; | ||
| } | ||
| } | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noHomogenousTags | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-multiple-empty-lines'; | ||
| function noMulitpleEmptyLines(unused, file) { | ||
| var errors = []; | ||
| for (var i = 0; i < file.lines.length - 1; i++) { | ||
| if (file.lines[i].trim() === '' && file.lines[i + 1].trim() == '') { | ||
| errors.push({ message: 'Multiple empty lines are not allowed', | ||
| rule: rule, | ||
| line: i + 2 }); | ||
| } | ||
| } | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noMulitpleEmptyLines | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-partially-commented-tag-lines'; | ||
| function noPartiallyCommentedTagLines(feature) { | ||
| var errors = []; | ||
| if (feature.children) { | ||
| feature.children.forEach(function (scenario) { | ||
| if (scenario.tags) { | ||
| scenario.tags.forEach(function (tag) { | ||
| if (tag.name.indexOf('#') > 0) { | ||
| errors.push({ message: 'Partially commented tag lines not allowed ', | ||
| rule: rule, | ||
| line: tag.location.line }); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noPartiallyCommentedTagLines | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'no-restricted-tags'; | ||
| var availableConfigs = { | ||
| 'tags': [] | ||
| }; | ||
| function noRestrictedTags(feature, fileName, configuration) { | ||
| var forbiddenTags = configuration.tags; | ||
| var featureErrors = checkTags(feature, forbiddenTags); | ||
| var childrenErrors = _(feature.children).map(function (child) { | ||
| return checkTags(child, forbiddenTags); | ||
| }).flatten().value(); | ||
| return featureErrors.concat(childrenErrors); | ||
| } | ||
| function checkTags(node, forbiddenTags) { | ||
| return (node.tags || []).filter(function (tag) { | ||
| return isForbidden(tag, forbiddenTags); | ||
| }).map(function (tag) { | ||
| return createError(node, tag); | ||
| }); | ||
| } | ||
| function isForbidden(tag, forbiddenTags) { | ||
| return _.includes(forbiddenTags, tag.name); | ||
| } | ||
| function createError(node, tag) { | ||
| return { message: 'Forbidden tag ' + tag.name + ' on ' + node.type, | ||
| rule: rule, | ||
| line: node.location && node.location.line || 0 }; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noRestrictedTags, | ||
| availableConfigs: availableConfigs | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-scenario-outlines-without-examples'; | ||
| function noScenarioOutlinesWithoutExamples(feature) { | ||
| if (feature.children) { | ||
| var errors = []; | ||
| feature.children.forEach(function (scenario) { | ||
| if (scenario.type === 'ScenarioOutline' && !scenario.examples.length) { | ||
| errors.push({ message: 'Scenario Outline does not have any Examples', | ||
| rule: rule, | ||
| line: scenario.location.line }); | ||
| } | ||
| }); | ||
| return errors; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noScenarioOutlinesWithoutExamples | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'no-superfluous-tags'; | ||
| function noSuperfluousTags(feature) { | ||
| var errors = []; | ||
| if (feature.tags !== undefined && feature.children !== undefined) { | ||
| feature.children.forEach(function (child) { | ||
| if (child.tags !== undefined) { | ||
| var superfluousTags = _.intersectionWith(child.tags, feature.tags, function (lhs, rhs) { | ||
| return lhs.name === rhs.name; | ||
| }); | ||
| if (superfluousTags.length !== 0) { | ||
| var superfluousTagNames = _.map(superfluousTags, function (tag) { | ||
| return tag.name; | ||
| }); | ||
| errors.push({ message: 'Tag(s) duplicated on a Feature and a Scenario in that Feature: ' + _.join(superfluousTagNames, ', '), | ||
| rule: rule, | ||
| line: superfluousTags[0].location.line }); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noSuperfluousTags | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-trailing-spaces'; | ||
| function noTrailingSpaces(unused, file) { | ||
| var errors = []; | ||
| var lineNo = 1; | ||
| file.lines.forEach(function (line) { | ||
| if (/[\t ]+$/.test(line)) { | ||
| errors.push({ message: 'Trailing spaces are not allowed', | ||
| rule: rule, | ||
| line: lineNo }); | ||
| } | ||
| lineNo++; | ||
| }); | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noTrailingSpaces | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-unnamed-features'; | ||
| function noUnNamedFeatures(feature) { | ||
| if (!feature || !feature.name) { | ||
| return { message: 'Missing Feature name', | ||
| rule: rule, | ||
| line: feature.location && feature.location.line || 0 }; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noUnNamedFeatures | ||
| }; |
| 'use strict'; | ||
| var rule = 'no-unnamed-scenarios'; | ||
| function noUnNamedScenarios(feature) { | ||
| if (feature.children) { | ||
| var errors = []; | ||
| feature.children.forEach(function (scenario) { | ||
| if (!scenario.name && scenario.type === 'Scenario') { | ||
| errors.push({ message: 'Missing Scenario name', | ||
| rule: rule, | ||
| line: scenario.location.line }); | ||
| } | ||
| }); | ||
| return errors; | ||
| } | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: noUnNamedScenarios | ||
| }; |
| 'use strict'; | ||
| var _ = require('lodash'); | ||
| var rule = 'one-space-between-tags'; | ||
| function run(feature) { | ||
| if (!feature || Object.keys(feature).length === 0) { | ||
| return []; | ||
| } | ||
| var errors = []; | ||
| function testTags(allTags) { | ||
| _(allTags).sort(function (tag) { | ||
| return -tag.location.column; | ||
| }).groupBy(function (tag) { | ||
| return tag.location.line; | ||
| }).forEach(function (tags) { | ||
| _.range(tags.length - 1).map(function (i) { | ||
| if (tags[i].location.column + tags[i].name.length < tags[i + 1].location.column - 1) { | ||
| errors.push({ | ||
| line: tags[i].location.line, | ||
| rule: rule, | ||
| message: 'There is more than one space between the tags ' + tags[i].name + ' and ' + tags[i + 1].name | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| testTags(feature.tags); | ||
| feature.children.forEach(function (child) { | ||
| if (child.type === 'Scenario' || child.type === 'ScenarioOutline') { | ||
| testTags(child.tags); | ||
| } | ||
| }); | ||
| return errors; | ||
| } | ||
| module.exports = { | ||
| run: run, | ||
| name: rule | ||
| }; |
| 'use strict'; | ||
| var rule = 'use-and'; | ||
| function useAnd(feature) { | ||
| var errors = []; | ||
| feature.children.forEach(function (child) { | ||
| var previousKeyword = undefined; | ||
| child.steps.forEach(function (step) { | ||
| if (step.keyword === 'And ') { | ||
| return; | ||
| } | ||
| if (step.keyword === previousKeyword) { | ||
| errors.push(createError(step)); | ||
| } | ||
| previousKeyword = step.keyword; | ||
| }); | ||
| }); | ||
| return errors; | ||
| } | ||
| function createError(step) { | ||
| return { message: 'Step "' + step.keyword + step.text + '" should use And instead of ' + step.keyword, | ||
| rule: rule, | ||
| line: step.location.line }; | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: useAnd | ||
| }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
+1
-1
| { | ||
| "name": "gherkin-lint", | ||
| "version": "2.7.0", | ||
| "version": "2.8.0", | ||
| "description": "A Gherkin linter/validator written in javascript", | ||
@@ -5,0 +5,0 @@ "author": "Vasiliki Siakka", |
+4
-2
@@ -88,7 +88,9 @@ # Gherkin lint | ||
| "and": 2, | ||
| "but": 2 | ||
| "but": 2, | ||
| "feature tag": 0, | ||
| "scenario tag": 0 | ||
| } | ||
| ] | ||
| } | ||
| There is no need to override all the defaults, as is done above, instead they can be overriden only where required. `Step` will be used as a fallback if the keyword of the step, eg. 'given', is not specified. | ||
| There is no need to override all the defaults, as is done above, instead they can be overriden only where required. `Step` will be used as a fallback if the keyword of the step, eg. 'given', is not specified. If `feature tag` is not set then `Feature` is used as a fallback, and if `scenario tag` is not set then `Scenario` is used as a fallback. | ||
@@ -95,0 +97,0 @@ This feature is able to handle all localizations of the gherkin steps. |
+75
-43
@@ -5,3 +5,3 @@ var _ = require('lodash'); | ||
| var availableConfigs = { | ||
| var defaultConfig = { | ||
| 'Feature': 0, | ||
@@ -20,56 +20,82 @@ 'Background': 0, | ||
| var errors = []; | ||
| var availableConfigs = _.merge({}, defaultConfig, { | ||
| // The values here are unused by the config parsing logic. | ||
| 'feature tag': -1, | ||
| 'scenario tag': -1 | ||
| }); | ||
| function test(parsedLocation, configuration, type) { | ||
| // location.column is 1 index based so, when we compare with the expected indentation we need to subtract 1 | ||
| if (--parsedLocation.column !== configuration[type]) { | ||
| errors.push({message: 'Wrong indentation for "' + type + | ||
| '", expected indentation level of ' + configuration[type] + | ||
| ', but got ' + parsedLocation.column, | ||
| rule : rule, | ||
| line : parsedLocation.line}); | ||
| function mergeConfiguration(configuration) { | ||
| var mergedConfiguration = _.merge({}, defaultConfig, configuration); | ||
| if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'feature tag')) { | ||
| mergedConfiguration['feature tag'] = mergedConfiguration['Feature']; | ||
| } | ||
| if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, 'scenario tag')) { | ||
| mergedConfiguration['scenario tag'] = mergedConfiguration['Scenario']; | ||
| } | ||
| return mergedConfiguration; | ||
| } | ||
| function testStep(step, language, configuration, mergedConfiguration) { | ||
| var keyword = step.keyword; | ||
| var stepType = _.findKey(language, function(values) { | ||
| return values instanceof Array && values.indexOf(keyword) !== -1; | ||
| }); | ||
| stepType = stepType in configuration ? stepType : 'Step'; | ||
| test(step.location, mergedConfiguration, stepType); | ||
| } | ||
| function testFeature(feature, configuration, mergedConfiguration) { | ||
| var errors = []; | ||
| function testScenarioOutline(scenarioOutline, mergedConfiguration) { | ||
| test(scenarioOutline.location, mergedConfiguration, 'Scenario'); | ||
| scenarioOutline.examples.forEach(function(examples) { | ||
| test(examples.location, mergedConfiguration, 'Examples'); | ||
| test(examples.tableHeader.location, mergedConfiguration, 'example'); | ||
| examples.tableBody.forEach(function(row) { | ||
| test(row.location, mergedConfiguration, 'example'); | ||
| function test(parsedLocation, type) { | ||
| // location.column is 1 index based so, when we compare with the expected | ||
| // indentation we need to subtract 1 | ||
| if (--parsedLocation.column !== mergedConfiguration[type]) { | ||
| errors.push({message: 'Wrong indentation for "' + type + | ||
| '", expected indentation level of ' + mergedConfiguration[type] + | ||
| ', but got ' + parsedLocation.column, | ||
| rule : rule, | ||
| line : parsedLocation.line}); | ||
| } | ||
| } | ||
| function testStep(step) { | ||
| var keyword = step.keyword; | ||
| var stepType = _.findKey(languageMapping[feature.language], function(values) { | ||
| return values instanceof Array && values.indexOf(keyword) !== -1; | ||
| }); | ||
| }); | ||
| } | ||
| stepType = stepType in configuration ? stepType : 'Step'; | ||
| test(step.location, stepType); | ||
| } | ||
| function indentation(feature, unused, configuration) { | ||
| if (!feature || Object.keys(feature).length === 0) { | ||
| return; | ||
| function testScenarioOutline(scenarioOutline) { | ||
| test(scenarioOutline.location, 'Scenario'); | ||
| scenarioOutline.examples.forEach(function(examples) { | ||
| test(examples.location, 'Examples'); | ||
| test(examples.tableHeader.location, 'example'); | ||
| examples.tableBody.forEach(function(row) { | ||
| test(row.location, 'example'); | ||
| }); | ||
| }); | ||
| } | ||
| var language = languageMapping[feature.language]; | ||
| var mergedConfiguration = _.merge(availableConfigs, configuration); | ||
| errors = []; | ||
| // Check Feature indentation | ||
| test(feature.location, mergedConfiguration, 'Feature'); | ||
| function testTags(tags, type) { | ||
| _(tags).map(function(tag) { | ||
| return tag.location; | ||
| }).groupBy(function(tagLocation) { | ||
| return tagLocation.line; | ||
| }).forEach(function(locationsPerLine) { | ||
| var firstLocation = locationsPerLine.sort(function(location) { | ||
| return -location.column; | ||
| })[0]; | ||
| test(firstLocation, type); | ||
| }); | ||
| } | ||
| test(feature.location, 'Feature'); | ||
| testTags(feature.tags, 'feature tag'); | ||
| feature.children.forEach(function(child) { | ||
| switch(child.type) { | ||
| case 'Background': | ||
| test(child.location, mergedConfiguration, 'Background'); | ||
| test(child.location, 'Background'); | ||
| break; | ||
| case 'Scenario': | ||
| test(child.location, mergedConfiguration, 'Scenario'); | ||
| test(child.location, 'Scenario'); | ||
| testTags(child.tags, 'scenario tag'); | ||
| break; | ||
| case 'ScenarioOutline': | ||
| testScenarioOutline(child, mergedConfiguration); | ||
| testScenarioOutline(child); | ||
| testTags(child.tags, 'scenario tag'); | ||
| break; | ||
@@ -83,6 +109,3 @@ default: | ||
| child.steps.forEach(function(step) { | ||
| // Check Step indentation | ||
| testStep(step, language, configuration, mergedConfiguration); | ||
| }); | ||
| child.steps.forEach(testStep); | ||
| }); | ||
@@ -93,6 +116,15 @@ | ||
| function run(feature, unused, configuration) { | ||
| if (!feature || Object.keys(feature).length === 0) { | ||
| return; | ||
| } | ||
| var mergedConfiguration = mergeConfiguration(configuration); | ||
| return testFeature(feature, configuration, mergedConfiguration); | ||
| } | ||
| module.exports = { | ||
| name: rule, | ||
| run: indentation, | ||
| run: run, | ||
| availableConfigs: availableConfigs | ||
| }; |
@@ -8,30 +8,45 @@ var ruleTestBase = require('../rule-test-base'); | ||
| messageElements: {element: 'Feature', expected: 0, actual: 1}, | ||
| line: 2 | ||
| },{ | ||
| messageElements: {element: 'feature tag', expected: 0, actual: 1}, | ||
| line: 1 | ||
| },{ | ||
| messageElements: {element: 'Background', expected: 0, actual: 4}, | ||
| line: 3 | ||
| line: 4 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 0}, | ||
| line: 4 | ||
| line: 5 | ||
| },{ | ||
| messageElements: {element: 'Scenario', expected: 0, actual: 1}, | ||
| line: 6 | ||
| line: 9 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 3}, | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 1}, | ||
| line: 7 | ||
| },{ | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 1}, | ||
| line: 8 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 3}, | ||
| line: 10 | ||
| },{ | ||
| messageElements: {element: 'Scenario', expected: 0, actual: 3}, | ||
| line: 9 | ||
| line: 14 | ||
| },{ | ||
| messageElements: {element: 'Examples', expected: 0, actual: 2}, | ||
| line: 11 | ||
| line: 16 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual:4}, | ||
| messageElements: {element: 'example', expected: 2, actual: 4}, | ||
| line: 17 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual: 4}, | ||
| line: 18 | ||
| },{ | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 3}, | ||
| line: 12 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual:4}, | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 4}, | ||
| line: 13 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 3}, | ||
| line: 10 | ||
| line: 15 | ||
| }]; | ||
@@ -59,34 +74,62 @@ | ||
| messageElements: {element: 'Feature', expected: 0, actual: 4}, | ||
| line: 3 | ||
| },{ | ||
| messageElements: {element: 'feature tag', expected: 0, actual: 4}, | ||
| line: 2 | ||
| },{ | ||
| messageElements: {element: 'Background', expected: 0, actual: 4}, | ||
| line: 4 | ||
| line: 5 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 0}, | ||
| line: 5 | ||
| line: 6 | ||
| },{ | ||
| messageElements: {element: 'Scenario', expected: 0, actual: 4}, | ||
| line: 7 | ||
| line: 10 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 12}, | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 4}, | ||
| line: 8 | ||
| },{ | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 1}, | ||
| line: 9 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 12}, | ||
| line: 11 | ||
| },{ | ||
| messageElements: {element: 'Scenario', expected: 0, actual: 12}, | ||
| line: 10 | ||
| line: 15 | ||
| },{ | ||
| messageElements: {element: 'Examples', expected: 0, actual: 7}, | ||
| line: 12 | ||
| line: 17 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual: 15}, | ||
| line: 18 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual: 15}, | ||
| line: 19 | ||
| },{ | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 4}, | ||
| line: 13 | ||
| },{ | ||
| messageElements: {element: 'example', expected: 2, actual: 15}, | ||
| messageElements: {element: 'scenario tag', expected: 0, actual: 1}, | ||
| line: 14 | ||
| },{ | ||
| messageElements: {element: 'Step', expected: 2, actual: 11}, | ||
| line: 11 | ||
| line: 16 | ||
| }]); | ||
| }); | ||
| it('defaults the tag indentation settings when they are not set', function() { | ||
| runTest('indentation/CorrectIndentationWithFeatureAndScenarioOverrides.feature', { | ||
| 'Feature': 1, | ||
| 'Scenario': 3 | ||
| }, []); | ||
| }); | ||
| it('observe tag indentation settings when they are overriden', function() { | ||
| runTest('indentation/CorrectIndentationWithScenarioTagOverrides.feature', { | ||
| 'scenario tag': 3 | ||
| }, []); | ||
| }); | ||
| // TODO: add tests for partial configurations and fallbacks (eg rule for Step is used for Given, Then etc is rule for Given, Then, etc has not been specified) | ||
| }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
99626
51.53%117
32.95%2270
75.43%159
1.27%11
83.33%