gherkin-lint
Version:
A Gherkin linter/validator written in javascript
106 lines (90 loc) • 4.01 kB
JavaScript
const gherkinUtils = require('./utils/gherkin.js');
const rule = 'no-restricted-patterns';
const availableConfigs = {
'Global': [],
'Scenario': [],
'ScenarioOutline': [],
'Background': [],
'Feature': []
};
function run(feature, unused, configuration) {
if (!feature) {
return [];
}
let errors = [];
const restrictedPatterns = getRestrictedPatterns(configuration);
const language = feature.language; // Check the feature itself
checkNameAndDescription(feature, restrictedPatterns, language, errors); // Check the feature children
feature.children.forEach(child => {
let node = child.background || child.scenario;
checkNameAndDescription(node, restrictedPatterns, language, errors); // And all the steps of each child
node.steps.forEach(step => {
checkStepNode(step, node, restrictedPatterns, language, errors);
});
});
return errors;
}
function getRestrictedPatterns(configuration) {
// Patterns applied to everything; feature, scenarios, etc.
let globalPatterns = (configuration.Global || []).map(pattern => new RegExp(pattern, 'i'));
let restrictedPatterns = {};
Object.keys(availableConfigs).forEach(key => {
const resolvedKey = key.toLowerCase().replace(/ /g, '');
const resolvedConfig = configuration[key] || [];
restrictedPatterns[resolvedKey] = resolvedConfig.map(pattern => new RegExp(pattern, 'i'));
restrictedPatterns[resolvedKey] = restrictedPatterns[resolvedKey].concat(globalPatterns);
});
return restrictedPatterns;
}
function getRestrictedPatternsForNode(node, restrictedPatterns, language) {
let key = gherkinUtils.getLanguageInsitiveKeyword(node, language).toLowerCase();
return restrictedPatterns[key];
}
function checkNameAndDescription(node, restrictedPatterns, language, errors) {
getRestrictedPatternsForNode(node, restrictedPatterns, language).forEach(pattern => {
check(node, 'name', pattern, language, errors);
check(node, 'description', pattern, language, errors);
});
}
function checkStepNode(node, parentNode, restrictedPatterns, language, errors) {
// Use the node keyword of the parent to determine which rule configuration to use
getRestrictedPatternsForNode(parentNode, restrictedPatterns, language).forEach(pattern => {
check(node, 'text', pattern, language, errors);
});
}
function check(node, property, pattern, language, errors) {
if (!node[property]) {
return;
}
let strings = [node[property]];
const type = gherkinUtils.getNodeType(node, language);
if (property == 'description') {
// Descriptions can be multiline, in which case the description will contain escapted
// newline characters "\n". If a multiline description matches one of the restricted patterns
// when the error message gets printed in the console, it will break the message into multiple lines.
// So let's split the description on newline chars and test each line separately.
// To make sure we don't accidentally pick up a doubly escaped new line "\\n" which would appear
// if a user wrote the string "\n" in a description, let's replace all escaped new lines
// with a sentinel, split lines and then restore the doubly escaped new line
const escapedNewLineSentinel = '<!gherkin-lint new line sentinel!>';
const escapedNewLine = '\\n';
strings = node[property].replace(escapedNewLine, escapedNewLineSentinel).split('\n').map(string => string.replace(escapedNewLineSentinel, escapedNewLine));
}
for (let i = 0; i < strings.length; i++) {
// We use trim() on the examined string because names and descriptions can contain
// white space before and after, unlike steps
if (strings[i].trim().match(pattern)) {
errors.push({
message: `${type} ${property}: "${strings[i].trim()}" matches restricted pattern "${pattern}"`,
rule: rule,
line: node.location.line
});
}
}
}
module.exports = {
name: rule,
run: run,
availableConfigs: availableConfigs
};
;