Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to disable rules via comments #56

Merged
merged 2 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@
</p>
</div>

# Contents
* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
* [Disabling Rules](#disabling-rules)
* [Automatic Fixing](#automatic-fixing)
* [Custom Rules](#custom-rules)
* [Generating Custom Rules](#generating-custom-rules)
* [Reporting](#reporting)
* [Reporter Configuratio](#reporter-configuration)
* [Contributing](#contributing)
* [Testing](#testing)

# Installation
Gherklin can be installed either via NPM or Yarn
```shell
npm install gherklin
```
```shell
yarn add gherklin
```

# Usage

#### Bin script
Expand Down Expand Up @@ -118,6 +140,16 @@ While `gherklin-disable` works for every rule, `gherklin-disable-next-line` only
where it makes sense.
For example, using `gherklin-disable-next-line` does not make sense for the `no-empty-file` rule.

You can also disable specific rules for the file, using the # gherklin-disable rule-name.

### Example
```gherkin
# gherklin-disable allowed-tags, no-unnamed-scenario
Feature: The below tag is invalid
@invalid-tag
Scenario:
```

# Automatic Fixing

**Gherklin** doesn't support automatic fixes that you may be used to with things like ESLint and
Expand Down
44 changes: 43 additions & 1 deletion src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ export default class Document {

public errors: Array<LintError> = []

// If true, this document has rule validation disabled
public disabled: boolean = false

// A list of lines that are disabled by the gherklin-disable-next-line comment
// Uses a map of line number => boolean for faster lookups
public linesDisabled: Map<number, boolean> = new Map()

// A list of rules that are disabled by the gherklin-disable rule-name comment
public rulesDisabled: Map<string, boolean> = new Map()

public constructor(filename: string) {
this.filename = filename
}
Expand All @@ -31,10 +41,42 @@ export default class Document {

if (gherkinDocument.feature) {
this.feature = gherkinDocument.feature

gherkinDocument.comments.forEach((comment) => {
const text = comment.text.trim()

if (comment.location.line === 1) {
if (text === '# gherklin-disable') {
this.disabled = true
return
}

const disableRuleMatch = text.match(/^# gherklin-disable ([a-zA-Z0-9-,\s]+)$/)
if (disableRuleMatch) {
const rules = (disableRuleMatch[1] || '').split(',')
rules.forEach((rule) => {
this.rulesDisabled.set(rule.trim(), true)
})
}
}

if (text === '# gherklin-disable-next-line') {
this.linesDisabled.set(comment.location.line + 1, true)
}
})
}
}

public addError = (message: string, location: Location): void => {
public addError = (ruleName: string, message: string, location: Location): void => {
// Don't add the error if the line has been disabled
if (this.linesDisabled.get(location.line)) {
return
}

if (this.rulesDisabled.get(ruleName) === true) {
return
}

this.errors.push({
message,
location,
Expand Down
3 changes: 1 addition & 2 deletions src/reporters/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { LintError } from '../error'
import { ReporterConfig } from '../types'
import { LintError, ReporterConfig } from '../types'

export default class Reporter {
protected readonly config: ReporterConfig
Expand Down
2 changes: 1 addition & 1 deletion src/rule_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default class RuleLoader {
const errors: Array<LintError> = []

for (const rule of this.rules) {
if (!rule.schema.enabled) {
if (!rule.schema.enabled || document.disabled) {
continue
}

Expand Down
2 changes: 2 additions & 0 deletions src/rules/allowed-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default class AllowedTags implements Rule {
document.feature.tags.forEach((tag) => {
if (!allowedTags.includes(tag.name)) {
document.addError(
this.name,
`Found a feature tag that is not allowed. Got ${tag.name}, wanted ${Array.isArray(allowedTags) ? allowedTags.join(', ') : allowedTags}`,
tag.location,
)
Expand All @@ -38,6 +39,7 @@ export default class AllowedTags implements Rule {
child.scenario.tags.forEach((tag) => {
if (!allowedTags.includes(tag.name)) {
document.addError(
this.name,
`Found a scenario tag that is not allowed. Got ${tag.name}, wanted ${Array.isArray(allowedTags) ? allowedTags.join(', ') : allowedTags}`,
tag.location,
)
Expand Down
8 changes: 8 additions & 0 deletions src/rules/indentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class Indentation implements Rule {
if (args.feature !== undefined) {
if (document.feature.location.column !== args.feature) {
document.addError(
this.name,
`Invalid indentation for feature. Got ${document.feature.location.column}, wanted ${args.feature}`,
document.feature.location,
)
Expand All @@ -31,6 +32,7 @@ export default class Indentation implements Rule {
if (child.background && args.background !== undefined) {
if (child.background.location.column !== args.background) {
document.addError(
this.name,
`Invalid indentation for background. Got ${child.background.location.column}, wanted ${args.background}`,
child.background.location,
)
Expand All @@ -40,6 +42,7 @@ export default class Indentation implements Rule {
if (child.scenario && args.scenario !== undefined) {
if (child.scenario.location.column !== args.scenario) {
document.addError(
this.name,
`Invalid indentation for scenario. Got ${child.scenario.location.column}, wanted ${args.scenario}`,
child.scenario.location,
)
Expand All @@ -51,6 +54,7 @@ export default class Indentation implements Rule {
if (step.keyword.toLowerCase() in args) {
if (step.location.column !== args[step.keyword.toLowerCase()]) {
document.addError(
this.name,
`Invalid indentation for "${step.keyword.toLowerCase()}". Got ${step.location.column}, wanted ${args[step.keyword.toLowerCase()]}`,
child.background.location,
)
Expand All @@ -65,6 +69,7 @@ export default class Indentation implements Rule {
if (stepNormalized in args) {
if (step.location.column !== args[stepNormalized]) {
document.addError(
this.name,
`Invalid indentation for "${stepNormalized}". Got ${step.location.column}, wanted ${args[stepNormalized]}`,
step.location,
)
Expand All @@ -76,6 +81,7 @@ export default class Indentation implements Rule {
child.scenario.examples.forEach((example) => {
if (example.location.column !== args.examples) {
document.addError(
this.name,
`Invalid indentation for "examples". Got ${example.location.column}, wanted ${args.examples}`,
example.location,
)
Expand All @@ -84,6 +90,7 @@ export default class Indentation implements Rule {
if (example.tableHeader && args.exampleTableHeader !== undefined) {
if (example.tableHeader.location.column !== args.exampleTableHeader) {
document.addError(
this.name,
`Invalid indentation for "example table header". Got ${example.tableHeader.location.column}, wanted ${args.exampleTableHeader}`,
example.location,
)
Expand All @@ -94,6 +101,7 @@ export default class Indentation implements Rule {
example.tableBody.forEach((row) => {
if (row.location.column !== args.exampleTableBody) {
document.addError(
this.name,
`Invalid indentation for "example table row". Got ${row.location.column}, wanted ${args.exampleTableBody}`,
example.location,
)
Expand Down
3 changes: 3 additions & 0 deletions src/rules/keywords-in-logical-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@ export default class KeywordsInLogicalOrder implements Rule {

if (given.includes(step.keyword) && !when.includes(nextStep.keyword)) {
document.addError(
this.name,
`Expected "${step.keyword.trim()}" to be followed by "${trimmedWhen.join(', ')}", got "${nextTrimmed}"`,
step.location,
)
}

if (when.includes(step.keyword) && !then.includes(nextStep.keyword)) {
document.addError(
this.name,
`Expected "${step.keyword.trim()}" to be followed by "${trimmedThen.join(', ')}", got "${nextTrimmed}"`,
step.location,
)
}

if (then.includes(step.keyword) && ![...and, ...when].includes(nextStep.keyword)) {
document.addError(
this.name,
`Expected "${step.keyword.trim()}" to be followed by "${[...trimmedAnd, ...trimmedWhen].join(', ')}", got "${nextTrimmed}"`,
step.location,
)
Expand Down
1 change: 1 addition & 0 deletions src/rules/max-scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class MaxScenarios implements Rule {
const expected = this.schema.args as number
if (scenarioCount > expected) {
document.addError(
this.name,
`Expected max ${expected} scenarios per file. Found ${scenarioCount}.`,
document.feature.location,
)
Expand Down
2 changes: 1 addition & 1 deletion src/rules/new-line-at-eof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class NewLineAtEof implements Rule {

const lastLine = lines[lines.length - 1]
if (lastLine !== '') {
document.addError('No new line at end of file.', {
document.addError(this.name, 'No new line at end of file.', {
line: lines.length,
column: 0,
})
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-background-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class NoBackgroundOnly implements Rule {
}

if (document.feature.children.length < 2) {
document.addError(`File contains only a background.`, document.feature.location)
document.addError(this.name, `File contains only a background.`, document.feature.location)
}
})
}
Expand Down
1 change: 1 addition & 0 deletions src/rules/no-dupe-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class NoDupeFeatures implements Rule {
} else {
this.features.set(featureName, [path.basename(document.filename), ...this.features.get(featureName)])
document.addError(
this.name,
`Found duplicate feature "${featureName}" in "${this.features.get(featureName).join(', ')}".`,
document.feature.location,
)
Expand Down
1 change: 1 addition & 0 deletions src/rules/no-dupe-scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default class NoDupeScenarios implements Rule {
this.scenarios.set(scenarioName, [path.basename(document.filename), ...this.scenarios.get(scenarioName)])
}
document.addError(
this.name,
`Found duplicate scenario "${scenarioName}" in "${this.scenarios.get(scenarioName).join(', ')}".`,
child.scenario.location,
)
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-empty-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default class NoEmptyFile implements Rule {

public async run(document: Document): Promise<void> {
if (document.feature.keyword === '') {
document.addError('Feature file is empty.', {
document.addError(this.name, 'Feature file is empty.', {
line: 0,
column: 0,
})
Expand Down
1 change: 1 addition & 0 deletions src/rules/no-similar-scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default class NoSimilarScenarios implements Rule {

if (percentage > threshold) {
document.addError(
this.name,
`Scenario "${child.scenario.name}" is too similar (> ${threshold}%) to scenario "${other.name}".`,
child.scenario.location,
)
Expand Down
1 change: 1 addition & 0 deletions src/rules/no-single-example-outline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default class NoSingleExampleOutline implements Rule {

if (totalExamples === 1) {
document.addError(
this.name,
'Scenario Outline has only one example. Consider converting to a simple Scenario.',
child.scenario.location,
)
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-trailing-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class NoTrailingSpaces implements Rule {

for await (const line of rl) {
if (line.charCodeAt(line.length - 1) === 32) {
document.addError('Found trailing whitespace.', {
document.addError(this.name, 'Found trailing whitespace.', {
line: lineNumber,
column: line.length,
})
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-unnamed-scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class NoUnnamedScenarios implements Rule {
}

if (child.scenario.name.length === 0) {
document.addError('Found scenario with no name.', child.scenario.location)
document.addError(this.name, 'Found scenario with no name.', child.scenario.location)
}
})
}
Expand Down
Loading