github.com/getgauge/gauge@v1.6.9/parser/stepParser.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  	"bytes"
    11  	"fmt"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/getgauge/gauge/logger"
    17  
    18  	"github.com/getgauge/gauge-proto/go/gauge_messages"
    19  	"github.com/getgauge/gauge/gauge"
    20  )
    21  
    22  const (
    23  	inDefault      = 1 << iota
    24  	inQuotes       = 1 << iota
    25  	inEscape       = 1 << iota
    26  	inDynamicParam = 1 << iota
    27  	inSpecialParam = 1 << iota
    28  )
    29  const (
    30  	quotes                 = '"'
    31  	escape                 = '\\'
    32  	dynamicParamStart      = '<'
    33  	dynamicParamEnd        = '>'
    34  	specialParamIdentifier = ':'
    35  )
    36  
    37  type acceptFn func(rune, int) (int, bool)
    38  
    39  // ExtractStepArgsFromToken extracts step args(Static and Dynamic) from the given step token.
    40  func ExtractStepArgsFromToken(stepToken *Token) ([]gauge.StepArg, error) {
    41  	_, argsType := extractStepValueAndParameterTypes(stepToken.Value)
    42  	if argsType != nil && len(argsType) != len(stepToken.Args) {
    43  		return nil, fmt.Errorf("Step text should not have '{static}' or '{dynamic}' or '{special}'")
    44  	}
    45  	var args []gauge.StepArg
    46  	for i, argType := range argsType {
    47  		if gauge.ArgType(argType) == gauge.Static {
    48  			args = append(args, gauge.StepArg{ArgType: gauge.Static, Value: stepToken.Args[i]})
    49  		} else {
    50  			args = append(args, gauge.StepArg{ArgType: gauge.Dynamic, Value: stepToken.Args[i]})
    51  		}
    52  	}
    53  	return args, nil
    54  }
    55  
    56  func acceptor(start rune, end rune, onEachChar func(rune, int) int, after func(state int), inState int) acceptFn {
    57  
    58  	return func(element rune, currentState int) (int, bool) {
    59  		currentState = onEachChar(element, currentState)
    60  		if element == start {
    61  			if currentState == inDefault {
    62  				return inState, true
    63  			}
    64  		}
    65  		if element == end {
    66  			if currentState&inState != 0 {
    67  				after(currentState)
    68  				return inDefault, true
    69  			}
    70  		}
    71  		return currentState, false
    72  	}
    73  }
    74  
    75  func simpleAcceptor(start rune, end rune, after func(int), inState int) acceptFn {
    76  	onEach := func(currentChar rune, state int) int {
    77  		return state
    78  	}
    79  	return acceptor(start, end, onEach, after, inState)
    80  }
    81  
    82  func processStep(parser *SpecParser, token *Token) ([]error, bool) {
    83  	if len(token.Value) == 0 {
    84  		return []error{fmt.Errorf("Step should not be blank")}, true
    85  	}
    86  
    87  	stepValue, args, err := processStepText(token.Value)
    88  	if err != nil {
    89  		return []error{err}, true
    90  	}
    91  
    92  	token.Value = stepValue
    93  	token.Args = args
    94  	parser.clearState()
    95  	return []error{}, false
    96  }
    97  
    98  func processStepText(text string) (string, []string, error) {
    99  	reservedChars := map[rune]struct{}{'{': {}, '}': {}}
   100  	var stepValue, argText bytes.Buffer
   101  
   102  	var args []string
   103  
   104  	curBuffer := func(state int) *bytes.Buffer {
   105  		if isInAnyState(state, inQuotes, inDynamicParam) {
   106  			return &argText
   107  		}
   108  		return &stepValue
   109  	}
   110  
   111  	currentState := inDefault
   112  	lastState := -1
   113  
   114  	acceptStaticParam := simpleAcceptor(rune(quotes), rune(quotes), func(int) {
   115  		_, err := stepValue.WriteString("{static}")
   116  		if err != nil {
   117  			logger.Errorf(false, "Unable to write `{static}` to step value while parsing : %s", err.Error())
   118  		}
   119  		args = append(args, argText.String())
   120  		argText.Reset()
   121  	}, inQuotes)
   122  
   123  	acceptSpecialDynamicParam := acceptor(rune(dynamicParamStart), rune(dynamicParamEnd), func(currentChar rune, state int) int {
   124  		if currentChar == specialParamIdentifier && state == inDynamicParam {
   125  			return state | inSpecialParam
   126  		}
   127  		return state
   128  	}, func(currentState int) {
   129  		if isInState(currentState, inSpecialParam) {
   130  			_, err := stepValue.WriteString("{special}")
   131  			if err != nil {
   132  				logger.Errorf(false, "Unable to write `{special}` to step value while parsing : %s", err.Error())
   133  			}
   134  		} else {
   135  			_, err := stepValue.WriteString("{dynamic}")
   136  			if err != nil {
   137  				logger.Errorf(false, "Unable to write `{special}` to step value while parsing : %s", err.Error())
   138  			}
   139  		}
   140  		args = append(args, argText.String())
   141  		argText.Reset()
   142  	}, inDynamicParam)
   143  
   144  	var inParamBoundary bool
   145  	for _, element := range text {
   146  		if currentState == inEscape {
   147  			currentState = lastState
   148  			if _, isReservedChar := reservedChars[element]; currentState == inDefault && !isReservedChar {
   149  				_, err := curBuffer(currentState).WriteRune(escape)
   150  				if err != nil {
   151  					logger.Errorf(false, "Unable to write `\\\\`(escape) to step value while parsing : %s", err.Error())
   152  				}
   153  			} else {
   154  				element = getEscapedRuneIfValid(element)
   155  			}
   156  		} else if element == escape {
   157  			lastState = currentState
   158  			currentState = inEscape
   159  			continue
   160  		} else if currentState, inParamBoundary = acceptSpecialDynamicParam(element, currentState); inParamBoundary {
   161  			continue
   162  		} else if currentState, inParamBoundary = acceptStaticParam(element, currentState); inParamBoundary {
   163  			continue
   164  		} else if _, isReservedChar := reservedChars[element]; currentState == inDefault && isReservedChar {
   165  			return "", nil, fmt.Errorf("'%c' is a reserved character and should be escaped", element)
   166  		}
   167  
   168  		_, err := curBuffer(currentState).WriteRune(element)
   169  		if err != nil {
   170  			logger.Errorf(false, "Unable to write `%c` to step value while parsing : %s", element, err.Error())
   171  		}
   172  	}
   173  
   174  	// If it is a valid step, the state should be default when the control reaches here
   175  	if currentState == inQuotes {
   176  		return "", nil, fmt.Errorf("String not terminated")
   177  	} else if isInState(currentState, inDynamicParam) {
   178  		return "", nil, fmt.Errorf("Dynamic parameter not terminated")
   179  	}
   180  
   181  	return strings.TrimSpace(stepValue.String()), args, nil
   182  
   183  }
   184  
   185  func getEscapedRuneIfValid(element rune) rune {
   186  	allEscapeChars := map[string]rune{"t": '\t', "n": '\n'}
   187  	elementToStr, err := strconv.Unquote(strconv.QuoteRune(element))
   188  	if err != nil {
   189  		return element
   190  	}
   191  	for key, val := range allEscapeChars {
   192  		if key == elementToStr {
   193  			return val
   194  		}
   195  	}
   196  	return element
   197  }
   198  
   199  func extractStepValueAndParameterTypes(stepTokenValue string) (string, []string) {
   200  	argsType := make([]string, 0)
   201  	r := regexp.MustCompile("{(dynamic|static|special)}")
   202  	/*
   203  		enter {dynamic} and {static}
   204  		returns
   205  		[
   206  		["{dynamic}","dynamic"]
   207  		["{static}","static"]
   208  		]
   209  	*/
   210  	args := r.FindAllStringSubmatch(stepTokenValue, -1)
   211  
   212  	if args == nil {
   213  		return stepTokenValue, nil
   214  	}
   215  	for _, arg := range args {
   216  		//arg[1] extracts the first group
   217  		argsType = append(argsType, arg[1])
   218  	}
   219  	return r.ReplaceAllString(stepTokenValue, gauge.ParameterPlaceholder), argsType
   220  }
   221  
   222  func createStepArg(argValue string, typeOfArg string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   223  	switch typeOfArg {
   224  	case "special":
   225  		resolvedArgValue, err := newSpecialTypeResolver().resolve(argValue)
   226  		if err != nil {
   227  			switch err.(type) {
   228  			case invalidSpecialParamError:
   229  				return treatArgAsDynamic(argValue, token, lookup, fileName)
   230  			default:
   231  				return &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue}, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText()}}}
   232  			}
   233  		}
   234  		return resolvedArgValue, nil
   235  	case "static":
   236  		return &gauge.StepArg{ArgType: gauge.Static, Value: argValue}, nil
   237  	default:
   238  		return validateDynamicArg(argValue, token, lookup, fileName)
   239  	}
   240  }
   241  
   242  func treatArgAsDynamic(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   243  	parseRes := &ParseResult{Warnings: []*Warning{&Warning{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Could not resolve special param type <%s>. Treating it as dynamic param.", argValue)}}}
   244  	stepArg, result := validateDynamicArg(argValue, token, lookup, fileName)
   245  	if result != nil {
   246  		if len(result.ParseErrors) > 0 {
   247  			parseRes.ParseErrors = result.ParseErrors
   248  		}
   249  		if result.Warnings != nil {
   250  			parseRes.Warnings = append(parseRes.Warnings, result.Warnings...)
   251  		}
   252  	}
   253  	return stepArg, parseRes
   254  }
   255  
   256  func validateDynamicArg(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) {
   257  	stepArgument := &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue}
   258  	if !isConceptHeader(lookup) && !lookup.ContainsArg(argValue) {
   259  		return stepArgument, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, SpanEnd: token.SpanEnd, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText()}}}
   260  	}
   261  
   262  	return stepArgument, nil
   263  }
   264  
   265  // ConvertToStepText accumulates fragments of a step, (ex. parameters) and returns the step text
   266  // used to generate the annotation text in a step implementation
   267  func ConvertToStepText(fragments []*gauge_messages.Fragment) string {
   268  	stepText := ""
   269  	var i int
   270  	for _, fragment := range fragments {
   271  		value := ""
   272  		if fragment.GetFragmentType() == gauge_messages.Fragment_Text {
   273  			value = fragment.GetText()
   274  		} else {
   275  			switch fragment.GetParameter().GetParameterType() {
   276  			case gauge_messages.Parameter_Static:
   277  				value = fmt.Sprintf("\"%s\"", fragment.GetParameter().GetValue())
   278  			case gauge_messages.Parameter_Dynamic:
   279  				value = fmt.Sprintf("<%s>", fragment.GetParameter().GetValue())
   280  			case gauge_messages.Parameter_Special_String:
   281  				i++
   282  				value = fmt.Sprintf("<%s%d>", "file", i)
   283  			case gauge_messages.Parameter_Special_Table:
   284  				i++
   285  				value = fmt.Sprintf("<%s%d>", "table", i)
   286  			}
   287  		}
   288  		stepText += value
   289  	}
   290  	return stepText
   291  }