gplint
Version:
A Gherkin linter/validator written in Javascript.
276 lines • 12 kB
JavaScript
import { StepKeywordType, } from '@cucumber/messages';
import * as gherkinUtils from './utils/gherkin.js';
import { featureSpread } from './utils/gherkin.js';
const keywords = ['Given', 'When', 'Then'];
let previousKeyword;
export const name = 'no-restricted-patterns';
export const availableConfigs = {
'Global': [],
'Feature': [],
'Rule': [],
'Background': [],
'Scenario': [],
'ScenarioOutline': [],
'Examples': [],
'ExampleHeader': [],
'ExampleBody': [],
'Step': [],
'Given': [],
'When': [],
'Then': [],
'DataTable': [],
'DocString': [],
};
export function run({ feature }, configuration) {
previousKeyword = '';
if (!feature) {
return [];
}
const errors = [];
const restrictedPatterns = getRestrictedPatterns(configuration);
const { language } = feature;
// Check the feature itself
checkNameAndDescription(feature, restrictedPatterns, language, errors);
const { children, rules, } = featureSpread(feature);
rules.forEach(rule => {
checkNameAndDescription(rule, restrictedPatterns, language, errors);
});
// Check the feature children
children.forEach(child => {
const node = child.background ?? child.scenario;
checkNameAndDescription(node, restrictedPatterns, language, errors);
// And all the steps of each child
node.steps.forEach((step, index) => {
checkStepNode(step, node.steps[index], restrictedPatterns, language, errors);
checkStepNode(step, node, restrictedPatterns, language, errors);
if (step.docString != null) {
restrictedPatterns.docstring.forEach(dsPattern => {
check(step.docString, 'DocString', 'content', dsPattern, language, errors);
});
}
if (step.dataTable != null) {
restrictedPatterns.datatable.forEach(dtPattern => {
step.dataTable.rows.forEach(row => {
row.cells.forEach(cell => {
check(cell, 'DataTable cell', 'value', dtPattern, language, errors);
});
});
});
}
});
if (child.scenario?.examples) {
child.scenario.examples.forEach(example => {
checkNameAndDescription(example, restrictedPatterns, language, errors);
checkExampleNode(example, restrictedPatterns, language, errors);
});
}
});
return errors.filter((obj, index, self) => index === self.findIndex((el) => el.message === obj.message));
}
function getRestrictedPatterns(configuration) {
// Patterns applied to everything; feature, scenarios, etc.
const globalPatterns = (configuration.Global ?? []).map(pattern => new RegExp(pattern, 'i'));
//pattern to apply on all steps
const stepPatterns = (configuration.Step ?? []).map(pattern => new RegExp(pattern, 'i'));
const 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'));
if (keywords.map(item => item.toLowerCase()).includes(resolvedKey.toLowerCase())) {
restrictedPatterns[resolvedKey] = restrictedPatterns[resolvedKey].concat(stepPatterns);
}
restrictedPatterns[resolvedKey] = restrictedPatterns[resolvedKey].concat(globalPatterns);
});
return restrictedPatterns;
}
function getRestrictedPatternsForNode(node, restrictedPatterns, language) {
const key = gherkinUtils.getLanguageInsensitiveKeyword(node, language).toLowerCase();
if (keywords.map(item => item.toLowerCase()).includes(key.toLowerCase())) {
previousKeyword = key;
}
if (node.keywordType === StepKeywordType.CONJUNCTION && keywords.map(item => item.toLowerCase()).includes(previousKeyword.toLowerCase())) {
return restrictedPatterns[previousKeyword];
}
return restrictedPatterns[key];
}
function checkNameAndDescription(node, restrictedPatterns, language, errors) {
getRestrictedPatternsForNode(node, restrictedPatterns, language)
.forEach(pattern => {
checkGuessType(node, 'name', pattern, language, errors);
checkGuessType(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 => {
checkGuessType(node, 'text', pattern, language, errors);
});
}
function checkExampleNode(node, restrictedPatterns, language, errors) {
restrictedPatterns.exampleheader.forEach(pattern => {
node.tableHeader.cells.forEach(cell => {
check(cell, 'ExampleHeader', 'value', pattern, language, errors);
});
});
restrictedPatterns.examplebody.forEach(pattern => {
node.tableBody.forEach(column => {
column.cells.forEach(cell => {
check(cell, 'ExampleBody', 'value', pattern, language, errors);
});
});
});
}
function checkGuessType(node, property, pattern, language, errors) {
const type = gherkinUtils.getNodeType(node, language);
check(node, type, property, pattern, language, errors);
}
function check(node, type, property, pattern, language, errors) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore IDK how to handle types for this...
let strings = [node[property]];
if (property === 'description') {
// Descriptions can be multiline, in which case the description will contain escaped
// 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 = '<!gplint new line sentinel!>';
const escapedNewLine = '\\n';
strings = node.description
.replace(escapedNewLine, escapedNewLineSentinel)
.split('\n')
.map((str) => str.replace(escapedNewLineSentinel, escapedNewLine));
}
for (const element of strings) {
// We use trim() on the examined string because names and descriptions can contain
// white space before and after, unlike steps
if (element.trim().match(pattern)) {
errors.push({
message: `${type} ${property}: "${element.trim()}" matches restricted pattern "${pattern}"`,
rule: name,
line: node.location.line,
column: node.location.column,
});
}
}
}
export const documentation = {
description: 'A list of patterns to disallow globally, or specifically in features, rules, backgrounds, scenarios, or scenario outlines, Steps. All patterns are treated as case-insensitive',
configuration: [{
name: 'Global',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at any level',
default: availableConfigs.Global,
}, {
name: 'Feature',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Feature level',
default: availableConfigs.Feature,
}, {
name: 'Rule',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Rule level',
default: availableConfigs.Rule,
}, {
name: 'Background',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Background level',
default: availableConfigs.Background,
}, {
name: 'Scenario',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Scenario level',
default: availableConfigs.Scenario,
}, {
name: 'ScenarioOutline',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at ScenarioOutline level',
default: availableConfigs.ScenarioOutline,
}, {
name: 'Examples',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Examples level',
default: availableConfigs.Examples,
}, {
name: 'ExampleHeader',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at ExampleHeader level',
default: availableConfigs.ExampleHeader,
}, {
name: 'ExampleBody',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at ExampleBody level',
default: availableConfigs.ExampleBody,
}, {
name: 'Step',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Step level',
default: availableConfigs.Step,
}, {
name: 'Given',
type: '(string|RegExp)[]',
description: 'Text patterns not allowed at Given level',
default: availableConfigs.Given,
}],
examples: [{
title: 'Example',
description: 'Configure multiple patterns, mixing plain strings with RegExps',
config: {
[name]: ['error', {
'Global': [
'^globally restricted pattern',
],
'Feature': [
'poor description',
'validate',
'verify',
],
'Background': [
'show last response',
'a debugging step',
],
'Scenario': [
'show last response',
'a debugging step',
],
'Examples': [
'poor examples name',
'really bad examples description',
],
'ExampleHeader': [
'^.*disallowed.*$',
],
'ExampleBody': [
'^.*invalid.*$',
],
'Step': [
'bad step',
],
'Given': [
'bad step given',
'a debugging step given',
],
'When': [
'bad step when',
'a debugging step when',
],
'Then': [
'bad step then',
'a debugging step then',
],
'DocString': [
'^.*disallowed.*$',
],
'DataTable': [
'^.*invalid.*$',
'wrong value',
],
}],
},
}],
};
//# sourceMappingURL=no-restricted-patterns.js.map