gplint
Version:
A Gherkin linter/validator written in Javascript.
159 lines • 6.13 kB
JavaScript
import fs from 'fs';
import _ from 'lodash';
import { GherkinStreams } from '@cucumber/gherkin-streams';
import * as logger from './logger.js';
import * as rules from './rules.js';
import * as configParser from './config-parser.js';
import { RuleErrors } from './errors.js';
export async function readAndParseFile(filePath) {
let feature;
const pickles = [];
const parsingErrors = [];
let fileContent = [];
let fileEOL = '\n';
return new Promise((resolve, reject) => {
const options = {
includeGherkinDocument: true,
includePickles: true,
includeSource: true,
};
const stream = GherkinStreams.fromPaths([filePath], options);
stream.on('data', (envelope) => {
if (envelope.parseError) {
parsingErrors.push(envelope.parseError);
}
else {
if (envelope.gherkinDocument?.feature) {
feature = envelope.gherkinDocument.feature;
}
if (envelope.pickle) {
pickles.push(envelope.pickle);
}
if (envelope.source) {
fileEOL = envelope.source.data.includes('\r\n') ? '\r\n' : '\n';
fileContent = envelope.source.data.split(fileEOL);
}
}
});
stream.on('error', data => {
logger.error(`Gherkin emitted an error while parsing ${filePath}: ${data.message}`);
reject(new RuleErrors(processFatalErrors([{ message: data.message }])));
});
stream.on('end', () => {
if (parsingErrors.length) {
// Process all errors/attachments at once, because a tag on a background will
// generate multiple error events, and it would be confusing to print a message for each
// one of them, when they are all caused by a single cause
reject(new RuleErrors(processFatalErrors(parsingErrors)));
}
else {
const file = {
relativePath: filePath,
lines: fileContent,
EOL: fileEOL,
};
resolve({ feature, pickles, file });
}
});
});
}
export async function lintInit(files, args, additionalRulesDirs) {
const configuration = await configParser.getConfiguration(args.config, additionalRulesDirs);
return lint(files, configuration, additionalRulesDirs, args.fix);
}
export async function lint(files, configuration, additionalRulesDirs, autoFix) {
const results = [];
await Promise.all(files.map(async (f) => {
let perFileErrors;
try {
const gherkinData = await readAndParseFile(f);
perFileErrors = await rules.runAllEnabledRules(gherkinData, configuration, additionalRulesDirs, autoFix);
}
catch (parsingErrors) {
if (parsingErrors instanceof RuleErrors) {
perFileErrors = parsingErrors;
}
else {
throw parsingErrors;
}
}
finally {
const fileErrors = {
filePath: fs.realpathSync(f),
errors: _.sortBy(perFileErrors?.getErrors(), 'line')
};
results.push(fileErrors);
}
}));
return results;
}
function processFatalErrors(errors) {
let errorMsgs = [];
if (errors.length > 1) {
const result = getFormattedTaggedBackgroundError(errors);
errors = result.errors;
errorMsgs = result.errorMsgs;
}
errors.forEach((error) => {
errorMsgs.push(getFormattedFatalError(error));
});
return errorMsgs;
}
function getFormattedTaggedBackgroundError(errors) {
const errorMsgs = [];
let index = 0;
if (errors[0].message?.includes('got \'Background') &&
errors[1].message?.includes('expected: #TagLine, #RuleLine, #Comment, #Empty')) {
errorMsgs.push({
message: 'Tags on Backgrounds are disallowed',
rule: 'no-tags-on-backgrounds',
line: parseInt((/\((\d+):.*/.exec(errors[0].message))?.[1] ?? '0'),
column: 0,
level: 2, // Force error
});
index = 2;
for (let i = 2; i < errors.length; i++) {
if (errors[i].message?.includes('expected: #TagLine, #RuleLine, #Comment, #Empty')) {
index = i + 1;
}
else {
/* c8 ignore next 3 */
// IDK if this could happen, but let's keep this to be safe
break;
}
}
}
return { errors: errors.slice(index), errorMsgs };
}
function getFormattedFatalError(error) {
const errorLine = parseInt((/\((\d+):.*/.exec(error.message))?.[1] ?? '0');
let errorMsg;
let rule;
if (error.message.includes('got \'Background')) {
errorMsg = 'Multiple "Background" definitions in the same file are disallowed';
rule = 'up-to-one-background-per-file';
}
else if (error.message.includes('got \'Feature')) {
errorMsg = 'Multiple "Feature" definitions in the same file are disallowed';
rule = 'one-feature-per-file';
}
else if (error.message.includes('expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got') ||
error.message.includes('expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ExamplesLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got')) {
errorMsg = 'Steps should begin with "Given", "When", "Then", "And" or "But". Multiline steps are disallowed';
rule = 'no-multiline-steps';
}
else {
/* c8 ignore next 4 */
// IDK if this could happen, but let's keep this to be safe
errorMsg = error.message;
rule = 'unexpected-error';
}
return {
message: errorMsg,
rule,
line: errorLine,
column: 0,
level: 2, // Force error
};
}
//# sourceMappingURL=linter.js.map