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  }