UNPKG

gplint

Version:

A Gherkin linter/validator written in Javascript.

242 lines (241 loc) 9.79 kB
import _ from 'lodash'; import * as gherkinUtils from './utils/gherkin.js'; export const name = 'indentation'; const defaultConfig = { // Levels Feature: 0, Background: 2, Rule: 2, Scenario: 2, Step: 4, Examples: 4, example: 6, given: 4, when: 4, then: 4, and: 4, but: 4, // Config RuleFallback: true, // If `true`, the indentation for nodes inside Rule is the sum of "Rule" and the node itself, else it uses the node directly type: 'both', // 'both' | 'space' | 'tab' preferType: 'space', // 'space' | 'tab' }; export const availableConfigs = _.merge({}, defaultConfig, { // The values here are unused by the config parsing logic. 'feature tag': -1, 'rule tag': -1, 'scenario tag': -1, 'examples tag': -1, }); function mergeConfiguration(configuration) { const mergedConfiguration = _.merge({}, defaultConfig, configuration); Object.entries({ 'feature tag': mergedConfiguration.Feature, 'rule tag': mergedConfiguration.Rule, 'scenario tag': mergedConfiguration.Scenario, 'examples tag': mergedConfiguration.Examples, }).forEach(([key, value]) => { if (!Object.prototype.hasOwnProperty.call(mergedConfiguration, key)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore mergedConfiguration[key] = value; } }); return mergedConfiguration; } export function run({ feature, file }, configuration) { if (!feature) { return []; } const errors = []; const mergedConfiguration = mergeConfiguration(configuration); function validate(location, type, modifier = 0) { const expectedIndentation = mergedConfiguration[type] + modifier; const lineContent = file.lines[location.line - 1]; const indentChar = mergedConfiguration.type === 'both' && [' ', '\t'].includes(lineContent[0]) ? lineContent[0] : mergedConfiguration.type === 'tab' ? '\t' : ' '; if (lineContent.substring(0, lineContent.length - lineContent.trimStart().length) !== indentChar.repeat(expectedIndentation)) { errors.push({ location, type, expectedIndentation, }); } } function validateStep(step, modifier = 0) { let stepType = gherkinUtils.getLanguageInsensitiveKeyword(step, feature?.language); stepType = stepType != null && stepType in configuration ? stepType : 'Step'; validate(step.location, stepType, modifier); } function validateTags(tags, type, modifier = 0) { _(tags).groupBy('location.line').forEach(tagLocationGroup => { const firstTag = _(tagLocationGroup).sortBy('location.column').head(); if (firstTag) { validate(firstTag.location, type, modifier); } }); } function validateChildren(child, modifier = 0) { if (child.background) { validate(child.background.location, 'Background', modifier); child.background.steps.forEach(step => { validateStep(step, modifier); }); } else if (child.scenario) { validate(child.scenario.location, 'Scenario', modifier); validateTags(child.scenario.tags, 'scenario tag', modifier); child.scenario.steps.forEach(step => { validateStep(step, modifier); }); child.scenario.examples.forEach(example => { validate(example.location, 'Examples', modifier); validateTags(example.tags, 'examples tag', modifier); if (example.tableHeader) { validate(example.tableHeader.location, 'example', modifier); example.tableBody.forEach(row => { validate(row.location, 'example', modifier); }); } }); } } validate(feature.location, 'Feature'); validateTags(feature.tags, 'feature tag'); feature.children.forEach(child => { if (child.rule) { validate(child.rule.location, 'Rule'); validateTags(child.rule.tags, 'rule tag'); child.rule.children.forEach(ruleChild => { validateChildren(ruleChild, mergedConfiguration.RuleFallback ? mergedConfiguration.Rule : 0); }); } else { validateChildren(child); } }); return errors; } export function buildRuleErrors(error) { return { message: `Wrong indentation for "${error.type}", expected indentation level of ${error.expectedIndentation}, but got ${error.location.column - 1}`, rule: name, line: error.location.line, column: error.location.column, }; } export function fix(error, file, configuration) { const mergedConfiguration = mergeConfiguration(configuration); const lineContent = file.lines[error.location.line - 1]; const lineTrimmed = lineContent.trimStart(); const indentChar = mergedConfiguration.type === 'both' && [' ', '\t'].includes(lineContent[0]) ? lineContent[0] : (mergedConfiguration.type === 'both' ? mergedConfiguration.preferType : mergedConfiguration.type) === 'tab' ? '\t' : ' '; file.lines[error.location.line - 1] = lineTrimmed.padStart(lineTrimmed.length + error.expectedIndentation, indentChar); } export const documentation = { description: `Allows the user to specify indentation rules. This rule can be configured in a more granular level and uses following rules by default: * Expected indentation for Feature, Background, Scenario, Examples heading: 0 spaces * Expected indentation for Steps and each example: 2 spaces`, configuration: [{ name: 'Feature', type: 'number', description: 'Defines the indentation size for the node `Feature`.', default: availableConfigs.Feature, }, { name: 'Background', type: 'number', description: 'Defines the indentation size for the node `Background`.', default: availableConfigs.Background, }, { name: 'Rule', type: 'number', description: 'Defines the indentation size for the node `Rule`.', default: availableConfigs.Rule, }, { name: 'Scenario', type: 'number', description: 'Defines the indentation size for the node `Scenario`.', default: availableConfigs.Scenario, }, { name: 'Step', type: 'number', description: 'Defines the indentation size for the node `Step`.', default: availableConfigs.Step, }, { name: 'Examples', type: 'number', description: 'Defines the indentation size for the node `Examples`.', default: availableConfigs.Examples, }, { name: 'example', type: 'number', description: 'Defines the indentation size for the node `example`.', default: availableConfigs.example, }, { name: 'given', type: 'number', description: 'Defines the indentation size for the node `given`.', default: availableConfigs.given, }, { name: 'when', type: 'number', description: 'Defines the indentation size for the node `when`.', default: availableConfigs.when, }, { name: 'then', type: 'number', description: 'Defines the indentation size for the node `then`.', default: availableConfigs.then, }, { name: 'and', type: 'number', description: 'Defines the indentation size for the node `and`.', default: availableConfigs.and, }, { name: 'but', type: 'number', description: 'Defines the indentation size for the node `but`.', default: availableConfigs.but, }, { name: 'RuleFallback', type: 'number', description: 'If enabled, the indentation for nodes inside Rule is the sum of "Rule" and the node itself, else it uses the node directly.', default: availableConfigs.RuleFallback, }, { name: 'type', type: 'string', description: 'Defines the type of indentation to use. If `both`, it will allow spaces and tabs. If `space`, it will use spaces. If `tab`, it will use tabs.', default: availableConfigs.type, }, { name: 'preferType', type: 'string', description: 'Defines the preferred type of indentation to use. If `space`, it will use spaces. If `tab`, it will use tabs. (Only applies to auto-fixing)', default: availableConfigs.preferType, }], examples: [{ title: 'Example', description: 'Override some indentations', config: { [name]: ['error', { 'Feature': 0, 'Background': 2, 'Scenario': 2, 'Step': 4, 'Examples': 2, 'example': 3, 'given': 3, 'when': 3, 'then': 3, 'and': 3, 'but': 3, 'feature tag': 0, 'scenario tag': 2, 'examples tag': 2, }], }, }], }; //# sourceMappingURL=indentation.js.map