gherkin
Version:
334 lines (305 loc) • 10.3 kB
text/typescript
import AstNode from './AstNode'
import { messages, IdGenerator } from 'cucumber-messages'
import { RuleType, TokenType } from './Parser'
import Token from './Token'
import { AstBuilderException } from './Errors'
import createLocation from './cli/createLocation'
export default class AstBuilder {
private stack: AstNode[]
private comments: messages.GherkinDocument.IComment[]
private readonly newId: IdGenerator.NewId
constructor(newId: IdGenerator.NewId) {
this.newId = newId
if (!newId) {
throw new Error('No newId')
}
this.reset()
}
public reset() {
this.stack = [new AstNode(RuleType.None)]
this.comments = []
}
public startRule(ruleType: RuleType) {
this.stack.push(new AstNode(ruleType))
}
public endRule(ruleType: RuleType) {
const node = this.stack.pop()
const transformedNode = this.transformNode(node)
this.currentNode().add(node.ruleType, transformedNode)
}
public build(token: Token) {
if (token.matchedType === TokenType.Comment) {
this.comments.push(
messages.GherkinDocument.Comment.create({
location: this.getLocation(token),
text: token.matchedText,
})
)
} else {
this.currentNode().add(token.matchedType, token)
}
}
public getResult() {
return this.currentNode().getSingle(RuleType.GherkinDocument)
}
public currentNode() {
return this.stack[this.stack.length - 1]
}
public getLocation(token: Token, column?: number): messages.ILocation {
return !column
? token.location
: createLocation({ line: token.location.line, column })
}
public getTags(node: AstNode) {
const tags: messages.GherkinDocument.Feature.ITag[] = []
const tagsNode = node.getSingle(RuleType.Tags)
if (!tagsNode) {
return tags
}
const tokens = tagsNode.getTokens(TokenType.TagLine)
for (const token of tokens) {
for (const tagItem of token.matchedItems) {
tags.push(
messages.GherkinDocument.Feature.Tag.create({
location: this.getLocation(token, tagItem.column),
name: tagItem.text,
id: this.newId(),
})
)
}
}
return tags
}
public getCells(tableRowToken: Token) {
return tableRowToken.matchedItems.map(cellItem =>
messages.GherkinDocument.Feature.TableRow.TableCell.create({
location: this.getLocation(tableRowToken, cellItem.column),
value: cellItem.text,
})
)
}
public getDescription(node: AstNode) {
return node.getSingle(RuleType.Description)
}
public getSteps(node: AstNode) {
return node.getItems(RuleType.Step)
}
public getTableRows(node: AstNode) {
const rows = node.getTokens(TokenType.TableRow).map(token =>
messages.GherkinDocument.Feature.TableRow.create({
id: this.newId(),
location: this.getLocation(token),
cells: this.getCells(token),
})
)
this.ensureCellCount(rows)
return rows
}
public ensureCellCount(rows: any[]) {
if (rows.length === 0) {
return
}
const cellCount = rows[0].cells.length
rows.forEach(row => {
if (row.cells.length !== cellCount) {
throw AstBuilderException.create(
'inconsistent cell count within the table',
row.location
)
}
})
}
public transformNode(node: AstNode) {
switch (node.ruleType) {
case RuleType.Step: {
const stepLine = node.getToken(TokenType.StepLine)
const dataTable = node.getSingle(RuleType.DataTable)
const docString = node.getSingle(RuleType.DocString)
return messages.GherkinDocument.Feature.Step.create({
id: this.newId(),
location: this.getLocation(stepLine),
keyword: stepLine.matchedKeyword,
text: stepLine.matchedText,
dataTable,
docString,
})
}
case RuleType.DocString: {
const separatorToken = node.getTokens(TokenType.DocStringSeparator)[0]
const contentType =
separatorToken.matchedText.length > 0
? separatorToken.matchedText
: undefined
const lineTokens = node.getTokens(TokenType.Other)
const content = lineTokens.map(t => t.matchedText).join('\n')
const result = messages.GherkinDocument.Feature.Step.DocString.create({
location: this.getLocation(separatorToken),
content,
delimiter: separatorToken.line.trimmedLineText.substring(0, 3),
})
// conditionally add this like this (needed to make tests pass on node 0.10 as well as 4.0)
if (contentType) {
result.contentType = contentType
}
return result
}
case RuleType.DataTable: {
const rows = this.getTableRows(node)
return messages.GherkinDocument.Feature.Step.DataTable.create({
location: rows[0].location,
rows,
})
}
case RuleType.Background: {
const backgroundLine = node.getToken(TokenType.BackgroundLine)
const description = this.getDescription(node)
const steps = this.getSteps(node)
return messages.GherkinDocument.Feature.Background.create({
location: this.getLocation(backgroundLine),
keyword: backgroundLine.matchedKeyword,
name: backgroundLine.matchedText,
description,
steps,
})
}
case RuleType.ScenarioDefinition: {
const tags = this.getTags(node)
const scenarioNode = node.getSingle(RuleType.Scenario)
const scenarioLine = scenarioNode.getToken(TokenType.ScenarioLine)
const description = this.getDescription(scenarioNode)
const steps = this.getSteps(scenarioNode)
const examples = scenarioNode.getItems(RuleType.ExamplesDefinition)
return messages.GherkinDocument.Feature.Scenario.create({
id: this.newId(),
tags,
location: this.getLocation(scenarioLine),
keyword: scenarioLine.matchedKeyword,
name: scenarioLine.matchedText,
description,
steps,
examples,
})
}
case RuleType.ExamplesDefinition: {
const tags = this.getTags(node)
const examplesNode = node.getSingle(RuleType.Examples)
const examplesLine = examplesNode.getToken(TokenType.ExamplesLine)
const description = this.getDescription(examplesNode)
const exampleTable: messages.GherkinDocument.Feature.TableRow[] = examplesNode.getSingle(
RuleType.ExamplesTable
)
return messages.GherkinDocument.Feature.Scenario.Examples.create({
tags,
location: this.getLocation(examplesLine),
keyword: examplesLine.matchedKeyword,
name: examplesLine.matchedText,
description,
tableHeader: exampleTable !== undefined ? exampleTable[0] : undefined,
tableBody:
exampleTable !== undefined ? exampleTable.slice(1) : undefined,
})
}
case RuleType.ExamplesTable: {
return this.getTableRows(node)
}
case RuleType.Description: {
let lineTokens = node.getTokens(TokenType.Other)
// Trim trailing empty lines
let end = lineTokens.length
while (end > 0 && lineTokens[end - 1].line.trimmedLineText === '') {
end--
}
lineTokens = lineTokens.slice(0, end)
return lineTokens.map(token => token.matchedText).join('\n')
}
case RuleType.Feature: {
const header = node.getSingle(RuleType.FeatureHeader)
if (!header) {
return null
}
const tags = this.getTags(header)
const featureLine = header.getToken(TokenType.FeatureLine)
if (!featureLine) {
return null
}
const children = []
const background = node.getSingle(RuleType.Background)
if (background) {
children.push(
messages.GherkinDocument.Feature.FeatureChild.create({
background,
})
)
}
for (const scenario of node.getItems(RuleType.ScenarioDefinition)) {
children.push(
messages.GherkinDocument.Feature.FeatureChild.create({
scenario,
})
)
}
for (const rule of node.getItems(RuleType.Rule)) {
children.push(
messages.GherkinDocument.Feature.FeatureChild.create({
rule,
})
)
}
const description = this.getDescription(header)
const language = featureLine.matchedGherkinDialect
return messages.GherkinDocument.Feature.create({
tags,
location: this.getLocation(featureLine),
language,
keyword: featureLine.matchedKeyword,
name: featureLine.matchedText,
description,
children,
})
}
case RuleType.Rule: {
const header = node.getSingle(RuleType.RuleHeader)
if (!header) {
return null
}
const ruleLine = header.getToken(TokenType.RuleLine)
if (!ruleLine) {
return null
}
const children = []
const background = node.getSingle(RuleType.Background)
if (background) {
children.push(
messages.GherkinDocument.Feature.FeatureChild.create({
background,
})
)
}
for (const scenario of node.getItems(RuleType.ScenarioDefinition)) {
children.push(
messages.GherkinDocument.Feature.FeatureChild.create({
scenario,
})
)
}
const description = this.getDescription(header)
return messages.GherkinDocument.Feature.FeatureChild.Rule.create({
location: this.getLocation(ruleLine),
keyword: ruleLine.matchedKeyword,
name: ruleLine.matchedText,
description,
children,
})
}
case RuleType.GherkinDocument: {
const feature = node.getSingle(RuleType.Feature)
return messages.GherkinDocument.create({
feature,
comments: this.comments,
})
}
default:
return node
}
}
}