Files
weave-scope/vendor/github.com/lsegal/gucumber/gherkin/parser.go
2016-03-23 15:41:37 +00:00

354 lines
7.2 KiB
Go

package gherkin
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
var reNL = regexp.MustCompile(`\r?\n`)
func ParseFilename(data, filename string) ([]Feature, error) {
lines := reNL.Split(data, -1)
p := parser{
lines: lines,
features: []Feature{},
translations: Translations[LANG_EN],
filename: filename,
}
if err := p.parse(); err != nil {
return nil, err
}
if len(p.features) == 0 {
return p.features, p.err("no features parsed")
}
return p.features, nil
}
func Parse(data string) ([]Feature, error) {
return ParseFilename(data, "")
}
type parser struct {
translations Translation
features []Feature
lines []string
lineNo int
lastLine int
started bool
filename string
}
func (p parser) err(msg string, args ...interface{}) error {
l := p.lineNo + 1
if l > len(p.lines) {
l = len(p.lines)
}
if p.filename == "" {
p.filename = "<unknown>.feature"
}
return fmt.Errorf("parse error (%s:%d): %s.",
filepath.Base(p.filename), l, fmt.Sprintf(msg, args...))
}
func (p *parser) line() string {
if p.lineNo < len(p.lines) {
return p.lines[p.lineNo]
}
return ""
}
func (p *parser) nextLine() bool {
p.lastLine = p.lineNo
if p.started {
p.lineNo++
}
p.started = true
for ; p.lineNo < len(p.lines); p.lineNo++ {
line, _ := p.lineStripped()
if line != "" && !strings.HasPrefix(line, "#") {
break
}
}
return p.stillReading()
}
func (p *parser) stillReading() bool {
return p.lineNo < len(p.lines)
}
func (p *parser) unread() {
if p.lineNo == 0 {
p.started = false
}
p.lineNo = p.lastLine
}
func (p *parser) parse() error {
for p.stillReading() {
if err := p.consumeFeature(); err != nil {
return err
}
}
return nil
}
func (p *parser) lineStripped() (string, int) {
line := p.line()
for i := 0; i < len(line); i++ {
c := line[i : i+1]
if c != " " && c != "\t" {
return line[i:], i
}
}
return line, 0
}
func (p *parser) consumeFeature() error {
desc := []string{}
tags, err := p.consumeTags()
if err != nil {
return err
}
if !p.nextLine() {
if len(tags) > 0 {
return p.err("tags not applied to feature")
}
return nil
}
line, startIndent := p.lineStripped()
parts := strings.SplitN(line, " ", 2)
prefix := p.translations.Feature + ":"
title := ""
if parts[0] != prefix {
return p.err("expected %q, found %q", prefix, line)
}
if len(parts) > 1 {
title = parts[1]
}
f := Feature{
Title: title, Tags: tags, Scenarios: []Scenario{},
Filename: p.filename, Line: p.lineNo,
}
var stags *[]string
seenScenario, seenBackground := false, false
for p.stillReading() { // consume until we get to Background or Scenario
// find indentation of next line
if p.nextLine() {
_, indent := p.lineStripped()
p.unread()
if indent <= startIndent { // de-dented
break // done parsing this feature
}
}
prevLine := p.lineNo
tags, err = p.consumeTags()
if err != nil {
return err
}
if p.lineNo != prevLine { // tags found
stags = &tags
}
if !p.nextLine() {
break
}
line, _ := p.lineStripped()
parts := strings.SplitN(line, ":", 2)
switch parts[0] {
case p.translations.Background:
if seenScenario {
return p.err("illegal background after scenario")
} else if seenBackground {
return p.err("multiple backgrounds not allowed")
}
seenBackground = true
b := Scenario{Filename: p.filename, Line: p.lineNo}
err = p.consumeScenario(&b)
if err != nil {
return err
}
if stags != nil {
b.Tags = *stags
} else {
b.Tags = []string{}
}
f.Background = b
stags = nil
case p.translations.Scenario, p.translations.Outline:
seenScenario = true
s := Scenario{Filename: p.filename, Line: p.lineNo}
err = p.consumeScenario(&s)
if err != nil {
return err
}
if stags != nil {
s.Tags = *stags
} else {
s.Tags = []string{}
}
if len(parts) > 1 {
s.Title = strings.TrimSpace(parts[1])
}
f.Scenarios = append(f.Scenarios, s)
stags = nil
default: // then this must be a description
if stags != nil {
return p.err("illegal description text after tags")
} else if seenScenario || seenBackground {
return p.err("illegal description text after scenario")
}
desc = append(desc, line)
}
}
f.Description = strings.Join(desc, "\n")
p.features = append(p.features, f)
return nil
}
func (p *parser) consumeScenario(scenario *Scenario) error {
scenario.Steps = []Step{}
_, startIndent := p.lineStripped()
for p.nextLine() { // consume all steps
_, indent := p.lineStripped()
if indent <= startIndent { // de-dented
p.unread()
break
}
if err := p.consumeStep(scenario); err != nil {
return err
}
}
return nil
}
func (p *parser) consumeStep(scenario *Scenario) error {
line, indent := p.lineStripped()
parts := strings.SplitN(line, " ", 2)
switch parts[0] {
case p.translations.Given, p.translations.Then,
p.translations.When, p.translations.And:
var arg StringData
if len(parts) < 2 {
return p.err("expected step text after %q", parts[0])
}
if p.nextLine() {
l, _ := p.lineStripped()
p.unread()
if len(l) > 0 && (l[0] == '|' || l == `"""`) {
// this is step argument data
arg = p.consumeIndentedData(indent)
}
}
var stype StepType
switch parts[0] {
case p.translations.Given:
stype = StepType("Given")
case p.translations.When:
stype = StepType("When")
case p.translations.Then:
stype = StepType("Then")
case p.translations.And:
stype = StepType("And")
}
s := Step{
Filename: p.filename, Line: p.lineNo,
Type: stype, Text: parts[1], Argument: arg,
}
scenario.Steps = append(scenario.Steps, s)
case p.translations.Examples + ":":
scenario.Examples = p.consumeIndentedData(indent)
default:
return p.err("illegal step prefix %q", parts[0])
}
return nil
}
func (p *parser) consumeIndentedData(scenarioIndent int) StringData {
stringData := []string{}
startIndent, quoted := -1, false
for p.nextLine() {
var line string
var indent int
if startIndent == -1 { // first line
line, indent = p.lineStripped()
startIndent = indent
if line == `"""` {
quoted = true // this is a docstring data block
continue // ignore this from data
}
} else {
line = p.line()
if len(line) <= startIndent {
// not enough text, not part of indented data
p.unread()
break
}
if line[0:startIndent] != strings.Repeat(" ", startIndent) {
// not enough indentation, not part of indented data
p.unread()
break
}
line = line[startIndent:]
if !quoted && line[0] != '|' {
// tabular data must start with | on each line
p.unread()
break
}
if quoted && line == `"""` { // end quote on docstring block
break
}
}
stringData = append(stringData, line)
}
return StringData(strings.Join(stringData, "\n"))
}
func (p *parser) consumeTags() ([]string, error) {
tags := []string{}
if !p.nextLine() {
return tags, nil
}
line, _ := p.lineStripped()
if len(p.lines) == 0 || !strings.HasPrefix(line, "@") {
p.unread()
return tags, nil
}
for _, t := range strings.Split(line, " ") {
if t == "" {
continue
}
if t[0:1] != "@" {
return nil, p.err("invalid tag %q", t)
}
tags = append(tags, t)
}
return tags, nil
}