github.com/hashicorp/hcl/v2@v2.20.0/hclsyntax/peeker.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hclsyntax
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  )
    15  
    16  // This is set to true at init() time in tests, to enable more useful output
    17  // if a stack discipline error is detected. It should not be enabled in
    18  // normal mode since there is a performance penalty from accessing the
    19  // runtime stack to produce the traces, but could be temporarily set to
    20  // true for debugging if desired.
    21  var tracePeekerNewlinesStack = false
    22  
    23  type peeker struct {
    24  	Tokens    Tokens
    25  	NextIndex int
    26  
    27  	IncludeComments      bool
    28  	IncludeNewlinesStack []bool
    29  
    30  	// used only when tracePeekerNewlinesStack is set
    31  	newlineStackChanges []peekerNewlineStackChange
    32  }
    33  
    34  // for use in debugging the stack usage only
    35  type peekerNewlineStackChange struct {
    36  	Pushing bool // if false, then popping
    37  	Frame   runtime.Frame
    38  	Include bool
    39  }
    40  
    41  func newPeeker(tokens Tokens, includeComments bool) *peeker {
    42  	return &peeker{
    43  		Tokens:          tokens,
    44  		IncludeComments: includeComments,
    45  
    46  		IncludeNewlinesStack: []bool{true},
    47  	}
    48  }
    49  
    50  func (p *peeker) Peek() Token {
    51  	ret, _ := p.nextToken()
    52  	return ret
    53  }
    54  
    55  func (p *peeker) Read() Token {
    56  	ret, nextIdx := p.nextToken()
    57  	p.NextIndex = nextIdx
    58  	return ret
    59  }
    60  
    61  func (p *peeker) NextRange() hcl.Range {
    62  	return p.Peek().Range
    63  }
    64  
    65  func (p *peeker) PrevRange() hcl.Range {
    66  	if p.NextIndex == 0 {
    67  		return p.NextRange()
    68  	}
    69  
    70  	return p.Tokens[p.NextIndex-1].Range
    71  }
    72  
    73  func (p *peeker) nextToken() (Token, int) {
    74  	for i := p.NextIndex; i < len(p.Tokens); i++ {
    75  		tok := p.Tokens[i]
    76  		switch tok.Type {
    77  		case TokenComment:
    78  			if !p.IncludeComments {
    79  				// Single-line comment tokens, starting with # or //, absorb
    80  				// the trailing newline that terminates them as part of their
    81  				// bytes. When we're filtering out comments, we must as a
    82  				// special case transform these to newline tokens in order
    83  				// to properly parse newline-terminated block items.
    84  
    85  				if p.includingNewlines() {
    86  					if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
    87  						fakeNewline := Token{
    88  							Type:  TokenNewline,
    89  							Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)],
    90  
    91  							// We use the whole token range as the newline
    92  							// range, even though that's a little... weird,
    93  							// because otherwise we'd need to go count
    94  							// characters again in order to figure out the
    95  							// column of the newline, and that complexity
    96  							// isn't justified when ranges of newlines are
    97  							// so rarely printed anyway.
    98  							Range: tok.Range,
    99  						}
   100  						return fakeNewline, i + 1
   101  					}
   102  				}
   103  
   104  				continue
   105  			}
   106  		case TokenNewline:
   107  			if !p.includingNewlines() {
   108  				continue
   109  			}
   110  		}
   111  
   112  		return tok, i + 1
   113  	}
   114  
   115  	// if we fall out here then we'll return the EOF token, and leave
   116  	// our index pointed off the end of the array so we'll keep
   117  	// returning EOF in future too.
   118  	return p.Tokens[len(p.Tokens)-1], len(p.Tokens)
   119  }
   120  
   121  func (p *peeker) includingNewlines() bool {
   122  	return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1]
   123  }
   124  
   125  func (p *peeker) PushIncludeNewlines(include bool) {
   126  	if tracePeekerNewlinesStack {
   127  		// Record who called us so that we can more easily track down any
   128  		// mismanagement of the stack in the parser.
   129  		callers := []uintptr{0}
   130  		runtime.Callers(2, callers)
   131  		frames := runtime.CallersFrames(callers)
   132  		frame, _ := frames.Next()
   133  		p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
   134  			true, frame, include,
   135  		})
   136  	}
   137  
   138  	p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include)
   139  }
   140  
   141  func (p *peeker) PopIncludeNewlines() bool {
   142  	stack := p.IncludeNewlinesStack
   143  	remain, ret := stack[:len(stack)-1], stack[len(stack)-1]
   144  	p.IncludeNewlinesStack = remain
   145  
   146  	if tracePeekerNewlinesStack {
   147  		// Record who called us so that we can more easily track down any
   148  		// mismanagement of the stack in the parser.
   149  		callers := []uintptr{0}
   150  		runtime.Callers(2, callers)
   151  		frames := runtime.CallersFrames(callers)
   152  		frame, _ := frames.Next()
   153  		p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
   154  			false, frame, ret,
   155  		})
   156  	}
   157  
   158  	return ret
   159  }
   160  
   161  // AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing
   162  // panicking if it is not. This can be used to catch stack mismanagement that
   163  // might otherwise just cause confusing downstream errors.
   164  //
   165  // This function is a no-op if the stack is empty when called.
   166  //
   167  // If newlines stack tracing is enabled by setting the global variable
   168  // tracePeekerNewlinesStack at init time, a full log of all of the push/pop
   169  // calls will be produced to help identify which caller in the parser is
   170  // misbehaving.
   171  func (p *peeker) AssertEmptyIncludeNewlinesStack() {
   172  	if len(p.IncludeNewlinesStack) != 1 {
   173  		// Should never happen; indicates mismanagement of the stack inside
   174  		// the parser.
   175  		if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above
   176  			panic(fmt.Errorf(
   177  				"non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s",
   178  				len(p.IncludeNewlinesStack)-1,
   179  				formatPeekerNewlineStackChanges(p.newlineStackChanges),
   180  			))
   181  		} else {
   182  			panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack))
   183  		}
   184  	}
   185  }
   186  
   187  func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string {
   188  	indent := 0
   189  	var buf bytes.Buffer
   190  	for _, change := range changes {
   191  		funcName := change.Frame.Function
   192  		if idx := strings.LastIndexByte(funcName, '.'); idx != -1 {
   193  			funcName = funcName[idx+1:]
   194  		}
   195  		filename := change.Frame.File
   196  		if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 {
   197  			filename = filename[idx+1:]
   198  		}
   199  
   200  		switch change.Pushing {
   201  
   202  		case true:
   203  			buf.WriteString(strings.Repeat("    ", indent))
   204  			fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
   205  			indent++
   206  
   207  		case false:
   208  			indent--
   209  			buf.WriteString(strings.Repeat("    ", indent))
   210  			fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
   211  
   212  		}
   213  	}
   214  	return buf.String()
   215  }