github.com/getgauge/gauge@v1.6.9/parser/conceptParser.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 "fmt" 11 "strings" 12 13 "github.com/getgauge/common" 14 "github.com/getgauge/gauge/gauge" 15 "github.com/getgauge/gauge/logger" 16 "github.com/getgauge/gauge/util" 17 ) 18 19 // ConceptParser is used for parsing concepts. Similar, but not the same as a SpecParser 20 type ConceptParser struct { 21 currentState int 22 currentConcept *gauge.Step 23 } 24 25 // Parse Generates token for the given concept file and cretes concepts(array of steps) and parse results. 26 // concept file can have multiple concept headings. 27 func (parser *ConceptParser) Parse(text, fileName string) ([]*gauge.Step, *ParseResult) { 28 defer parser.resetState() 29 30 specParser := new(SpecParser) 31 tokens, errs := specParser.GenerateTokens(text, fileName) 32 concepts, res := parser.createConcepts(tokens, fileName) 33 return concepts, &ParseResult{ParseErrors: append(errs, res.ParseErrors...), Warnings: res.Warnings} 34 } 35 36 // ParseFile Reads file contents from a give file and parses the file. 37 func (parser *ConceptParser) ParseFile(file string) ([]*gauge.Step, *ParseResult) { 38 fileText, fileReadErr := common.ReadFileContents(file) 39 if fileReadErr != nil { 40 return nil, &ParseResult{ParseErrors: []ParseError{{Message: fmt.Sprintf("failed to read concept file %s", file)}}} 41 } 42 return parser.Parse(fileText, file) 43 } 44 45 func (parser *ConceptParser) resetState() { 46 parser.currentState = initial 47 parser.currentConcept = nil 48 } 49 50 func (parser *ConceptParser) createConcepts(tokens []*Token, fileName string) ([]*gauge.Step, *ParseResult) { 51 parser.currentState = initial 52 var concepts []*gauge.Step 53 parseRes := &ParseResult{ParseErrors: make([]ParseError, 0)} 54 var preComments []*gauge.Comment 55 addPreComments := false 56 for _, token := range tokens { 57 if parser.isConceptHeading(token) { 58 if isInState(parser.currentState, conceptScope, stepScope) { 59 if len(parser.currentConcept.ConceptSteps) < 1 { 60 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: parser.currentConcept.LineNo, SpanEnd: parser.currentConcept.LineSpanEnd, Message: "Concept should have atleast one step", LineText: parser.currentConcept.LineText}) 61 continue 62 } 63 concepts = append(concepts, parser.currentConcept) 64 } 65 var res *ParseResult 66 parser.currentConcept, res = parser.processConceptHeading(token, fileName) 67 parser.currentState = initial 68 if len(res.ParseErrors) > 0 { 69 parseRes.ParseErrors = append(parseRes.ParseErrors, res.ParseErrors...) 70 continue 71 } 72 if addPreComments { 73 parser.currentConcept.PreComments = preComments 74 addPreComments = false 75 } 76 addStates(&parser.currentState, conceptScope) 77 } else if parser.isStep(token) { 78 if !isInState(parser.currentState, conceptScope) { 79 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: "Step is not defined inside a concept heading", LineText: token.LineText()}) 80 continue 81 } 82 if errs := parser.processConceptStep(token, fileName); len(errs) > 0 { 83 parseRes.ParseErrors = append(parseRes.ParseErrors, errs...) 84 } 85 addStates(&parser.currentState, stepScope) 86 } else if parser.isTableHeader(token) { 87 if !isInState(parser.currentState, stepScope) { 88 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: "Table doesn't belong to any step", LineText: token.LineText()}) 89 continue 90 } 91 parser.processTableHeader(token) 92 addStates(&parser.currentState, tableScope) 93 } else if parser.isScenarioHeading(token) { 94 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: "Scenario Heading is not allowed in concept file", LineText: token.LineText()}) 95 continue 96 } else if parser.isTableDataRow(token) { 97 if areUnderlined(token.Args) && !isInState(parser.currentState, tableSeparatorScope) { 98 addStates(&parser.currentState, tableSeparatorScope) 99 } else if isInState(parser.currentState, stepScope) { 100 parser.processTableDataRow(token, &parser.currentConcept.Lookup, fileName) 101 } 102 } else { 103 retainStates(&parser.currentState, conceptScope) 104 addStates(&parser.currentState, commentScope) 105 comment := &gauge.Comment{Value: token.Value, LineNo: token.LineNo} 106 if parser.currentConcept == nil { 107 preComments = append(preComments, comment) 108 addPreComments = true 109 continue 110 } 111 parser.currentConcept.Items = append(parser.currentConcept.Items, comment) 112 } 113 } 114 if parser.currentConcept != nil && len(parser.currentConcept.ConceptSteps) < 1 { 115 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: parser.currentConcept.LineNo, SpanEnd: parser.currentConcept.LineSpanEnd, Message: "Concept should have atleast one step", LineText: parser.currentConcept.LineText}) 116 return nil, parseRes 117 } 118 119 if parser.currentConcept != nil { 120 concepts = append(concepts, parser.currentConcept) 121 } 122 return concepts, parseRes 123 } 124 125 func (parser *ConceptParser) isConceptHeading(token *Token) bool { 126 return token.Kind == gauge.SpecKind 127 } 128 129 func (parser *ConceptParser) isStep(token *Token) bool { 130 return token.Kind == gauge.StepKind 131 } 132 133 func (parser *ConceptParser) isScenarioHeading(token *Token) bool { 134 return token.Kind == gauge.ScenarioKind 135 } 136 137 func (parser *ConceptParser) isTableHeader(token *Token) bool { 138 return token.Kind == gauge.TableHeader 139 } 140 141 func (parser *ConceptParser) isTableDataRow(token *Token) bool { 142 return token.Kind == gauge.TableRow 143 } 144 145 func (parser *ConceptParser) processConceptHeading(token *Token, fileName string) (*gauge.Step, *ParseResult) { 146 processStep(new(SpecParser), token) 147 token.Lines[0] = strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(token.Lines[0]), "#")) 148 var concept *gauge.Step 149 var parseRes *ParseResult 150 concept, parseRes = CreateStepUsingLookup(token, nil, fileName) 151 if parseRes != nil && len(parseRes.ParseErrors) > 0 { 152 return nil, parseRes 153 } 154 if !parser.hasOnlyDynamicParams(concept) { 155 parseRes.ParseErrors = []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: "Concept heading can have only Dynamic Parameters", LineText: token.LineText()}} 156 return nil, parseRes 157 } 158 159 concept.IsConcept = true 160 parser.createConceptLookup(concept) 161 concept.Items = append(concept.Items, concept) 162 return concept, parseRes 163 } 164 165 func (parser *ConceptParser) processConceptStep(token *Token, fileName string) []ParseError { 166 processStep(new(SpecParser), token) 167 conceptStep, parseRes := CreateStepUsingLookup(token, &parser.currentConcept.Lookup, fileName) 168 if conceptStep != nil { 169 conceptStep.Suffix = token.Suffix 170 parser.currentConcept.ConceptSteps = append(parser.currentConcept.ConceptSteps, conceptStep) 171 parser.currentConcept.Items = append(parser.currentConcept.Items, conceptStep) 172 } 173 return parseRes.ParseErrors 174 } 175 176 func (parser *ConceptParser) processTableHeader(token *Token) { 177 steps := parser.currentConcept.ConceptSteps 178 currentStep := steps[len(steps)-1] 179 addInlineTableHeader(currentStep, token) 180 items := parser.currentConcept.Items 181 items[len(items)-1] = currentStep 182 } 183 184 func (parser *ConceptParser) processTableDataRow(token *Token, argLookup *gauge.ArgLookup, fileName string) { 185 steps := parser.currentConcept.ConceptSteps 186 currentStep := steps[len(steps)-1] 187 addInlineTableRow(currentStep, token, argLookup, fileName) 188 items := parser.currentConcept.Items 189 items[len(items)-1] = currentStep 190 } 191 192 func (parser *ConceptParser) hasOnlyDynamicParams(step *gauge.Step) bool { 193 for _, arg := range step.Args { 194 if arg.ArgType != gauge.Dynamic { 195 return false 196 } 197 } 198 return true 199 } 200 201 func (parser *ConceptParser) createConceptLookup(concept *gauge.Step) { 202 for _, arg := range concept.Args { 203 concept.Lookup.AddArgName(arg.Value) 204 } 205 } 206 207 // CreateConceptsDictionary generates a ConceptDictionary which is map of concept text to concept. ConceptDictionary is used to search for a concept. 208 func CreateConceptsDictionary() (*gauge.ConceptDictionary, *ParseResult, error) { 209 cptFilesMap := make(map[string]bool) 210 for _, cpt := range util.GetConceptFiles() { 211 cptFilesMap[cpt] = true 212 } 213 var conceptFiles []string 214 for cpt := range cptFilesMap { 215 conceptFiles = append(conceptFiles, cpt) 216 } 217 conceptsDictionary := gauge.NewConceptDictionary() 218 res := &ParseResult{Ok: true} 219 if _, errs, e := AddConcepts(conceptFiles, conceptsDictionary); len(errs) > 0 { 220 if e != nil { 221 return nil, nil, e 222 } 223 for _, err := range errs { 224 logger.Errorf(false, "Concept parse failure: %s %s", conceptFiles[0], err) 225 } 226 res.ParseErrors = append(res.ParseErrors, errs...) 227 res.Ok = false 228 } 229 vRes := ValidateConcepts(conceptsDictionary) 230 if len(vRes.ParseErrors) > 0 { 231 res.Ok = false 232 res.ParseErrors = append(res.ParseErrors, vRes.ParseErrors...) 233 } 234 return conceptsDictionary, res, nil 235 } 236 237 // AddConcept adds the concept in the ConceptDictionary. 238 func AddConcept(concepts []*gauge.Step, file string, conceptDictionary *gauge.ConceptDictionary) ([]ParseError, error) { 239 parseErrors := make([]ParseError, 0) 240 for _, conceptStep := range concepts { 241 if dupConcept, exists := conceptDictionary.ConceptsMap[conceptStep.Value]; exists { 242 parseErrors = append(parseErrors, ParseError{ 243 FileName: file, 244 LineNo: conceptStep.LineNo, 245 SpanEnd: conceptStep.LineSpanEnd, 246 Message: "Duplicate concept definition found", 247 LineText: conceptStep.LineText, 248 }, 249 ParseError{ 250 FileName: dupConcept.FileName, 251 LineNo: dupConcept.ConceptStep.LineNo, 252 SpanEnd: conceptStep.LineSpanEnd, 253 Message: "Duplicate concept definition found", 254 LineText: dupConcept.ConceptStep.LineText, 255 }) 256 } 257 conceptDictionary.ConceptsMap[conceptStep.Value] = &gauge.Concept{ConceptStep: conceptStep, FileName: file} 258 if err := conceptDictionary.ReplaceNestedConceptSteps(conceptStep); err != nil { 259 return nil, err 260 } 261 } 262 err := conceptDictionary.UpdateLookupForNestedConcepts() 263 return parseErrors, err 264 } 265 266 // AddConcepts parses the given concept file and adds each concept to the concept dictionary. 267 func AddConcepts(conceptFiles []string, conceptDictionary *gauge.ConceptDictionary) ([]*gauge.Step, []ParseError, error) { 268 var conceptSteps []*gauge.Step 269 var parseResults []*ParseResult 270 for _, conceptFile := range conceptFiles { 271 concepts, parseRes := new(ConceptParser).ParseFile(conceptFile) 272 if parseRes != nil && parseRes.Warnings != nil { 273 for _, warning := range parseRes.Warnings { 274 logger.Warning(true, warning.String()) 275 } 276 } 277 parseErrors, err := AddConcept(concepts, conceptFile, conceptDictionary) 278 if err != nil { 279 return nil, nil, err 280 } 281 parseRes.ParseErrors = append(parseRes.ParseErrors, parseErrors...) 282 conceptSteps = append(conceptSteps, concepts...) 283 parseResults = append(parseResults, parseRes) 284 } 285 errs := collectAllParseErrors(parseResults) 286 return conceptSteps, errs, nil 287 } 288 289 func collectAllParseErrors(results []*ParseResult) (errs []ParseError) { 290 for _, res := range results { 291 errs = append(errs, res.ParseErrors...) 292 } 293 return 294 } 295 296 // ValidateConcepts ensures that there are no circular references within 297 func ValidateConcepts(conceptDictionary *gauge.ConceptDictionary) *ParseResult { 298 res := &ParseResult{ParseErrors: []ParseError{}} 299 var conceptsWithError []*gauge.Concept 300 for _, concept := range conceptDictionary.ConceptsMap { 301 errs := checkCircularReferencing(conceptDictionary, concept.ConceptStep, nil) 302 if errs != nil { 303 delete(conceptDictionary.ConceptsMap, concept.ConceptStep.Value) 304 res.ParseErrors = append(res.ParseErrors, errs...) 305 conceptsWithError = append(conceptsWithError, concept) 306 } 307 } 308 for _, con := range conceptsWithError { 309 removeAllReferences(conceptDictionary, con) 310 } 311 return res 312 } 313 314 func removeAllReferences(conceptDictionary *gauge.ConceptDictionary, concept *gauge.Concept) { 315 for _, cpt := range conceptDictionary.ConceptsMap { 316 var nestedSteps []*gauge.Step 317 for _, con := range cpt.ConceptStep.ConceptSteps { 318 if con.Value != concept.ConceptStep.Value { 319 nestedSteps = append(nestedSteps, con) 320 } 321 } 322 cpt.ConceptStep.ConceptSteps = nestedSteps 323 } 324 } 325 326 func checkCircularReferencing(conceptDictionary *gauge.ConceptDictionary, concept *gauge.Step, traversedSteps map[string]string) []ParseError { 327 if traversedSteps == nil { 328 traversedSteps = make(map[string]string) 329 } 330 con := conceptDictionary.Search(concept.Value) 331 if con == nil { 332 return nil 333 } 334 currentConceptFileName := con.FileName 335 traversedSteps[concept.Value] = currentConceptFileName 336 for _, step := range concept.ConceptSteps { 337 if _, exists := traversedSteps[step.Value]; exists { 338 conceptDictionary.Remove(concept.Value) 339 return []ParseError{ 340 { 341 FileName: step.FileName, 342 LineText: step.LineText, 343 LineNo: step.LineNo, 344 SpanEnd: step.LineSpanEnd, 345 Message: fmt.Sprintf("Circular reference found in concept. \"%s\" => %s:%d", concept.LineText, concept.FileName, concept.LineNo), 346 }, 347 { 348 FileName: concept.FileName, 349 LineText: concept.LineText, 350 LineNo: concept.LineNo, 351 SpanEnd: step.LineSpanEnd, 352 Message: fmt.Sprintf("Circular reference found in concept. \"%s\" => %s:%d", step.LineText, step.FileName, step.LineNo), 353 }, 354 } 355 } 356 if step.IsConcept { 357 if errs := checkCircularReferencing(conceptDictionary, step, traversedSteps); errs != nil { 358 conceptDictionary.Remove(concept.Value) 359 return errs 360 } 361 } 362 } 363 delete(traversedSteps, concept.Value) 364 return nil 365 }