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  }