github.com/getgauge/gauge@v1.6.9/parser/specparser.go (about) 1 /*---------------------------------------------------------------- 2 * Copyright (c) ThoughtWorks, Inc. 3 * Licensed under the Apache License, Version 2.0 4 * See LICENSE in the project root for license information. 5 *----------------------------------------------------------------*/ 6 7 package parser 8 9 import ( 10 "bufio" 11 "strings" 12 13 "github.com/getgauge/gauge/gauge" 14 ) 15 16 // SpecParser is responsible for parsing a Specification. It delegates to respective processors composed sub-entities 17 type SpecParser struct { 18 scanner *bufio.Scanner 19 lineNo int 20 tokens []*Token 21 currentState int 22 processors map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool) 23 conceptDictionary *gauge.ConceptDictionary 24 } 25 26 // Parse generates tokens for the given spec text and creates the specification. 27 func (parser *SpecParser) Parse(specText string, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult, error) { 28 tokens, errs := parser.GenerateTokens(specText, specFile) 29 spec, res, err := parser.CreateSpecification(tokens, conceptDictionary, specFile) 30 if err != nil { 31 return nil, nil, err 32 } 33 res.FileName = specFile 34 if len(errs) > 0 { 35 res.Ok = false 36 } 37 res.ParseErrors = append(errs, res.ParseErrors...) 38 return spec, res, nil 39 } 40 41 // ParseSpecText without validating and replacing concepts. 42 func (parser *SpecParser) ParseSpecText(specText string, specFile string) (*gauge.Specification, *ParseResult) { 43 tokens, errs := parser.GenerateTokens(specText, specFile) 44 spec, res := parser.createSpecification(tokens, specFile) 45 res.FileName = specFile 46 if len(errs) > 0 { 47 res.Ok = false 48 } 49 res.ParseErrors = append(errs, res.ParseErrors...) 50 return spec, res 51 } 52 53 // CreateSpecification creates specification from the given set of tokens. 54 func (parser *SpecParser) CreateSpecification(tokens []*Token, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult, error) { 55 parser.conceptDictionary = conceptDictionary 56 specification, finalResult := parser.createSpecification(tokens, specFile) 57 if err := specification.ProcessConceptStepsFrom(conceptDictionary); err != nil { 58 return nil, nil, err 59 } 60 err := parser.validateSpec(specification) 61 if err != nil { 62 finalResult.Ok = false 63 finalResult.ParseErrors = append([]ParseError{err.(ParseError)}, finalResult.ParseErrors...) 64 } 65 return specification, finalResult, nil 66 } 67 68 func (parser *SpecParser) createSpecification(tokens []*Token, specFile string) (*gauge.Specification, *ParseResult) { 69 finalResult := &ParseResult{ParseErrors: make([]ParseError, 0), Ok: true} 70 converters := parser.initializeConverters() 71 specification := &gauge.Specification{FileName: specFile} 72 state := initial 73 for _, token := range tokens { 74 for _, converter := range converters { 75 result := converter(token, &state, specification) 76 if !result.Ok { 77 if result.ParseErrors != nil { 78 finalResult.Ok = false 79 finalResult.ParseErrors = append(finalResult.ParseErrors, result.ParseErrors...) 80 } 81 } 82 if result.Warnings != nil { 83 if finalResult.Warnings == nil { 84 finalResult.Warnings = make([]*Warning, 0) 85 } 86 finalResult.Warnings = append(finalResult.Warnings, result.Warnings...) 87 } 88 } 89 } 90 if len(specification.Scenarios) > 0 { 91 specification.LatestScenario().Span.End = tokens[len(tokens)-1].LineNo 92 } 93 return specification, finalResult 94 } 95 96 func (parser *SpecParser) validateSpec(specification *gauge.Specification) error { 97 if len(specification.Items) == 0 { 98 specification.AddHeading(&gauge.Heading{}) 99 return ParseError{FileName: specification.FileName, LineNo: 1, SpanEnd: 1, Message: "Spec does not have any elements"} 100 } 101 if specification.Heading == nil { 102 specification.AddHeading(&gauge.Heading{}) 103 return ParseError{FileName: specification.FileName, LineNo: 1, SpanEnd: 1, Message: "Spec heading not found"} 104 } 105 if len(strings.TrimSpace(specification.Heading.Value)) < 1 { 106 return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, SpanEnd: specification.Heading.LineNo, Message: "Spec heading should have at least one character"} 107 } 108 109 dataTable := specification.DataTable.Table 110 if dataTable.IsInitialized() && dataTable.GetRowCount() == 0 { 111 return ParseError{FileName: specification.FileName, LineNo: dataTable.LineNo, SpanEnd: dataTable.LineNo, Message: "Data table should have at least 1 data row"} 112 } 113 if len(specification.Scenarios) == 0 { 114 return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, SpanEnd: specification.Heading.SpanEnd, Message: "Spec should have atleast one scenario"} 115 } 116 for _, sce := range specification.Scenarios { 117 if len(sce.Steps) == 0 { 118 return ParseError{FileName: specification.FileName, LineNo: sce.Heading.LineNo, SpanEnd: sce.Heading.SpanEnd, Message: "Scenario should have atleast one step"} 119 } 120 } 121 return nil 122 } 123 124 func createStep(spec *gauge.Specification, scn *gauge.Scenario, stepToken *Token) (*gauge.Step, *ParseResult) { 125 tables := []*gauge.Table{spec.DataTable.Table} 126 if scn != nil { 127 tables = append(tables, scn.DataTable.Table) 128 } 129 dataTableLookup := new(gauge.ArgLookup).FromDataTables(tables...) 130 stepToAdd, parseDetails := CreateStepUsingLookup(stepToken, dataTableLookup, spec.FileName) 131 if stepToAdd != nil { 132 stepToAdd.Suffix = stepToken.Suffix 133 } 134 return stepToAdd, parseDetails 135 } 136 137 // CreateStepUsingLookup generates gauge steps from step token and args lookup. 138 func CreateStepUsingLookup(stepToken *Token, lookup *gauge.ArgLookup, specFileName string) (*gauge.Step, *ParseResult) { 139 stepValue, argsType := extractStepValueAndParameterTypes(stepToken.Value) 140 if argsType != nil && len(argsType) != len(stepToken.Args) { 141 return nil, &ParseResult{ParseErrors: []ParseError{ParseError{specFileName, stepToken.LineNo, stepToken.SpanEnd, "Step text should not have '{static}' or '{dynamic}' or '{special}'", stepToken.LineText()}}, Warnings: nil} 142 } 143 lineText := strings.Join(stepToken.Lines, " ") 144 step := &gauge.Step{FileName: specFileName, LineNo: stepToken.LineNo, Value: stepValue, LineText: strings.TrimSpace(lineText), LineSpanEnd: stepToken.SpanEnd} 145 arguments := make([]*gauge.StepArg, 0) 146 var errors []ParseError 147 var warnings []*Warning 148 for i, argType := range argsType { 149 argument, parseDetails := createStepArg(stepToken.Args[i], argType, stepToken, lookup, specFileName) 150 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 151 errors = append(errors, parseDetails.ParseErrors...) 152 } 153 arguments = append(arguments, argument) 154 if parseDetails != nil && parseDetails.Warnings != nil { 155 warnings = append(warnings, parseDetails.Warnings...) 156 } 157 } 158 step.AddArgs(arguments...) 159 return step, &ParseResult{ParseErrors: errors, Warnings: warnings} 160 }