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  }