UNPKG

@cucumber/gherkin

Version:
321 lines (292 loc) 9.71 kB
import type * as messages from '@cucumber/messages' import AstNode from './AstNode' import { AstBuilderException } from './Errors' import type { IAstBuilder } from './IAstBuilder' import type IToken from './IToken' import { RuleType, TokenType } from './Parser' export default class AstBuilder implements IAstBuilder<AstNode, TokenType, RuleType> { stack: AstNode[] comments: messages.Comment[] readonly newId: messages.IdGenerator.NewId constructor(newId: messages.IdGenerator.NewId) { this.newId = newId if (!newId) { throw new Error('No newId') } this.reset() } reset() { this.stack = [new AstNode(RuleType.None)] this.comments = [] } startRule(ruleType: RuleType) { this.stack.push(new AstNode(ruleType)) } endRule() { const node = this.stack.pop() const transformedNode = this.transformNode(node) this.currentNode().add(node.ruleType, transformedNode) } build(token: IToken<TokenType>) { if (token.matchedType === TokenType.Comment) { this.comments.push({ location: this.getLocation(token), text: token.matchedText, }) } else { this.currentNode().add(token.matchedType, token) } } getResult() { return this.currentNode().getSingle(RuleType.GherkinDocument) } currentNode() { return this.stack[this.stack.length - 1] } getLocation(token: IToken<TokenType>, column?: number): messages.Location { return !column ? token.location : { line: token.location.line, column } } getTags(node: AstNode) { const tags: messages.Tag[] = [] 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({ location: this.getLocation(token, tagItem.column), name: tagItem.text, id: this.newId(), }) } } return tags } getCells(tableRowToken: IToken<TokenType>) { return tableRowToken.matchedItems.map((cellItem) => ({ location: this.getLocation(tableRowToken, cellItem.column), value: cellItem.text, })) } getDescription(node: AstNode) { return node.getSingle(RuleType.Description) || '' } getSteps(node: AstNode) { return node.getItems(RuleType.Step) } getTableRows(node: AstNode) { const rows = node.getTokens(TokenType.TableRow).map((token) => ({ id: this.newId(), location: this.getLocation(token), cells: this.getCells(token), })) this.ensureCellCount(rows) return rows.length === 0 ? [] : rows } ensureCellCount(rows: messages.TableRow[]) { 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) } }) } 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) const location = this.getLocation(stepLine) const step: messages.Step = { id: this.newId(), location, keyword: stepLine.matchedKeyword, keywordType: stepLine.matchedKeywordType, text: stepLine.matchedText, dataTable: dataTable, docString: docString, } return step } case RuleType.DocString: { const separatorToken = node.getTokens(TokenType.DocStringSeparator)[0] const mediaType = 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.DocString = { location: this.getLocation(separatorToken), content, delimiter: separatorToken.matchedKeyword, } // conditionally add this like this (needed to make tests pass on node 0.10 as well as 4.0) if (mediaType) { result.mediaType = mediaType } return result } case RuleType.DataTable: { const rows = this.getTableRows(node) const dataTable: messages.DataTable = { location: rows[0].location, rows, } return dataTable } case RuleType.Background: { const backgroundLine = node.getToken(TokenType.BackgroundLine) const description = this.getDescription(node) const steps = this.getSteps(node) const background: messages.Background = { id: this.newId(), location: this.getLocation(backgroundLine), keyword: backgroundLine.matchedKeyword, name: backgroundLine.matchedText, description, steps, } return background } 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) const scenario: messages.Scenario = { id: this.newId(), tags, location: this.getLocation(scenarioLine), keyword: scenarioLine.matchedKeyword, name: scenarioLine.matchedText, description, steps, examples, } return scenario } 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 examplesTable: messages.TableRow[] = examplesNode.getSingle(RuleType.ExamplesTable) const examples: messages.Examples = { id: this.newId(), tags, location: this.getLocation(examplesLine), keyword: examplesLine.matchedKeyword, name: examplesLine.matchedText, description, tableHeader: examplesTable ? examplesTable[0] : undefined, tableBody: examplesTable ? examplesTable.slice(1) : [], } return examples } 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: messages.FeatureChild[] = [] const background = node.getSingle(RuleType.Background) if (background) { children.push({ background, }) } for (const scenario of node.getItems(RuleType.ScenarioDefinition)) { children.push({ scenario, }) } for (const rule of node.getItems(RuleType.Rule)) { children.push({ rule, }) } const description = this.getDescription(header) const language = featureLine.matchedGherkinDialect const feature: messages.Feature = { tags, location: this.getLocation(featureLine), language, keyword: featureLine.matchedKeyword, name: featureLine.matchedText, description, children, } return feature } case RuleType.Rule: { const header = node.getSingle(RuleType.RuleHeader) if (!header) { return null } const ruleLine = header.getToken(TokenType.RuleLine) if (!ruleLine) { return null } const tags = this.getTags(header) const children: messages.RuleChild[] = [] const background = node.getSingle(RuleType.Background) if (background) { children.push({ background, }) } for (const scenario of node.getItems(RuleType.ScenarioDefinition)) { children.push({ scenario, }) } const description = this.getDescription(header) const rule: messages.Rule = { id: this.newId(), location: this.getLocation(ruleLine), keyword: ruleLine.matchedKeyword, name: ruleLine.matchedText, description, children, tags, } return rule } case RuleType.GherkinDocument: { const feature = node.getSingle(RuleType.Feature) const gherkinDocument: messages.GherkinDocument = { feature, comments: this.comments, } return gherkinDocument } default: return node } } }