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