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 }