github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/parser/specparser.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package parser
    19  
    20  import (
    21  	"bufio"
    22  	"fmt"
    23  	"regexp"
    24  	"strings"
    25  
    26  	"github.com/getgauge/common"
    27  	"github.com/getgauge/gauge/gauge"
    28  	"github.com/getgauge/gauge/gauge_messages"
    29  )
    30  
    31  type SpecParser struct {
    32  	scanner           *bufio.Scanner
    33  	lineNo            int
    34  	tokens            []*Token
    35  	currentState      int
    36  	processors        map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool)
    37  	conceptDictionary *gauge.ConceptDictionary
    38  }
    39  
    40  const (
    41  	initial             = 1 << iota
    42  	specScope           = 1 << iota
    43  	scenarioScope       = 1 << iota
    44  	commentScope        = 1 << iota
    45  	tableScope          = 1 << iota
    46  	tableSeparatorScope = 1 << iota
    47  	tableDataScope      = 1 << iota
    48  	stepScope           = 1 << iota
    49  	contextScope        = 1 << iota
    50  	tearDownScope       = 1 << iota
    51  	conceptScope        = 1 << iota
    52  	keywordScope        = 1 << iota
    53  	tagsScope           = 1 << iota
    54  )
    55  
    56  func (parser *SpecParser) initialize() {
    57  	parser.processors = make(map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool))
    58  	parser.processors[gauge.SpecKind] = processSpec
    59  	parser.processors[gauge.ScenarioKind] = processScenario
    60  	parser.processors[gauge.CommentKind] = processComment
    61  	parser.processors[gauge.StepKind] = processStep
    62  	parser.processors[gauge.TagKind] = processTag
    63  	parser.processors[gauge.TableHeader] = processTable
    64  	parser.processors[gauge.TableRow] = processTable
    65  	parser.processors[gauge.DataTableKind] = processDataTable
    66  	parser.processors[gauge.TearDownKind] = processTearDown
    67  }
    68  
    69  func (parser *SpecParser) Parse(specText string, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult) {
    70  	tokens, errs := parser.GenerateTokens(specText, specFile)
    71  	spec, res := parser.CreateSpecification(tokens, conceptDictionary, specFile)
    72  	res.FileName = specFile
    73  	if len(errs) > 0 {
    74  		res.Ok = false
    75  	}
    76  	res.ParseErrors = append(errs, res.ParseErrors...)
    77  	return spec, res
    78  }
    79  
    80  // ParseSpecText without validating and replacing concepts.
    81  func (parser *SpecParser) ParseSpecText(specText string, specFile string) (*gauge.Specification, *ParseResult) {
    82  	tokens, errs := parser.GenerateTokens(specText, specFile)
    83  	spec, res := parser.createSpecification(tokens, specFile)
    84  	res.FileName = specFile
    85  	if len(errs) > 0 {
    86  		res.Ok = false
    87  	}
    88  	res.ParseErrors = append(errs, res.ParseErrors...)
    89  	return spec, res
    90  }
    91  
    92  func (parser *SpecParser) GenerateTokens(specText, fileName string) ([]*Token, []ParseError) {
    93  	parser.initialize()
    94  	parser.scanner = bufio.NewScanner(strings.NewReader(specText))
    95  	parser.currentState = initial
    96  	var errors []ParseError
    97  	var newToken *Token
    98  	for line, hasLine := parser.nextLine(); hasLine; line, hasLine = parser.nextLine() {
    99  		trimmedLine := strings.TrimSpace(line)
   100  		if len(trimmedLine) == 0 {
   101  			if newToken != nil && newToken.Kind == gauge.StepKind {
   102  				newToken.Suffix = "\n"
   103  				continue
   104  			}
   105  			newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, LineText: line, Value: "\n"}
   106  		} else if parser.isScenarioHeading(trimmedLine) {
   107  			newToken = &Token{Kind: gauge.ScenarioKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[2:])}
   108  		} else if parser.isSpecHeading(trimmedLine) {
   109  			newToken = &Token{Kind: gauge.SpecKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[1:])}
   110  		} else if parser.isSpecUnderline(trimmedLine) && (isInState(parser.currentState, commentScope)) {
   111  			newToken = parser.tokens[len(parser.tokens)-1]
   112  			newToken.Kind = gauge.SpecKind
   113  			parser.tokens = append(parser.tokens[:len(parser.tokens)-1])
   114  		} else if parser.isScenarioUnderline(trimmedLine) && (isInState(parser.currentState, commentScope)) {
   115  			newToken = parser.tokens[len(parser.tokens)-1]
   116  			newToken.Kind = gauge.ScenarioKind
   117  			parser.tokens = append(parser.tokens[:len(parser.tokens)-1])
   118  		} else if parser.isStep(trimmedLine) {
   119  			newToken = &Token{Kind: gauge.StepKind, LineNo: parser.lineNo, LineText: strings.TrimSpace(trimmedLine[1:]), Value: strings.TrimSpace(trimmedLine[1:])}
   120  		} else if found, startIndex := parser.checkTag(trimmedLine); found || isInState(parser.currentState, tagsScope) {
   121  			if isInState(parser.currentState, tagsScope) {
   122  				startIndex = 0
   123  			}
   124  			if parser.isTagEndingWithComma(trimmedLine) {
   125  				addStates(&parser.currentState, tagsScope)
   126  			} else {
   127  				parser.clearState()
   128  			}
   129  			newToken = &Token{Kind: gauge.TagKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[startIndex:])}
   130  		} else if parser.isTableRow(trimmedLine) {
   131  			kind := parser.tokenKindBasedOnCurrentState(tableScope, gauge.TableRow, gauge.TableHeader)
   132  			newToken = &Token{Kind: kind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine)}
   133  		} else if value, found := parser.isDataTable(trimmedLine); found {
   134  			newToken = &Token{Kind: gauge.DataTableKind, LineNo: parser.lineNo, LineText: line, Value: value}
   135  		} else if parser.isTearDown(trimmedLine) {
   136  			newToken = &Token{Kind: gauge.TearDownKind, LineNo: parser.lineNo, LineText: line, Value: trimmedLine}
   137  		} else {
   138  			newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, LineText: line, Value: common.TrimTrailingSpace(line)}
   139  		}
   140  		errors = append(errors, parser.accept(newToken, fileName)...)
   141  	}
   142  	return parser.tokens, errors
   143  }
   144  
   145  func (parser *SpecParser) tokenKindBasedOnCurrentState(state int, matchingToken gauge.TokenKind, alternateToken gauge.TokenKind) gauge.TokenKind {
   146  	if isInState(parser.currentState, state) {
   147  		return matchingToken
   148  	} else {
   149  		return alternateToken
   150  	}
   151  }
   152  
   153  func (parser *SpecParser) checkTag(text string) (bool, int) {
   154  	lowerCased := strings.ToLower
   155  	tagColon := "tags:"
   156  	tagSpaceColon := "tags :"
   157  	if tagStartIndex := strings.Index(lowerCased(text), tagColon); tagStartIndex == 0 {
   158  		return true, len(tagColon)
   159  	} else if tagStartIndex := strings.Index(lowerCased(text), tagSpaceColon); tagStartIndex == 0 {
   160  		return true, len(tagSpaceColon)
   161  	}
   162  	return false, -1
   163  }
   164  func (parser *SpecParser) isTagEndingWithComma(text string) bool {
   165  	return strings.HasSuffix(strings.ToLower(text), ",")
   166  }
   167  
   168  func (parser *SpecParser) isSpecHeading(text string) bool {
   169  	if len(text) > 1 {
   170  		return text[0] == '#' && text[1] != '#'
   171  	} else {
   172  		return text[0] == '#'
   173  	}
   174  }
   175  
   176  func (parser *SpecParser) isScenarioHeading(text string) bool {
   177  	if len(text) > 2 {
   178  		return text[0] == '#' && text[1] == '#' && text[2] != '#'
   179  	} else if len(text) == 2 {
   180  		return text[0] == '#' && text[1] == '#'
   181  	}
   182  	return false
   183  }
   184  
   185  func (parser *SpecParser) isStep(text string) bool {
   186  	if len(text) > 1 {
   187  		return text[0] == '*' && text[1] != '*'
   188  	} else {
   189  		return text[0] == '*'
   190  	}
   191  }
   192  
   193  func (parser *SpecParser) isScenarioUnderline(text string) bool {
   194  	return isUnderline(text, rune('-'))
   195  }
   196  
   197  func (parser *SpecParser) isTableRow(text string) bool {
   198  	return text[0] == '|' && text[len(text)-1] == '|'
   199  }
   200  
   201  func (parser *SpecParser) isTearDown(text string) bool {
   202  	return isUnderline(text, rune('_'))
   203  }
   204  
   205  func (parser *SpecParser) isSpecUnderline(text string) bool {
   206  	return isUnderline(text, rune('='))
   207  }
   208  
   209  func (parser *SpecParser) isDataTable(text string) (string, bool) {
   210  	lowerCased := strings.ToLower
   211  	tableColon := "table:"
   212  	tableSpaceColon := "table :"
   213  	if strings.HasPrefix(lowerCased(text), tableColon) {
   214  		return tableColon + " " + strings.TrimSpace(strings.Replace(lowerCased(text), tableColon, "", 1)), true
   215  	} else if strings.HasPrefix(lowerCased(text), tableSpaceColon) {
   216  		return tableColon + " " + strings.TrimSpace(strings.Replace(lowerCased(text), tableSpaceColon, "", 1)), true
   217  	}
   218  	return "", false
   219  }
   220  
   221  //concept header will have dynamic param and should not be resolved through lookup, so passing nil lookup
   222  func isConceptHeader(lookup *gauge.ArgLookup) bool {
   223  	return lookup == nil
   224  }
   225  
   226  func (parser *SpecParser) accept(token *Token, fileName string) []ParseError {
   227  	errs, _ := parser.processors[token.Kind](parser, token)
   228  	parser.tokens = append(parser.tokens, token)
   229  	var parseErrs []ParseError
   230  	for _, err := range errs {
   231  		parseErrs = append(parseErrs, ParseError{FileName: fileName, LineNo: token.LineNo, Message: err.Error(), LineText: token.Value})
   232  	}
   233  	return parseErrs
   234  }
   235  
   236  func (parser *SpecParser) nextLine() (string, bool) {
   237  	scanned := parser.scanner.Scan()
   238  	if scanned {
   239  		parser.lineNo++
   240  		return parser.scanner.Text(), true
   241  	}
   242  	if err := parser.scanner.Err(); err != nil {
   243  		panic(err)
   244  	}
   245  
   246  	return "", false
   247  }
   248  
   249  func (parser *SpecParser) clearState() {
   250  	parser.currentState = 0
   251  }
   252  
   253  func (parser *SpecParser) CreateSpecification(tokens []*Token, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult) {
   254  	parser.conceptDictionary = conceptDictionary
   255  	specification, finalResult := parser.createSpecification(tokens, specFile)
   256  	specification.ProcessConceptStepsFrom(conceptDictionary)
   257  	err := parser.validateSpec(specification)
   258  	if err != nil {
   259  		finalResult.Ok = false
   260  		finalResult.ParseErrors = append([]ParseError{err.(ParseError)}, finalResult.ParseErrors...)
   261  	}
   262  	return specification, finalResult
   263  }
   264  
   265  func (parser *SpecParser) createSpecification(tokens []*Token, specFile string) (*gauge.Specification, *ParseResult) {
   266  	finalResult := &ParseResult{ParseErrors: make([]ParseError, 0), Ok: true}
   267  	converters := parser.initializeConverters()
   268  	specification := &gauge.Specification{FileName: specFile}
   269  	state := initial
   270  	for _, token := range tokens {
   271  		for _, converter := range converters {
   272  			result := converter(token, &state, specification)
   273  			if !result.Ok {
   274  				if result.ParseErrors != nil {
   275  					finalResult.Ok = false
   276  					finalResult.ParseErrors = append(finalResult.ParseErrors, result.ParseErrors...)
   277  				}
   278  			}
   279  			if result.Warnings != nil {
   280  				if finalResult.Warnings == nil {
   281  					finalResult.Warnings = make([]*Warning, 0)
   282  				}
   283  				finalResult.Warnings = append(finalResult.Warnings, result.Warnings...)
   284  			}
   285  		}
   286  	}
   287  	if len(specification.Scenarios) > 0 {
   288  		specification.LatestScenario().Span.End = tokens[len(tokens)-1].LineNo
   289  	}
   290  	return specification, finalResult
   291  }
   292  
   293  func (parser *SpecParser) initializeConverters() []func(*Token, *int, *gauge.Specification) ParseResult {
   294  	specConverter := converterFn(func(token *Token, state *int) bool {
   295  		return token.Kind == gauge.SpecKind
   296  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   297  		if spec.Heading != nil {
   298  			return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Multiple spec headings found in same file", token.LineText}}}
   299  		}
   300  
   301  		spec.AddHeading(&gauge.Heading{LineNo: token.LineNo, Value: token.Value})
   302  		addStates(state, specScope)
   303  		return ParseResult{Ok: true}
   304  	})
   305  
   306  	scenarioConverter := converterFn(func(token *Token, state *int) bool {
   307  		return token.Kind == gauge.ScenarioKind
   308  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   309  		if spec.Heading == nil {
   310  			return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Scenario should be defined after the spec heading", token.LineText}}}
   311  		}
   312  		for _, scenario := range spec.Scenarios {
   313  			if strings.ToLower(scenario.Heading.Value) == strings.ToLower(token.Value) {
   314  				return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Duplicate scenario definition '" + scenario.Heading.Value + "' found in the same specification", token.LineText}}}
   315  			}
   316  		}
   317  		scenario := &gauge.Scenario{Span: &gauge.Span{Start: token.LineNo, End: token.LineNo}}
   318  		if len(spec.Scenarios) > 0 {
   319  			spec.LatestScenario().Span.End = token.LineNo - 1
   320  		}
   321  		scenario.AddHeading(&gauge.Heading{Value: token.Value, LineNo: token.LineNo})
   322  		spec.AddScenario(scenario)
   323  
   324  		retainStates(state, specScope)
   325  		addStates(state, scenarioScope)
   326  		return ParseResult{Ok: true}
   327  	})
   328  
   329  	stepConverter := converterFn(func(token *Token, state *int) bool {
   330  		return token.Kind == gauge.StepKind && isInState(*state, scenarioScope)
   331  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   332  		latestScenario := spec.LatestScenario()
   333  		stepToAdd, parseDetails := createStep(spec, token)
   334  		if stepToAdd == nil {
   335  			return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings}
   336  		}
   337  		latestScenario.AddStep(stepToAdd)
   338  		retainStates(state, specScope, scenarioScope)
   339  		addStates(state, stepScope)
   340  		if parseDetails != nil && len(parseDetails.ParseErrors) > 0 {
   341  			return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings}
   342  		}
   343  		if parseDetails.Warnings != nil {
   344  			return ParseResult{Ok: false, Warnings: parseDetails.Warnings}
   345  		}
   346  		return ParseResult{Ok: true, Warnings: parseDetails.Warnings}
   347  	})
   348  
   349  	contextConverter := converterFn(func(token *Token, state *int) bool {
   350  		return token.Kind == gauge.StepKind && !isInState(*state, scenarioScope) && isInState(*state, specScope) && !isInState(*state, tearDownScope)
   351  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   352  		stepToAdd, parseDetails := createStep(spec, token)
   353  		if stepToAdd == nil {
   354  			return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings}
   355  		}
   356  		spec.AddContext(stepToAdd)
   357  		retainStates(state, specScope)
   358  		addStates(state, contextScope)
   359  		if parseDetails != nil && len(parseDetails.ParseErrors) > 0 {
   360  			parseDetails.Ok = false
   361  			return *parseDetails
   362  		}
   363  		if parseDetails.Warnings != nil {
   364  			return ParseResult{Ok: false, Warnings: parseDetails.Warnings}
   365  		}
   366  		return ParseResult{Ok: true, Warnings: parseDetails.Warnings}
   367  	})
   368  
   369  	tearDownConverter := converterFn(func(token *Token, state *int) bool {
   370  		return token.Kind == gauge.TearDownKind
   371  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   372  		retainStates(state, specScope)
   373  		addStates(state, tearDownScope)
   374  		spec.AddItem(&gauge.TearDown{LineNo: token.LineNo, Value: token.Value})
   375  		return ParseResult{Ok: true}
   376  	})
   377  
   378  	tearDownStepConverter := converterFn(func(token *Token, state *int) bool {
   379  		return token.Kind == gauge.StepKind && isInState(*state, tearDownScope)
   380  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   381  		stepToAdd, parseDetails := createStep(spec, token)
   382  		if stepToAdd == nil {
   383  			return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings}
   384  		}
   385  		spec.TearDownSteps = append(spec.TearDownSteps, stepToAdd)
   386  		spec.AddItem(stepToAdd)
   387  		retainStates(state, specScope, tearDownScope)
   388  		if parseDetails != nil && len(parseDetails.ParseErrors) > 0 {
   389  			parseDetails.Ok = false
   390  			return *parseDetails
   391  		}
   392  		if parseDetails.Warnings != nil {
   393  			return ParseResult{Ok: false, Warnings: parseDetails.Warnings}
   394  		}
   395  		return ParseResult{Ok: true, Warnings: parseDetails.Warnings}
   396  	})
   397  
   398  	commentConverter := converterFn(func(token *Token, state *int) bool {
   399  		return token.Kind == gauge.CommentKind
   400  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   401  		comment := &gauge.Comment{token.Value, token.LineNo}
   402  		if isInState(*state, scenarioScope) {
   403  			spec.LatestScenario().AddComment(comment)
   404  		} else {
   405  			spec.AddComment(comment)
   406  		}
   407  		retainStates(state, specScope, scenarioScope, tearDownScope)
   408  		addStates(state, commentScope)
   409  		return ParseResult{Ok: true}
   410  	})
   411  
   412  	keywordConverter := converterFn(func(token *Token, state *int) bool {
   413  		return token.Kind == gauge.DataTableKind
   414  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   415  		resolvedArg, err := newSpecialTypeResolver().resolve(token.Value)
   416  		if resolvedArg == nil || err != nil {
   417  			e := ParseError{FileName: spec.FileName, LineNo: token.LineNo, LineText: token.LineText, Message: fmt.Sprintf("Could not resolve table from %s", token.LineText)}
   418  			return ParseResult{ParseErrors: []ParseError{e}, Ok: false}
   419  		}
   420  		if isInState(*state, specScope) && !spec.DataTable.IsInitialized() {
   421  			externalTable := &gauge.DataTable{}
   422  			externalTable.Table = resolvedArg.Table
   423  			externalTable.LineNo = token.LineNo
   424  			externalTable.Value = token.Value
   425  			externalTable.IsExternal = true
   426  			spec.AddExternalDataTable(externalTable)
   427  		} else if isInState(*state, specScope) && spec.DataTable.IsInitialized() {
   428  			value := "Multiple data table present, ignoring table"
   429  			spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   430  			return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}}
   431  		} else {
   432  			value := "Data table not associated with spec"
   433  			spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   434  			return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}}
   435  		}
   436  		retainStates(state, specScope)
   437  		addStates(state, keywordScope)
   438  		return ParseResult{Ok: true}
   439  	})
   440  
   441  	tableHeaderConverter := converterFn(func(token *Token, state *int) bool {
   442  		return token.Kind == gauge.TableHeader && isInState(*state, specScope)
   443  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   444  		if isInState(*state, stepScope) {
   445  			latestScenario := spec.LatestScenario()
   446  			latestStep := latestScenario.LatestStep()
   447  			addInlineTableHeader(latestStep, token)
   448  		} else if isInState(*state, contextScope) {
   449  			latestContext := spec.LatestContext()
   450  			addInlineTableHeader(latestContext, token)
   451  		} else if isInState(*state, tearDownScope) {
   452  			if len(spec.TearDownSteps) > 0 {
   453  				latestTeardown := spec.LatestTeardown()
   454  				addInlineTableHeader(latestTeardown, token)
   455  			} else {
   456  				spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   457  			}
   458  		} else if !isInState(*state, scenarioScope) {
   459  			if !spec.DataTable.Table.IsInitialized() {
   460  				dataTable := &gauge.Table{}
   461  				dataTable.LineNo = token.LineNo
   462  				dataTable.AddHeaders(token.Args)
   463  				spec.AddDataTable(dataTable)
   464  			} else {
   465  				value := "Multiple data table present, ignoring table"
   466  				spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   467  				return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}}
   468  			}
   469  		} else {
   470  			value := "Table not associated with a step, ignoring table"
   471  			spec.LatestScenario().AddComment(&gauge.Comment{token.LineText, token.LineNo})
   472  			return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}}
   473  		}
   474  		retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope)
   475  		addStates(state, tableScope)
   476  		return ParseResult{Ok: true}
   477  	})
   478  
   479  	tableRowConverter := converterFn(func(token *Token, state *int) bool {
   480  		return token.Kind == gauge.TableRow
   481  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   482  		var result ParseResult
   483  		//When table is to be treated as a comment
   484  		if !isInState(*state, tableScope) {
   485  			if isInState(*state, scenarioScope) {
   486  				spec.LatestScenario().AddComment(&gauge.Comment{token.LineText, token.LineNo})
   487  			} else {
   488  				spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   489  			}
   490  		} else if areUnderlined(token.Args) && !isInState(*state, tableSeparatorScope) {
   491  			retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope)
   492  			addStates(state, tableSeparatorScope)
   493  			// skip table separator
   494  			result = ParseResult{Ok: true}
   495  		} else if isInState(*state, stepScope) {
   496  			latestScenario := spec.LatestScenario()
   497  			latestStep := latestScenario.LatestStep()
   498  			result = addInlineTableRow(latestStep, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName)
   499  		} else if isInState(*state, contextScope) {
   500  			latestContext := spec.LatestContext()
   501  			result = addInlineTableRow(latestContext, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName)
   502  		} else if isInState(*state, tearDownScope) {
   503  			if len(spec.TearDownSteps) > 0 {
   504  				latestTeardown := spec.LatestTeardown()
   505  				result = addInlineTableRow(latestTeardown, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName)
   506  			} else {
   507  				spec.AddComment(&gauge.Comment{token.LineText, token.LineNo})
   508  			}
   509  		} else {
   510  			//todo validate datatable rows also
   511  			spec.DataTable.Table.AddRowValues(token.Args)
   512  			result = ParseResult{Ok: true}
   513  		}
   514  		retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope, tableSeparatorScope)
   515  		return result
   516  	})
   517  
   518  	tagConverter := converterFn(func(token *Token, state *int) bool {
   519  		return (token.Kind == gauge.TagKind)
   520  	}, func(token *Token, spec *gauge.Specification, state *int) ParseResult {
   521  		tags := &gauge.Tags{RawValues: [][]string{token.Args}}
   522  		if isInState(*state, scenarioScope) {
   523  			if isInState(*state, tagsScope) {
   524  				spec.LatestScenario().Tags.Add(tags.RawValues[0])
   525  			} else {
   526  				if spec.LatestScenario().NTags() != 0 {
   527  					return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{FileName: spec.FileName, LineNo: token.LineNo, Message: "Tags can be defined only once per scenario", LineText: token.LineText}}}
   528  				}
   529  				spec.LatestScenario().AddTags(tags)
   530  			}
   531  		} else {
   532  			if isInState(*state, tagsScope) {
   533  				spec.Tags.Add(tags.RawValues[0])
   534  			} else {
   535  				if spec.NTags() != 0 {
   536  					return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{FileName: spec.FileName, LineNo: token.LineNo, Message: "Tags can be defined only once per specification", LineText: token.LineText}}}
   537  				}
   538  				spec.AddTags(tags)
   539  			}
   540  		}
   541  		addStates(state, tagsScope)
   542  		return ParseResult{Ok: true}
   543  	})
   544  
   545  	converter := []func(*Token, *int, *gauge.Specification) ParseResult{
   546  		specConverter, scenarioConverter, stepConverter, contextConverter, commentConverter, tableHeaderConverter, tableRowConverter, tagConverter, keywordConverter, tearDownConverter, tearDownStepConverter,
   547  	}
   548  
   549  	return converter
   550  }
   551  
   552  func (parser *SpecParser) validateSpec(specification *gauge.Specification) error {
   553  	if len(specification.Items) == 0 {
   554  		specification.AddHeading(&gauge.Heading{})
   555  		return ParseError{FileName: specification.FileName, LineNo: 1, Message: "Spec does not have any elements"}
   556  	}
   557  	if specification.Heading == nil {
   558  		specification.AddHeading(&gauge.Heading{})
   559  		return ParseError{FileName: specification.FileName, LineNo: 1, Message: "Spec heading not found"}
   560  	}
   561  	if len(strings.TrimSpace(specification.Heading.Value)) < 1 {
   562  		return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, Message: "Spec heading should have at least one character"}
   563  	}
   564  
   565  	dataTable := specification.DataTable.Table
   566  	if dataTable.IsInitialized() && dataTable.GetRowCount() == 0 {
   567  		return ParseError{FileName: specification.FileName, LineNo: dataTable.LineNo, Message: "Data table should have at least 1 data row"}
   568  	}
   569  	if len(specification.Scenarios) == 0 {
   570  		return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, Message: "Spec should have atleast one scenario"}
   571  	}
   572  	for _, sce := range specification.Scenarios {
   573  		if len(sce.Steps) == 0 {
   574  			return ParseError{FileName: specification.FileName, LineNo: sce.Heading.LineNo, Message: "Scenario should have atleast one step"}
   575  		}
   576  	}
   577  	return nil
   578  }
   579  
   580  func converterFn(predicate func(token *Token, state *int) bool, apply func(token *Token, spec *gauge.Specification, state *int) ParseResult) func(*Token, *int, *gauge.Specification) ParseResult {
   581  	return func(token *Token, state *int, spec *gauge.Specification) ParseResult {
   582  		if !predicate(token, state) {
   583  			return ParseResult{Ok: true}
   584  		}
   585  		return apply(token, spec, state)
   586  	}
   587  }
   588  
   589  func createStep(spec *gauge.Specification, stepToken *Token) (*gauge.Step, *ParseResult) {
   590  	dataTableLookup := new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table)
   591  	stepToAdd, parseDetails := CreateStepUsingLookup(stepToken, dataTableLookup, spec.FileName)
   592  	if stepToAdd != nil {
   593  		stepToAdd.Suffix = stepToken.Suffix
   594  	}
   595  	return stepToAdd, parseDetails
   596  }
   597  
   598  func CreateStepUsingLookup(stepToken *Token, lookup *gauge.ArgLookup, specFileName string) (*gauge.Step, *ParseResult) {
   599  	stepValue, argsType := extractStepValueAndParameterTypes(stepToken.Value)
   600  	if argsType != nil && len(argsType) != len(stepToken.Args) {
   601  		return nil, &ParseResult{ParseErrors: []ParseError{ParseError{specFileName, stepToken.LineNo, "Step text should not have '{static}' or '{dynamic}' or '{special}'", stepToken.LineText}}, Warnings: nil}
   602  	}
   603  	step := &gauge.Step{LineNo: stepToken.LineNo, Value: stepValue, LineText: strings.TrimSpace(stepToken.LineText)}
   604  	arguments := make([]*gauge.StepArg, 0)
   605  	var errors []ParseError
   606  	var warnings []*Warning
   607  	for i, argType := range argsType {
   608  		argument, parseDetails := createStepArg(stepToken.Args[i], argType, stepToken, lookup, specFileName)
   609  		if parseDetails != nil && len(parseDetails.ParseErrors) > 0 {
   610  			errors = append(errors, parseDetails.ParseErrors...)
   611  		}
   612  		arguments = append(arguments, argument)
   613  		if parseDetails != nil && parseDetails.Warnings != nil {
   614  			for _, warn := range parseDetails.Warnings {
   615  				warnings = append(warnings, warn)
   616  			}
   617  		}
   618  	}
   619  	step.AddArgs(arguments...)
   620  	return step, &ParseResult{ParseErrors: errors, Warnings: warnings}
   621  }
   622  
   623  func ExtractStepArgsFromToken(stepToken *Token) ([]gauge.StepArg, error) {
   624  	_, argsType := extractStepValueAndParameterTypes(stepToken.Value)
   625  	if argsType != nil && len(argsType) != len(stepToken.Args) {
   626  		return nil, fmt.Errorf("Step text should not have '{static}' or '{dynamic}' or '{special}'")
   627  	}
   628  	var args []gauge.StepArg
   629  	for i, argType := range argsType {
   630  		if gauge.ArgType(argType) == gauge.Static {
   631  			args = append(args, gauge.StepArg{ArgType: gauge.Static, Value: stepToken.Args[i]})
   632  		} else {
   633  			args = append(args, gauge.StepArg{ArgType: gauge.Dynamic, Value: stepToken.Args[i]})
   634  		}
   635  	}
   636  	return args, nil
   637  }
   638  
   639  func extractStepValueAndParameterTypes(stepTokenValue string) (string, []string) {
   640  	argsType := make([]string, 0)
   641  	r := regexp.MustCompile("{(dynamic|static|special)}")
   642  	/*
   643  		enter {dynamic} and {static}
   644  		returns
   645  		[
   646  		["{dynamic}","dynamic"]
   647  		["{static}","static"]
   648  		]
   649  	*/
   650  	args := r.FindAllStringSubmatch(stepTokenValue, -1)
   651  
   652  	if args == nil {
   653  		return stepTokenValue, nil
   654  	}
   655  	for _, arg := range args {
   656  		//arg[1] extracts the first group
   657  		argsType = append(argsType, arg[1])
   658  	}
   659  	return r.ReplaceAllString(stepTokenValue, gauge.ParameterPlaceholder), argsType
   660  }
   661  
   662  func createStepArg(argValue string, typeOfArg string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   663  	if typeOfArg == "special" {
   664  		resolvedArgValue, err := newSpecialTypeResolver().resolve(argValue)
   665  		if err != nil {
   666  			switch err.(type) {
   667  			case invalidSpecialParamError:
   668  				return treatArgAsDynamic(argValue, token, lookup, fileName)
   669  			default:
   670  				return &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue}, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText}}}
   671  			}
   672  		}
   673  		return resolvedArgValue, nil
   674  	} else if typeOfArg == "static" {
   675  		return &gauge.StepArg{ArgType: gauge.Static, Value: argValue}, nil
   676  	} else {
   677  		return validateDynamicArg(argValue, token, lookup, fileName)
   678  	}
   679  }
   680  
   681  func treatArgAsDynamic(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   682  	parseRes := &ParseResult{Warnings: []*Warning{&Warning{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Could not resolve special param type <%s>. Treating it as dynamic param.", argValue)}}}
   683  	stepArg, result := validateDynamicArg(argValue, token, lookup, fileName)
   684  	if result != nil {
   685  		if len(result.ParseErrors) > 0 {
   686  			parseRes.ParseErrors = result.ParseErrors
   687  		}
   688  		if result.Warnings != nil {
   689  			for _, warn := range result.Warnings {
   690  				parseRes.Warnings = append(parseRes.Warnings, warn)
   691  			}
   692  		}
   693  	}
   694  	return stepArg, parseRes
   695  }
   696  
   697  func validateDynamicArg(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   698  	stepArgument := &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue}
   699  	if !isConceptHeader(lookup) && !lookup.ContainsArg(argValue) {
   700  		return stepArgument, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText}}}
   701  	}
   702  
   703  	return stepArgument, nil
   704  }
   705  
   706  //Step value is modified when inline table is found to account for the new parameter by appending {}
   707  //todo validate headers for dynamic
   708  func addInlineTableHeader(step *gauge.Step, token *Token) {
   709  	step.Value = fmt.Sprintf("%s %s", step.Value, gauge.ParameterPlaceholder)
   710  	step.HasInlineTable = true
   711  	step.AddInlineTableHeaders(token.Args)
   712  }
   713  
   714  func addInlineTableRow(step *gauge.Step, token *Token, argLookup *gauge.ArgLookup, fileName string) ParseResult {
   715  	dynamicArgMatcher := regexp.MustCompile("^<(.*)>$")
   716  	tableValues := make([]gauge.TableCell, 0)
   717  	warnings := make([]*Warning, 0)
   718  	for _, tableValue := range token.Args {
   719  		if dynamicArgMatcher.MatchString(tableValue) {
   720  			match := dynamicArgMatcher.FindAllStringSubmatch(tableValue, -1)
   721  			param := match[0][1]
   722  			if !argLookup.ContainsArg(param) {
   723  				tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static})
   724  				warnings = append(warnings, &Warning{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic param <%s> could not be resolved, Treating it as static param", param)})
   725  			} else {
   726  				tableValues = append(tableValues, gauge.TableCell{Value: param, CellType: gauge.Dynamic})
   727  			}
   728  		} else {
   729  			tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static})
   730  		}
   731  	}
   732  	step.AddInlineTableRow(tableValues)
   733  	return ParseResult{Ok: true, Warnings: warnings}
   734  }
   735  
   736  func ConvertToStepText(fragments []*gauge_messages.Fragment) string {
   737  	stepText := ""
   738  	for _, fragment := range fragments {
   739  		value := ""
   740  		if fragment.GetFragmentType() == gauge_messages.Fragment_Text {
   741  			value = fragment.GetText()
   742  		} else {
   743  			switch fragment.GetParameter().GetParameterType() {
   744  			case gauge_messages.Parameter_Static:
   745  				value = fmt.Sprintf("\"%s\"", fragment.GetParameter().GetValue())
   746  				break
   747  			case gauge_messages.Parameter_Dynamic:
   748  				value = fmt.Sprintf("<%s>", fragment.GetParameter().GetValue())
   749  				break
   750  			}
   751  		}
   752  		stepText += value
   753  	}
   754  	return stepText
   755  }
   756  
   757  type Token struct {
   758  	Kind     gauge.TokenKind
   759  	LineNo   int
   760  	LineText string
   761  	Suffix   string
   762  	Args     []string
   763  	Value    string
   764  }
   765  
   766  type ParseError struct {
   767  	FileName string
   768  	LineNo   int
   769  	Message  string
   770  	LineText string
   771  }
   772  
   773  func (se ParseError) Error() string {
   774  	if se.LineNo == 0 && se.FileName == "" {
   775  		return fmt.Sprintf("%s", se.Message)
   776  	}
   777  	return fmt.Sprintf("%s:%d %s => '%s'", se.FileName, se.LineNo, se.Message, se.LineText)
   778  }
   779  
   780  func (token *Token) String() string {
   781  	return fmt.Sprintf("kind:%d, lineNo:%d, value:%s, line:%s, args:%s", token.Kind, token.LineNo, token.Value, token.LineText, token.Args)
   782  }
   783  
   784  type ParseResult struct {
   785  	ParseErrors []ParseError
   786  	Warnings    []*Warning
   787  	Ok          bool
   788  	FileName    string
   789  }
   790  
   791  func (result *ParseResult) Errors() (errors []string) {
   792  	for _, err := range result.ParseErrors {
   793  		errors = append(errors, fmt.Sprintf("[ParseError] %s", err.Error()))
   794  	}
   795  	return
   796  }
   797  
   798  type Warning struct {
   799  	FileName string
   800  	LineNo   int
   801  	Message  string
   802  }
   803  
   804  func (warning *Warning) String() string {
   805  	return fmt.Sprintf("%s:%d %s", warning.FileName, warning.LineNo, warning.Message)
   806  }