378 строки
9,1 КиБ
Go
378 строки
9,1 КиБ
Go
package gherkin
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
type AstBuilder interface {
|
|
Builder
|
|
GetFeature() *Feature
|
|
}
|
|
|
|
type astBuilder struct {
|
|
stack []*astNode
|
|
comments []*Comment
|
|
}
|
|
|
|
func (t *astBuilder) Reset() {
|
|
t.comments = []*Comment{}
|
|
t.stack = []*astNode{}
|
|
t.push(newAstNode(RuleType_None))
|
|
}
|
|
|
|
func (t *astBuilder) GetFeature() *Feature {
|
|
res := t.currentNode().getSingle(RuleType_Feature)
|
|
if val, ok := res.(*Feature); ok {
|
|
return val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type astNode struct {
|
|
ruleType RuleType
|
|
subNodes map[RuleType][]interface{}
|
|
}
|
|
|
|
func (a *astNode) add(rt RuleType, obj interface{}) {
|
|
a.subNodes[rt] = append(a.subNodes[rt], obj)
|
|
}
|
|
|
|
func (a *astNode) getSingle(rt RuleType) interface{} {
|
|
if val, ok := a.subNodes[rt]; ok {
|
|
for i := range val {
|
|
return val[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *astNode) getItems(rt RuleType) []interface{} {
|
|
var res []interface{}
|
|
if val, ok := a.subNodes[rt]; ok {
|
|
for i := range val {
|
|
res = append(res, val[i])
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (a *astNode) getToken(tt TokenType) *Token {
|
|
if val, ok := a.getSingle(tt.RuleType()).(*Token); ok {
|
|
return val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *astNode) getTokens(tt TokenType) []*Token {
|
|
var items = a.getItems(tt.RuleType())
|
|
var tokens []*Token
|
|
for i := range items {
|
|
if val, ok := items[i].(*Token); ok {
|
|
tokens = append(tokens, val)
|
|
}
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func (t *astBuilder) currentNode() *astNode {
|
|
if len(t.stack) > 0 {
|
|
return t.stack[len(t.stack)-1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newAstNode(rt RuleType) *astNode {
|
|
return &astNode{
|
|
ruleType: rt,
|
|
subNodes: make(map[RuleType][]interface{}),
|
|
}
|
|
}
|
|
|
|
func NewAstBuilder() AstBuilder {
|
|
builder := new(astBuilder)
|
|
builder.comments = []*Comment{}
|
|
builder.push(newAstNode(RuleType_None))
|
|
return builder
|
|
}
|
|
|
|
func (t *astBuilder) push(n *astNode) {
|
|
t.stack = append(t.stack, n)
|
|
}
|
|
|
|
func (t *astBuilder) pop() *astNode {
|
|
x := t.stack[len(t.stack)-1]
|
|
t.stack = t.stack[:len(t.stack)-1]
|
|
return x
|
|
}
|
|
|
|
func (t *astBuilder) Build(tok *Token) (bool, error) {
|
|
if tok.Type == TokenType_Comment {
|
|
comment := new(Comment)
|
|
comment.Type = "Comment"
|
|
comment.Location = astLocation(tok)
|
|
comment.Text = tok.Text
|
|
t.comments = append(t.comments, comment)
|
|
} else {
|
|
t.currentNode().add(tok.Type.RuleType(), tok)
|
|
}
|
|
return true, nil
|
|
}
|
|
func (t *astBuilder) StartRule(r RuleType) (bool, error) {
|
|
t.push(newAstNode(r))
|
|
return true, nil
|
|
}
|
|
func (t *astBuilder) EndRule(r RuleType) (bool, error) {
|
|
node := t.pop()
|
|
transformedNode, err := t.transformNode(node)
|
|
t.currentNode().add(node.ruleType, transformedNode)
|
|
return true, err
|
|
}
|
|
|
|
func (t *astBuilder) transformNode(node *astNode) (interface{}, error) {
|
|
switch node.ruleType {
|
|
|
|
case RuleType_Step:
|
|
stepLine := node.getToken(TokenType_StepLine)
|
|
step := new(Step)
|
|
step.Type = "Step"
|
|
step.Location = astLocation(stepLine)
|
|
step.Keyword = stepLine.Keyword
|
|
step.Text = stepLine.Text
|
|
step.Argument = node.getSingle(RuleType_DataTable)
|
|
if step.Argument == nil {
|
|
step.Argument = node.getSingle(RuleType_DocString)
|
|
}
|
|
return step, nil
|
|
|
|
case RuleType_DocString:
|
|
separatorToken := node.getToken(TokenType_DocStringSeparator)
|
|
contentType := separatorToken.Text
|
|
lineTokens := node.getTokens(TokenType_Other)
|
|
var text string
|
|
for i := range lineTokens {
|
|
if i > 0 {
|
|
text += "\n"
|
|
}
|
|
text += lineTokens[i].Text
|
|
}
|
|
ds := new(DocString)
|
|
ds.Type = "DocString"
|
|
ds.Location = astLocation(separatorToken)
|
|
ds.ContentType = contentType
|
|
ds.Content = text
|
|
ds.Delimitter = DOCSTRING_SEPARATOR // TODO: remember separator
|
|
return ds, nil
|
|
|
|
case RuleType_DataTable:
|
|
rows, err := astTableRows(node)
|
|
dt := new(DataTable)
|
|
dt.Type = "DataTable"
|
|
dt.Location = rows[0].Location
|
|
dt.Rows = rows
|
|
return dt, err
|
|
|
|
case RuleType_Background:
|
|
backgroundLine := node.getToken(TokenType_BackgroundLine)
|
|
description, _ := node.getSingle(RuleType_Description).(string)
|
|
bg := new(Background)
|
|
bg.Type = "Background"
|
|
bg.Location = astLocation(backgroundLine)
|
|
bg.Keyword = backgroundLine.Keyword
|
|
bg.Name = backgroundLine.Text
|
|
bg.Description = description
|
|
bg.Steps = astSteps(node)
|
|
return bg, nil
|
|
|
|
case RuleType_Scenario_Definition:
|
|
tags := astTags(node)
|
|
scenarioNode, _ := node.getSingle(RuleType_Scenario).(*astNode)
|
|
if scenarioNode != nil {
|
|
scenarioLine := scenarioNode.getToken(TokenType_ScenarioLine)
|
|
description, _ := scenarioNode.getSingle(RuleType_Description).(string)
|
|
sc := new(Scenario)
|
|
sc.Type = "Scenario"
|
|
sc.Tags = tags
|
|
sc.Location = astLocation(scenarioLine)
|
|
sc.Keyword = scenarioLine.Keyword
|
|
sc.Name = scenarioLine.Text
|
|
sc.Description = description
|
|
sc.Steps = astSteps(scenarioNode)
|
|
return sc, nil
|
|
} else {
|
|
scenarioOutlineNode, ok := node.getSingle(RuleType_ScenarioOutline).(*astNode)
|
|
if !ok {
|
|
panic("Internal grammar error")
|
|
}
|
|
scenarioOutlineLine := scenarioOutlineNode.getToken(TokenType_ScenarioOutlineLine)
|
|
description, _ := scenarioOutlineNode.getSingle(RuleType_Description).(string)
|
|
sc := new(ScenarioOutline)
|
|
sc.Type = "ScenarioOutline"
|
|
sc.Tags = tags
|
|
sc.Location = astLocation(scenarioOutlineLine)
|
|
sc.Keyword = scenarioOutlineLine.Keyword
|
|
sc.Name = scenarioOutlineLine.Text
|
|
sc.Description = description
|
|
sc.Steps = astSteps(scenarioOutlineNode)
|
|
sc.Examples = astExamples(scenarioOutlineNode)
|
|
return sc, nil
|
|
}
|
|
|
|
case RuleType_Examples_Definition:
|
|
tags := astTags(node)
|
|
examplesNode, _ := node.getSingle(RuleType_Examples).(*astNode)
|
|
examplesLine := examplesNode.getToken(TokenType_ExamplesLine)
|
|
description, _ := examplesNode.getSingle(RuleType_Description).(string)
|
|
allRows, err := astTableRows(examplesNode)
|
|
ex := new(Examples)
|
|
ex.Type = "Examples"
|
|
ex.Tags = tags
|
|
ex.Location = astLocation(examplesLine)
|
|
ex.Keyword = examplesLine.Keyword
|
|
ex.Name = examplesLine.Text
|
|
ex.Description = description
|
|
ex.TableHeader = allRows[0]
|
|
ex.TableBody = allRows[1:]
|
|
return ex, err
|
|
|
|
case RuleType_Description:
|
|
lineTokens := node.getTokens(TokenType_Other)
|
|
// Trim trailing empty lines
|
|
end := len(lineTokens)
|
|
for end > 0 && strings.TrimSpace(lineTokens[end-1].Text) == "" {
|
|
end--
|
|
}
|
|
var desc []string
|
|
for i := range lineTokens[0:end] {
|
|
desc = append(desc, lineTokens[i].Text)
|
|
}
|
|
return strings.Join(desc, "\n"), nil
|
|
|
|
case RuleType_Feature:
|
|
header, ok := node.getSingle(RuleType_Feature_Header).(*astNode)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
tags := astTags(header)
|
|
featureLine := header.getToken(TokenType_FeatureLine)
|
|
if featureLine == nil {
|
|
return nil, nil
|
|
}
|
|
background, _ := node.getSingle(RuleType_Background).(*Background)
|
|
scenarioDefinitions := node.getItems(RuleType_Scenario_Definition)
|
|
if scenarioDefinitions == nil {
|
|
scenarioDefinitions = []interface{}{}
|
|
}
|
|
description, _ := header.getSingle(RuleType_Description).(string)
|
|
|
|
feat := new(Feature)
|
|
feat.Type = "Feature"
|
|
feat.Tags = tags
|
|
feat.Location = astLocation(featureLine)
|
|
feat.Language = featureLine.GherkinDialect
|
|
feat.Keyword = featureLine.Keyword
|
|
feat.Name = featureLine.Text
|
|
feat.Description = description
|
|
feat.Background = background
|
|
feat.ScenarioDefinitions = scenarioDefinitions
|
|
feat.Comments = t.comments
|
|
return feat, nil
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func astLocation(t *Token) *Location {
|
|
return &Location{
|
|
Line: t.Location.Line,
|
|
Column: t.Location.Column,
|
|
}
|
|
}
|
|
|
|
func astTableRows(t *astNode) (rows []*TableRow, err error) {
|
|
rows = []*TableRow{}
|
|
tokens := t.getTokens(TokenType_TableRow)
|
|
for i := range tokens {
|
|
row := new(TableRow)
|
|
row.Type = "TableRow"
|
|
row.Location = astLocation(tokens[i])
|
|
row.Cells = astTableCells(tokens[i])
|
|
rows = append(rows, row)
|
|
}
|
|
err = ensureCellCount(rows)
|
|
return
|
|
}
|
|
|
|
func ensureCellCount(rows []*TableRow) error {
|
|
if len(rows) <= 1 {
|
|
return nil
|
|
}
|
|
cellCount := len(rows[0].Cells)
|
|
for i := range rows {
|
|
if cellCount != len(rows[i].Cells) {
|
|
return &parseError{"inconsistent cell count within the table", &Location{
|
|
Line: rows[i].Location.Line,
|
|
Column: rows[i].Location.Column,
|
|
}}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func astTableCells(t *Token) (cells []*TableCell) {
|
|
cells = []*TableCell{}
|
|
for i := range t.Items {
|
|
item := t.Items[i]
|
|
cell := new(TableCell)
|
|
cell.Type = "TableCell"
|
|
cell.Location = &Location{
|
|
Line: t.Location.Line,
|
|
Column: item.Column,
|
|
}
|
|
cell.Value = item.Text
|
|
cells = append(cells, cell)
|
|
}
|
|
return
|
|
}
|
|
|
|
func astSteps(t *astNode) (steps []*Step) {
|
|
steps = []*Step{}
|
|
tokens := t.getItems(RuleType_Step)
|
|
for i := range tokens {
|
|
step, _ := tokens[i].(*Step)
|
|
steps = append(steps, step)
|
|
}
|
|
return
|
|
}
|
|
|
|
func astExamples(t *astNode) (examples []*Examples) {
|
|
examples = []*Examples{}
|
|
tokens := t.getItems(RuleType_Examples_Definition)
|
|
for i := range tokens {
|
|
example, _ := tokens[i].(*Examples)
|
|
examples = append(examples, example)
|
|
}
|
|
return
|
|
}
|
|
|
|
func astTags(node *astNode) (tags []*Tag) {
|
|
tags = []*Tag{}
|
|
tagsNode, ok := node.getSingle(RuleType_Tags).(*astNode)
|
|
if !ok {
|
|
return
|
|
}
|
|
tokens := tagsNode.getTokens(TokenType_TagLine)
|
|
for i := range tokens {
|
|
token := tokens[i]
|
|
for k := range token.Items {
|
|
item := token.Items[k]
|
|
tag := new(Tag)
|
|
tag.Type = "Tag"
|
|
tag.Location = &Location{
|
|
Line: token.Location.Line,
|
|
Column: item.Column,
|
|
}
|
|
tag.Name = item.Text
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
return
|
|
}
|