github.com/getgauge/gauge@v1.6.9/parser/lex.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 "fmt" 12 "regexp" 13 "strings" 14 15 "github.com/getgauge/common" 16 "github.com/getgauge/gauge/env" 17 "github.com/getgauge/gauge/gauge" 18 ) 19 20 const ( 21 initial = 1 << iota 22 specScope = 1 << iota 23 scenarioScope = 1 << iota 24 commentScope = 1 << iota 25 tableScope = 1 << iota 26 tableSeparatorScope = 1 << iota 27 tableDataScope = 1 << iota 28 stepScope = 1 << iota 29 contextScope = 1 << iota 30 tearDownScope = 1 << iota 31 conceptScope = 1 << iota 32 keywordScope = 1 << iota 33 tagsScope = 1 << iota 34 newLineScope = 1 << iota 35 ) 36 37 // Token defines the type of entity identified by the lexer 38 type Token struct { 39 Kind gauge.TokenKind 40 LineNo int 41 Suffix string 42 Args []string 43 Value string 44 Lines []string 45 SpanEnd int 46 } 47 48 func (t *Token) LineText() string { 49 return strings.Join(t.Lines, " ") 50 } 51 func (parser *SpecParser) initialize() { 52 parser.processors = make(map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool)) 53 parser.processors[gauge.SpecKind] = processSpec 54 parser.processors[gauge.ScenarioKind] = processScenario 55 parser.processors[gauge.CommentKind] = processComment 56 parser.processors[gauge.StepKind] = processStep 57 parser.processors[gauge.TagKind] = processTag 58 parser.processors[gauge.TableHeader] = processTable 59 parser.processors[gauge.TableRow] = processTable 60 parser.processors[gauge.DataTableKind] = processDataTable 61 parser.processors[gauge.TearDownKind] = processTearDown 62 } 63 64 // GenerateTokens gets tokens based on the parsed line. 65 func (parser *SpecParser) GenerateTokens(specText, fileName string) ([]*Token, []ParseError) { 66 parser.initialize() 67 parser.scanner = bufio.NewScanner(strings.NewReader(specText)) 68 parser.currentState = initial 69 var errors []ParseError 70 var newToken *Token 71 var lastTokenErrorCount int 72 for line, hasLine, err := parser.nextLine(); hasLine; line, hasLine, err = parser.nextLine() { 73 if err != nil { 74 errors = append(errors, ParseError{Message: err.Error()}) 75 return nil, errors 76 } 77 trimmedLine := strings.TrimSpace(line) 78 if len(trimmedLine) == 0 { 79 addStates(&parser.currentState, newLineScope) 80 if newToken != nil && newToken.Kind == gauge.StepKind { 81 newToken.Suffix = "\n" 82 continue 83 } 84 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, Lines: []string{line}, Value: "\n", SpanEnd: parser.lineNo} 85 } else if parser.isScenarioHeading(trimmedLine) { 86 newToken = &Token{Kind: gauge.ScenarioKind, LineNo: parser.lineNo, Lines: []string{line}, Value: strings.TrimSpace(trimmedLine[2:]), SpanEnd: parser.lineNo} 87 } else if parser.isSpecHeading(trimmedLine) { 88 newToken = &Token{Kind: gauge.SpecKind, LineNo: parser.lineNo, Lines: []string{line}, Value: strings.TrimSpace(trimmedLine[1:]), SpanEnd: parser.lineNo} 89 } else if parser.isSpecUnderline(trimmedLine) { 90 if isInState(parser.currentState, commentScope) { 91 newToken = parser.tokens[len(parser.tokens)-1] 92 newToken.Kind = gauge.SpecKind 93 newToken.SpanEnd = parser.lineNo 94 parser.discardLastToken() 95 } else { 96 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, Lines: []string{line}, Value: common.TrimTrailingSpace(line), SpanEnd: parser.lineNo} 97 } 98 } else if parser.isScenarioUnderline(trimmedLine) { 99 if isInState(parser.currentState, commentScope) { 100 newToken = parser.tokens[len(parser.tokens)-1] 101 newToken.Kind = gauge.ScenarioKind 102 newToken.SpanEnd = parser.lineNo 103 parser.discardLastToken() 104 } else { 105 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, Lines: []string{line}, Value: common.TrimTrailingSpace(line), SpanEnd: parser.lineNo} 106 } 107 } else if parser.isStep(trimmedLine) { 108 newToken = &Token{Kind: gauge.StepKind, LineNo: parser.lineNo, Lines: []string{strings.TrimSpace(trimmedLine[1:])}, Value: strings.TrimSpace(trimmedLine[1:]), SpanEnd: parser.lineNo} 109 } else if found, startIndex := parser.checkTag(trimmedLine); found || isInState(parser.currentState, tagsScope) { 110 if isInState(parser.currentState, tagsScope) { 111 startIndex = 0 112 } 113 if parser.isTagEndingWithComma(trimmedLine) { 114 addStates(&parser.currentState, tagsScope) 115 } else { 116 parser.clearState() 117 } 118 newToken = &Token{Kind: gauge.TagKind, LineNo: parser.lineNo, Lines: []string{line}, Value: strings.TrimSpace(trimmedLine[startIndex:]), SpanEnd: parser.lineNo} 119 } else if parser.isTableRow(trimmedLine) { 120 kind := parser.tokenKindBasedOnCurrentState(tableScope, gauge.TableRow, gauge.TableHeader) 121 newToken = &Token{Kind: kind, LineNo: parser.lineNo, Lines: []string{line}, Value: strings.TrimSpace(trimmedLine), SpanEnd: parser.lineNo} 122 } else if value, found := parser.isDataTable(trimmedLine); found { // skipcq CRT-A0013 123 newToken = &Token{Kind: gauge.DataTableKind, LineNo: parser.lineNo, Lines: []string{line}, Value: value, SpanEnd: parser.lineNo} 124 } else if parser.isTearDown(trimmedLine) { 125 newToken = &Token{Kind: gauge.TearDownKind, LineNo: parser.lineNo, Lines: []string{line}, Value: trimmedLine, SpanEnd: parser.lineNo} 126 } else if env.AllowMultiLineStep() && newToken != nil && newToken.Kind == gauge.StepKind && !isInState(parser.currentState, newLineScope) { 127 v := strings.TrimSpace(fmt.Sprintf("%s %s", newToken.LineText(), line)) 128 newToken = parser.tokens[len(parser.tokens)-1] 129 newToken.Value = v 130 newToken.Lines = append(newToken.Lines, line) 131 newToken.SpanEnd = parser.lineNo 132 errors = errors[:lastTokenErrorCount] 133 parser.discardLastToken() 134 } else { 135 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, Lines: []string{line}, Value: common.TrimTrailingSpace(line), SpanEnd: parser.lineNo} 136 } 137 pErrs := parser.accept(newToken, fileName) 138 lastTokenErrorCount = len(pErrs) 139 errors = append(errors, pErrs...) 140 } 141 return parser.tokens, errors 142 } 143 144 func (parser *SpecParser) tokenKindBasedOnCurrentState(state int, matchingToken gauge.TokenKind, alternateToken gauge.TokenKind) gauge.TokenKind { 145 if isInState(parser.currentState, state) { 146 return matchingToken 147 } 148 return alternateToken 149 } 150 151 func (parser *SpecParser) checkTag(text string) (bool, int) { 152 lowerCased := strings.ToLower 153 tagColon := "tags:" 154 tagSpaceColon := "tags :" 155 if tagStartIndex := strings.Index(lowerCased(text), tagColon); tagStartIndex == 0 { 156 return true, len(tagColon) 157 } else if tagStartIndex := strings.Index(lowerCased(text), tagSpaceColon); tagStartIndex == 0 { 158 return true, len(tagSpaceColon) 159 } 160 return false, -1 161 } 162 163 func (parser *SpecParser) isTagEndingWithComma(text string) bool { 164 return strings.HasSuffix(strings.ToLower(text), ",") 165 } 166 167 func (parser *SpecParser) isSpecHeading(text string) bool { 168 if len(text) > 1 { 169 return text[0] == '#' && text[1] != '#' 170 } 171 return text[0] == '#' 172 } 173 174 func (parser *SpecParser) isScenarioHeading(text string) bool { 175 if len(text) > 2 { 176 return text[0] == '#' && text[1] == '#' && text[2] != '#' 177 } else if len(text) == 2 { 178 return text[0] == '#' && text[1] == '#' 179 } 180 return false 181 } 182 183 func (parser *SpecParser) isStep(text string) bool { 184 if len(text) > 1 { 185 return text[0] == '*' && text[1] != '*' 186 } 187 return text[0] == '*' 188 } 189 190 func (parser *SpecParser) isScenarioUnderline(text string) bool { 191 return isUnderline(text, rune('-')) 192 } 193 194 func (parser *SpecParser) isTableRow(text string) bool { 195 return text[0] == '|' && text[len(text)-1] == '|' 196 } 197 198 func (parser *SpecParser) isTearDown(text string) bool { 199 return isUnderline(text, rune('_')) 200 } 201 202 func (parser *SpecParser) isSpecUnderline(text string) bool { 203 return isUnderline(text, rune('=')) 204 } 205 206 func (parser *SpecParser) isDataTable(text string) (string, bool) { 207 if regexp.MustCompile(`^\s*[tT][aA][bB][lL][eE]\s*:(\s*)`).FindIndex([]byte(text)) != nil { 208 index := strings.Index(text, ":") 209 if index != -1 { 210 return "table:" + " " + strings.TrimSpace(strings.SplitAfterN(text, ":", 2)[1]), true 211 } 212 } 213 return "", false 214 } 215 216 //concept header will have dynamic param and should not be resolved through lookup, so passing nil lookup 217 func isConceptHeader(lookup *gauge.ArgLookup) bool { 218 return lookup == nil 219 } 220 221 func (parser *SpecParser) accept(token *Token, fileName string) []ParseError { 222 errs, _ := parser.processors[token.Kind](parser, token) 223 parser.tokens = append(parser.tokens, token) 224 var parseErrs []ParseError 225 for _, err := range errs { 226 parseErrs = append(parseErrs, ParseError{FileName: fileName, LineNo: token.LineNo, Message: err.Error(), LineText: token.Value, SpanEnd: token.SpanEnd}) 227 } 228 return parseErrs 229 } 230 231 func (parser *SpecParser) nextLine() (string, bool, error) { 232 scanned := parser.scanner.Scan() 233 if scanned { 234 parser.lineNo++ 235 return parser.scanner.Text(), true, nil 236 } 237 if err := parser.scanner.Err(); err != nil { 238 return "", false, err 239 } 240 241 return "", false, nil 242 } 243 244 func (parser *SpecParser) clearState() { 245 parser.currentState = 0 246 } 247 248 func (parser *SpecParser) discardLastToken() { 249 if len(parser.tokens) < 1 { 250 return 251 } 252 parser.tokens = parser.tokens[:len(parser.tokens)-1] 253 }