Skip to content

Commit

Permalink
Diagnostics for undefined steps in Scenario Outlines (#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
kieran-ryan committed May 12, 2024
1 parent 5f1f502 commit d913c5b
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Diagnostics for marking steps as undefined in scenario outlines ([#210](https://github.com/cucumber/language-service/pull/210))

## [1.5.1] - 2024-04-20
### Fixed
Expand Down
1 change: 1 addition & 0 deletions src/service/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const diagnosticCodeUndefinedStep = 'cucumber.undefined-step'
export const CONTAINS_PARAMETERS = /<.*?>/
88 changes: 72 additions & 16 deletions src/service/getGherkinDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Expression } from '@cucumber/cucumber-expressions'
import { dialects, Errors } from '@cucumber/gherkin'
import { walkGherkinDocument } from '@cucumber/gherkin-utils'
import * as messages from '@cucumber/messages'
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'

import { parseGherkinDocument } from '../gherkin/parseGherkinDocument.js'
import { diagnosticCodeUndefinedStep } from './constants.js'
import { CONTAINS_PARAMETERS, diagnosticCodeUndefinedStep } from './constants.js'

// https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnostic
export function getGherkinDiagnostics(
Expand Down Expand Up @@ -48,35 +49,72 @@ export function getGherkinDiagnostics(
const noStars = (keyword: string) => keyword !== '* '
const codeKeywords = [...dialect.given, ...dialect.when, ...dialect.then].filter(noStars)
let snippetKeyword = dialect.given.filter(noStars)[0]
let examples: readonly messages.Examples[]

return walkGherkinDocument<Diagnostic[]>(gherkinDocument, diagnostics, {
scenario(scenario, diagnostics) {
inScenarioOutline = (scenario.examples || []).length > 0
examples = inScenarioOutline ? scenario.examples : []
return diagnostics
},
step(step, diagnostics) {
if (inScenarioOutline) {
return diagnostics
}
if (codeKeywords.includes(step.keyword)) {
snippetKeyword = step.keyword
}
return inScenarioOutline
? getOutlineStepDiagnostics(step, diagnostics, expressions, snippetKeyword, examples)
: getStepDiagnostics(step, diagnostics, expressions, snippetKeyword)
},
})
}

if (isUndefined(step.text, expressions) && step.location.column !== undefined) {
const line = step.location.line - 1
const character = step.location.column - 1 + step.keyword.length
const diagnostic: Diagnostic = makeUndefinedStepDiagnostic(
line,
character,
step.keyword,
function getOutlineStepDiagnostics(
step: messages.Step,
diagnostics: Diagnostic[],
expressions: readonly Expression[],
snippetKeyword: string,
examples: readonly messages.Examples[]
): Diagnostic[] {
// Interpolate steps containing parameters
if (CONTAINS_PARAMETERS.test(step.text)) {
for (const example of examples) {
for (const row of example.tableBody) {
const stepText = interpolate(
step.text,
snippetKeyword
// @ts-ignore Can not be undefined with non-empty table body
example.tableHeader.cells,
row.cells
)
return diagnostics.concat(diagnostic)

if (!isUndefined(stepText, expressions)) {
return diagnostics
}
}
return diagnostics
},
})
}
}

return getStepDiagnostics(step, diagnostics, expressions, snippetKeyword)
}

function getStepDiagnostics(
step: messages.Step,
diagnostics: Diagnostic[],
expressions: readonly Expression[],
snippetKeyword: string
): Diagnostic[] {
if (isUndefined(step.text, expressions) && step.location.column !== undefined) {
const line = step.location.line - 1
const character = step.location.column - 1 + step.keyword.length
const diagnostic: Diagnostic = makeUndefinedStepDiagnostic(
line,
character,
step.keyword,
step.text,
snippetKeyword
)
return diagnostics.concat(diagnostic)
}
return diagnostics
}

export function makeUndefinedStepDiagnostic(
Expand Down Expand Up @@ -117,3 +155,21 @@ function isUndefined(stepText: string, expressions: readonly Expression[]): bool
}
return true
}

function interpolate(
name: string,
variableCells: readonly messages.TableCell[],
valueCells: readonly messages.TableCell[]
): string {
variableCells.forEach((variableCell, n) => {
const valueCell = valueCells[n]
const valuePattern = '<' + variableCell.value + '>'
const escapedPattern = valuePattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
const regexp = new RegExp(escapedPattern, 'g')
// JS Specific - dollar sign needs to be escaped with another dollar sign
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
const replacement = valueCell.value.replace(new RegExp('\\$', 'g'), '$$$$')
name = name.replace(regexp, replacement)
})
return name
}
63 changes: 55 additions & 8 deletions test/service/getGherkinDiagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,62 @@ describe('getGherkinDiagnostics', () => {
assert.deepStrictEqual(diagnostics, expectedDiagnostics)
})

it('does not return warning diagnostic for undefined step in Scenario Outline', () => {
const diagnostics = getGherkinDiagnostics(
`Feature: Hello
Scenario: Hi
Given an undefined step
it('returns diagnostic for undefined step with unreferenced parameter in Scenario Outline', () => {
const noExamples = `Feature:
Scenario Outline:
Given a <parameter>`
const noTableHeader = `Feature:
Scenario Outline:
Given a <parameter>
Examples:`
const noTableBody = `Feature:
Scenario Outline:
Given a <parameter>
Examples:
| parameter |`

Examples: Hello
`,
[]
for (const document of [noExamples, noTableHeader, noTableBody]) {
const diagnostics = getGherkinDiagnostics(document, [])
const expectedDiagnostics: Diagnostic[] = [
{
code: 'cucumber.undefined-step',
codeDescription: {
href: 'https://cucumber.io/docs/cucumber/step-definitions/',
},
data: {
snippetKeyword: 'Given ',
stepText: 'a <parameter>',
},
message: 'Undefined step: a <parameter>',
range: {
end: {
character: 27,
line: 2,
},
start: {
character: 14,
line: 2,
},
},
severity: 2,
source: 'Cucumber',
},
]
assert.deepStrictEqual(diagnostics, expectedDiagnostics)
}
})

it('returns no diagnostics with valid parameter match in Scenario Outline', () => {
const diagnostics = getGherkinDiagnostics(
`Feature:
Scenario Outline:
Given a <state> <entity>
Examples:
| parameter | state | entity | other |
| unused | defined | step | unused |
`,
[new CucumberExpression('a defined step', new ParameterTypeRegistry())]
)
const expectedDiagnostics: Diagnostic[] = []
assert.deepStrictEqual(diagnostics, expectedDiagnostics)
Expand Down

0 comments on commit d913c5b

Please sign in to comment.