whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Module file printer.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format returns a go.mod file as a byte slice, formatted in standard style.
|
||||
func Format(f *FileSyntax) []byte {
|
||||
pr := &printer{}
|
||||
pr.file(f)
|
||||
|
||||
// remove trailing blank lines
|
||||
b := pr.Bytes()
|
||||
for len(b) > 0 && b[len(b)-1] == '\n' && (len(b) == 1 || b[len(b)-2] == '\n') {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// A printer collects the state during printing of a file or expression.
|
||||
type printer struct {
|
||||
bytes.Buffer // output buffer
|
||||
comment []Comment // pending end-of-line comments
|
||||
margin int // left margin (indent), a number of tabs
|
||||
}
|
||||
|
||||
// printf prints to the buffer.
|
||||
func (p *printer) printf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(p, format, args...)
|
||||
}
|
||||
|
||||
// indent returns the position on the current line, in bytes, 0-indexed.
|
||||
func (p *printer) indent() int {
|
||||
b := p.Bytes()
|
||||
n := 0
|
||||
for n < len(b) && b[len(b)-1-n] != '\n' {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// newline ends the current line, flushing end-of-line comments.
|
||||
func (p *printer) newline() {
|
||||
if len(p.comment) > 0 {
|
||||
p.printf(" ")
|
||||
for i, com := range p.comment {
|
||||
if i > 0 {
|
||||
p.trim()
|
||||
p.printf("\n")
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
}
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
}
|
||||
p.comment = p.comment[:0]
|
||||
}
|
||||
|
||||
p.trim()
|
||||
if b := p.Bytes(); len(b) == 0 || (len(b) >= 2 && b[len(b)-1] == '\n' && b[len(b)-2] == '\n') {
|
||||
// skip the blank line at top of file or after a blank line
|
||||
} else {
|
||||
p.printf("\n")
|
||||
}
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
}
|
||||
|
||||
// trim removes trailing spaces and tabs from the current line.
|
||||
func (p *printer) trim() {
|
||||
// Remove trailing spaces and tabs from line we're about to end.
|
||||
b := p.Bytes()
|
||||
n := len(b)
|
||||
for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') {
|
||||
n--
|
||||
}
|
||||
p.Truncate(n)
|
||||
}
|
||||
|
||||
// file formats the given file into the print buffer.
|
||||
func (p *printer) file(f *FileSyntax) {
|
||||
for _, com := range f.Before {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
|
||||
for i, stmt := range f.Stmt {
|
||||
switch x := stmt.(type) {
|
||||
case *CommentBlock:
|
||||
// comments already handled
|
||||
p.expr(x)
|
||||
|
||||
default:
|
||||
p.expr(x)
|
||||
p.newline()
|
||||
}
|
||||
|
||||
for _, com := range stmt.Comment().After {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
|
||||
if i+1 < len(f.Stmt) {
|
||||
p.newline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *printer) expr(x Expr) {
|
||||
// Emit line-comments preceding this expression.
|
||||
if before := x.Comment().Before; len(before) > 0 {
|
||||
// Want to print a line comment.
|
||||
// Line comments must be at the current margin.
|
||||
p.trim()
|
||||
if p.indent() > 0 {
|
||||
// There's other text on the line. Start a new line.
|
||||
p.printf("\n")
|
||||
}
|
||||
// Re-indent to margin.
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
for _, com := range before {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
}
|
||||
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
panic(fmt.Errorf("printer: unexpected type %T", x))
|
||||
|
||||
case *CommentBlock:
|
||||
// done
|
||||
|
||||
case *LParen:
|
||||
p.printf("(")
|
||||
case *RParen:
|
||||
p.printf(")")
|
||||
|
||||
case *Line:
|
||||
p.tokens(x.Token)
|
||||
|
||||
case *LineBlock:
|
||||
p.tokens(x.Token)
|
||||
p.printf(" ")
|
||||
p.expr(&x.LParen)
|
||||
p.margin++
|
||||
for _, l := range x.Line {
|
||||
p.newline()
|
||||
p.expr(l)
|
||||
}
|
||||
p.margin--
|
||||
p.newline()
|
||||
p.expr(&x.RParen)
|
||||
}
|
||||
|
||||
// Queue end-of-line comments for printing when we
|
||||
// reach the end of the line.
|
||||
p.comment = append(p.comment, x.Comment().Suffix...)
|
||||
}
|
||||
|
||||
func (p *printer) tokens(tokens []string) {
|
||||
sep := ""
|
||||
for _, t := range tokens {
|
||||
if t == "," || t == ")" || t == "]" || t == "}" {
|
||||
sep = ""
|
||||
}
|
||||
p.printf("%s%s", sep, t)
|
||||
sep = " "
|
||||
if t == "(" || t == "[" || t == "{" {
|
||||
sep = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,959 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A Position describes an arbitrary source position in a file, including the
|
||||
// file, line, column, and byte offset.
|
||||
type Position struct {
|
||||
Line int // line in input (starting at 1)
|
||||
LineRune int // rune in line (starting at 1)
|
||||
Byte int // byte in input (starting at 0)
|
||||
}
|
||||
|
||||
// add returns the position at the end of s, assuming it starts at p.
|
||||
func (p Position) add(s string) Position {
|
||||
p.Byte += len(s)
|
||||
if n := strings.Count(s, "\n"); n > 0 {
|
||||
p.Line += n
|
||||
s = s[strings.LastIndex(s, "\n")+1:]
|
||||
p.LineRune = 1
|
||||
}
|
||||
p.LineRune += utf8.RuneCountInString(s)
|
||||
return p
|
||||
}
|
||||
|
||||
// An Expr represents an input element.
|
||||
type Expr interface {
|
||||
// Span returns the start and end position of the expression,
|
||||
// excluding leading or trailing comments.
|
||||
Span() (start, end Position)
|
||||
|
||||
// Comment returns the comments attached to the expression.
|
||||
// This method would normally be named 'Comments' but that
|
||||
// would interfere with embedding a type of the same name.
|
||||
Comment() *Comments
|
||||
}
|
||||
|
||||
// A Comment represents a single // comment.
|
||||
type Comment struct {
|
||||
Start Position
|
||||
Token string // without trailing newline
|
||||
Suffix bool // an end of line (not whole line) comment
|
||||
}
|
||||
|
||||
// Comments collects the comments associated with an expression.
|
||||
type Comments struct {
|
||||
Before []Comment // whole-line comments before this expression
|
||||
Suffix []Comment // end-of-line comments after this expression
|
||||
|
||||
// For top-level expressions only, After lists whole-line
|
||||
// comments following the expression.
|
||||
After []Comment
|
||||
}
|
||||
|
||||
// Comment returns the receiver. This isn't useful by itself, but
|
||||
// a [Comments] struct is embedded into all the expression
|
||||
// implementation types, and this gives each of those a Comment
|
||||
// method to satisfy the Expr interface.
|
||||
func (c *Comments) Comment() *Comments {
|
||||
return c
|
||||
}
|
||||
|
||||
// A FileSyntax represents an entire go.mod file.
|
||||
type FileSyntax struct {
|
||||
Name string // file path
|
||||
Comments
|
||||
Stmt []Expr
|
||||
}
|
||||
|
||||
func (x *FileSyntax) Span() (start, end Position) {
|
||||
if len(x.Stmt) == 0 {
|
||||
return
|
||||
}
|
||||
start, _ = x.Stmt[0].Span()
|
||||
_, end = x.Stmt[len(x.Stmt)-1].Span()
|
||||
return start, end
|
||||
}
|
||||
|
||||
// addLine adds a line containing the given tokens to the file.
|
||||
//
|
||||
// If the first token of the hint matches the first token of the
|
||||
// line, the new line is added at the end of the block containing hint,
|
||||
// extracting hint into a new block if it is not yet in one.
|
||||
//
|
||||
// If the hint is non-nil buts its first token does not match,
|
||||
// the new line is added after the block containing hint
|
||||
// (or hint itself, if not in a block).
|
||||
//
|
||||
// If no hint is provided, addLine appends the line to the end of
|
||||
// the last block with a matching first token,
|
||||
// or to the end of the file if no such block exists.
|
||||
func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
|
||||
if hint == nil {
|
||||
// If no hint given, add to the last statement of the given type.
|
||||
Loop:
|
||||
for i := len(x.Stmt) - 1; i >= 0; i-- {
|
||||
stmt := x.Stmt[i]
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt.Token != nil && stmt.Token[0] == tokens[0] {
|
||||
hint = stmt
|
||||
break Loop
|
||||
}
|
||||
case *LineBlock:
|
||||
if stmt.Token[0] == tokens[0] {
|
||||
hint = stmt
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newLineAfter := func(i int) *Line {
|
||||
new := &Line{Token: tokens}
|
||||
if i == len(x.Stmt) {
|
||||
x.Stmt = append(x.Stmt, new)
|
||||
} else {
|
||||
x.Stmt = append(x.Stmt, nil)
|
||||
copy(x.Stmt[i+2:], x.Stmt[i+1:])
|
||||
x.Stmt[i+1] = new
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
if hint != nil {
|
||||
for i, stmt := range x.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt == hint {
|
||||
if stmt.Token == nil || stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
// Convert line to line block.
|
||||
stmt.InBlock = true
|
||||
block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}}
|
||||
stmt.Token = stmt.Token[1:]
|
||||
x.Stmt[i] = block
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
block.Line = append(block.Line, new)
|
||||
return new
|
||||
}
|
||||
|
||||
case *LineBlock:
|
||||
if stmt == hint {
|
||||
if stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
stmt.Line = append(stmt.Line, new)
|
||||
return new
|
||||
}
|
||||
|
||||
for j, line := range stmt.Line {
|
||||
if line == hint {
|
||||
if stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
// Add new line after hint within the block.
|
||||
stmt.Line = append(stmt.Line, nil)
|
||||
copy(stmt.Line[j+2:], stmt.Line[j+1:])
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
stmt.Line[j+1] = new
|
||||
return new
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new := &Line{Token: tokens}
|
||||
x.Stmt = append(x.Stmt, new)
|
||||
return new
|
||||
}
|
||||
|
||||
func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
|
||||
if line.InBlock {
|
||||
tokens = tokens[1:]
|
||||
}
|
||||
line.Token = tokens
|
||||
}
|
||||
|
||||
// markRemoved modifies line so that it (and its end-of-line comment, if any)
|
||||
// will be dropped by (*FileSyntax).Cleanup.
|
||||
func (line *Line) markRemoved() {
|
||||
line.Token = nil
|
||||
line.Comments.Suffix = nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the file syntax x after any edit operations.
|
||||
// To avoid quadratic behavior, (*Line).markRemoved marks the line as dead
|
||||
// by setting line.Token = nil but does not remove it from the slice
|
||||
// in which it appears. After edits have all been indicated,
|
||||
// calling Cleanup cleans out the dead lines.
|
||||
func (x *FileSyntax) Cleanup() {
|
||||
w := 0
|
||||
for _, stmt := range x.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt.Token == nil {
|
||||
continue
|
||||
}
|
||||
case *LineBlock:
|
||||
ww := 0
|
||||
for _, line := range stmt.Line {
|
||||
if line.Token != nil {
|
||||
stmt.Line[ww] = line
|
||||
ww++
|
||||
}
|
||||
}
|
||||
if ww == 0 {
|
||||
continue
|
||||
}
|
||||
if ww == 1 && len(stmt.RParen.Comments.Before) == 0 {
|
||||
// Collapse block into single line but keep the Line reference used by the
|
||||
// parsed File structure.
|
||||
*stmt.Line[0] = Line{
|
||||
Comments: Comments{
|
||||
Before: commentsAdd(stmt.Before, stmt.Line[0].Before),
|
||||
Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix),
|
||||
After: commentsAdd(stmt.Line[0].After, stmt.After),
|
||||
},
|
||||
Token: stringsAdd(stmt.Token, stmt.Line[0].Token),
|
||||
}
|
||||
x.Stmt[w] = stmt.Line[0]
|
||||
w++
|
||||
continue
|
||||
}
|
||||
stmt.Line = stmt.Line[:ww]
|
||||
}
|
||||
x.Stmt[w] = stmt
|
||||
w++
|
||||
}
|
||||
x.Stmt = x.Stmt[:w]
|
||||
}
|
||||
|
||||
func commentsAdd(x, y []Comment) []Comment {
|
||||
return append(x[:len(x):len(x)], y...)
|
||||
}
|
||||
|
||||
func stringsAdd(x, y []string) []string {
|
||||
return append(x[:len(x):len(x)], y...)
|
||||
}
|
||||
|
||||
// A CommentBlock represents a top-level block of comments separate
|
||||
// from any rule.
|
||||
type CommentBlock struct {
|
||||
Comments
|
||||
Start Position
|
||||
}
|
||||
|
||||
func (x *CommentBlock) Span() (start, end Position) {
|
||||
return x.Start, x.Start
|
||||
}
|
||||
|
||||
// A Line is a single line of tokens.
|
||||
type Line struct {
|
||||
Comments
|
||||
Start Position
|
||||
Token []string
|
||||
InBlock bool
|
||||
End Position
|
||||
}
|
||||
|
||||
func (x *Line) Span() (start, end Position) {
|
||||
return x.Start, x.End
|
||||
}
|
||||
|
||||
// A LineBlock is a factored block of lines, like
|
||||
//
|
||||
// require (
|
||||
// "x"
|
||||
// "y"
|
||||
// )
|
||||
type LineBlock struct {
|
||||
Comments
|
||||
Start Position
|
||||
LParen LParen
|
||||
Token []string
|
||||
Line []*Line
|
||||
RParen RParen
|
||||
}
|
||||
|
||||
func (x *LineBlock) Span() (start, end Position) {
|
||||
return x.Start, x.RParen.Pos.add(")")
|
||||
}
|
||||
|
||||
// An LParen represents the beginning of a parenthesized line block.
|
||||
// It is a place to store suffix comments.
|
||||
type LParen struct {
|
||||
Comments
|
||||
Pos Position
|
||||
}
|
||||
|
||||
func (x *LParen) Span() (start, end Position) {
|
||||
return x.Pos, x.Pos.add(")")
|
||||
}
|
||||
|
||||
// An RParen represents the end of a parenthesized line block.
|
||||
// It is a place to store whole-line (before) comments.
|
||||
type RParen struct {
|
||||
Comments
|
||||
Pos Position
|
||||
}
|
||||
|
||||
func (x *RParen) Span() (start, end Position) {
|
||||
return x.Pos, x.Pos.add(")")
|
||||
}
|
||||
|
||||
// An input represents a single input file being parsed.
|
||||
type input struct {
|
||||
// Lexing state.
|
||||
filename string // name of input file, for errors
|
||||
complete []byte // entire input
|
||||
remaining []byte // remaining input
|
||||
tokenStart []byte // token being scanned to end of input
|
||||
token token // next token to be returned by lex, peek
|
||||
pos Position // current input position
|
||||
comments []Comment // accumulated comments
|
||||
|
||||
// Parser state.
|
||||
file *FileSyntax // returned top-level syntax tree
|
||||
parseErrors ErrorList // errors encountered during parsing
|
||||
|
||||
// Comment assignment state.
|
||||
pre []Expr // all expressions, in preorder traversal
|
||||
post []Expr // all expressions, in postorder traversal
|
||||
}
|
||||
|
||||
func newInput(filename string, data []byte) *input {
|
||||
return &input{
|
||||
filename: filename,
|
||||
complete: data,
|
||||
remaining: data,
|
||||
pos: Position{Line: 1, LineRune: 1, Byte: 0},
|
||||
}
|
||||
}
|
||||
|
||||
// parse parses the input file.
|
||||
func parse(file string, data []byte) (f *FileSyntax, err error) {
|
||||
// The parser panics for both routine errors like syntax errors
|
||||
// and for programmer bugs like array index errors.
|
||||
// Turn both into error returns. Catching bug panics is
|
||||
// especially important when processing many files.
|
||||
in := newInput(file, data)
|
||||
defer func() {
|
||||
if e := recover(); e != nil && e != &in.parseErrors {
|
||||
in.parseErrors = append(in.parseErrors, Error{
|
||||
Filename: in.filename,
|
||||
Pos: in.pos,
|
||||
Err: fmt.Errorf("internal error: %v", e),
|
||||
})
|
||||
}
|
||||
if err == nil && len(in.parseErrors) > 0 {
|
||||
err = in.parseErrors
|
||||
}
|
||||
}()
|
||||
|
||||
// Prime the lexer by reading in the first token. It will be available
|
||||
// in the next peek() or lex() call.
|
||||
in.readToken()
|
||||
|
||||
// Invoke the parser.
|
||||
in.parseFile()
|
||||
if len(in.parseErrors) > 0 {
|
||||
return nil, in.parseErrors
|
||||
}
|
||||
in.file.Name = in.filename
|
||||
|
||||
// Assign comments to nearby syntax.
|
||||
in.assignComments()
|
||||
|
||||
return in.file, nil
|
||||
}
|
||||
|
||||
// Error is called to report an error.
|
||||
// Error does not return: it panics.
|
||||
func (in *input) Error(s string) {
|
||||
in.parseErrors = append(in.parseErrors, Error{
|
||||
Filename: in.filename,
|
||||
Pos: in.pos,
|
||||
Err: errors.New(s),
|
||||
})
|
||||
panic(&in.parseErrors)
|
||||
}
|
||||
|
||||
// eof reports whether the input has reached end of file.
|
||||
func (in *input) eof() bool {
|
||||
return len(in.remaining) == 0
|
||||
}
|
||||
|
||||
// peekRune returns the next rune in the input without consuming it.
|
||||
func (in *input) peekRune() int {
|
||||
if len(in.remaining) == 0 {
|
||||
return 0
|
||||
}
|
||||
r, _ := utf8.DecodeRune(in.remaining)
|
||||
return int(r)
|
||||
}
|
||||
|
||||
// peekPrefix reports whether the remaining input begins with the given prefix.
|
||||
func (in *input) peekPrefix(prefix string) bool {
|
||||
// This is like bytes.HasPrefix(in.remaining, []byte(prefix))
|
||||
// but without the allocation of the []byte copy of prefix.
|
||||
for i := 0; i < len(prefix); i++ {
|
||||
if i >= len(in.remaining) || in.remaining[i] != prefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// readRune consumes and returns the next rune in the input.
|
||||
func (in *input) readRune() int {
|
||||
if len(in.remaining) == 0 {
|
||||
in.Error("internal lexer error: readRune at EOF")
|
||||
}
|
||||
r, size := utf8.DecodeRune(in.remaining)
|
||||
in.remaining = in.remaining[size:]
|
||||
if r == '\n' {
|
||||
in.pos.Line++
|
||||
in.pos.LineRune = 1
|
||||
} else {
|
||||
in.pos.LineRune++
|
||||
}
|
||||
in.pos.Byte += size
|
||||
return int(r)
|
||||
}
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
pos Position
|
||||
endPos Position
|
||||
text string
|
||||
}
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
_EOF tokenKind = -(iota + 1)
|
||||
_EOLCOMMENT
|
||||
_IDENT
|
||||
_STRING
|
||||
_COMMENT
|
||||
|
||||
// newlines and punctuation tokens are allowed as ASCII codes.
|
||||
)
|
||||
|
||||
func (k tokenKind) isComment() bool {
|
||||
return k == _COMMENT || k == _EOLCOMMENT
|
||||
}
|
||||
|
||||
// isEOL returns whether a token terminates a line.
|
||||
func (k tokenKind) isEOL() bool {
|
||||
return k == _EOF || k == _EOLCOMMENT || k == '\n'
|
||||
}
|
||||
|
||||
// startToken marks the beginning of the next input token.
|
||||
// It must be followed by a call to endToken, once the token's text has
|
||||
// been consumed using readRune.
|
||||
func (in *input) startToken() {
|
||||
in.tokenStart = in.remaining
|
||||
in.token.text = ""
|
||||
in.token.pos = in.pos
|
||||
}
|
||||
|
||||
// endToken marks the end of an input token.
|
||||
// It records the actual token string in tok.text.
|
||||
// A single trailing newline (LF or CRLF) will be removed from comment tokens.
|
||||
func (in *input) endToken(kind tokenKind) {
|
||||
in.token.kind = kind
|
||||
text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)])
|
||||
if kind.isComment() {
|
||||
if strings.HasSuffix(text, "\r\n") {
|
||||
text = text[:len(text)-2]
|
||||
} else {
|
||||
text = strings.TrimSuffix(text, "\n")
|
||||
}
|
||||
}
|
||||
in.token.text = text
|
||||
in.token.endPos = in.pos
|
||||
}
|
||||
|
||||
// peek returns the kind of the next token returned by lex.
|
||||
func (in *input) peek() tokenKind {
|
||||
return in.token.kind
|
||||
}
|
||||
|
||||
// lex is called from the parser to obtain the next input token.
|
||||
func (in *input) lex() token {
|
||||
tok := in.token
|
||||
in.readToken()
|
||||
return tok
|
||||
}
|
||||
|
||||
// readToken lexes the next token from the text and stores it in in.token.
|
||||
func (in *input) readToken() {
|
||||
// Skip past spaces, stopping at non-space or EOF.
|
||||
for !in.eof() {
|
||||
c := in.peekRune()
|
||||
if c == ' ' || c == '\t' || c == '\r' {
|
||||
in.readRune()
|
||||
continue
|
||||
}
|
||||
|
||||
// Comment runs to end of line.
|
||||
if in.peekPrefix("//") {
|
||||
in.startToken()
|
||||
|
||||
// Is this comment the only thing on its line?
|
||||
// Find the last \n before this // and see if it's all
|
||||
// spaces from there to here.
|
||||
i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n"))
|
||||
suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0
|
||||
in.readRune()
|
||||
in.readRune()
|
||||
|
||||
// Consume comment.
|
||||
for len(in.remaining) > 0 && in.readRune() != '\n' {
|
||||
}
|
||||
|
||||
// If we are at top level (not in a statement), hand the comment to
|
||||
// the parser as a _COMMENT token. The grammar is written
|
||||
// to handle top-level comments itself.
|
||||
if !suffix {
|
||||
in.endToken(_COMMENT)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, save comment for later attachment to syntax tree.
|
||||
in.endToken(_EOLCOMMENT)
|
||||
in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix})
|
||||
return
|
||||
}
|
||||
|
||||
if in.peekPrefix("/*") {
|
||||
in.Error("mod files must use // comments (not /* */ comments)")
|
||||
}
|
||||
|
||||
// Found non-space non-comment.
|
||||
break
|
||||
}
|
||||
|
||||
// Found the beginning of the next token.
|
||||
in.startToken()
|
||||
|
||||
// End of file.
|
||||
if in.eof() {
|
||||
in.endToken(_EOF)
|
||||
return
|
||||
}
|
||||
|
||||
// Punctuation tokens.
|
||||
switch c := in.peekRune(); c {
|
||||
case '\n', '(', ')', '[', ']', '{', '}', ',':
|
||||
in.readRune()
|
||||
in.endToken(tokenKind(c))
|
||||
return
|
||||
|
||||
case '"', '`': // quoted string
|
||||
quote := c
|
||||
in.readRune()
|
||||
for {
|
||||
if in.eof() {
|
||||
in.pos = in.token.pos
|
||||
in.Error("unexpected EOF in string")
|
||||
}
|
||||
if in.peekRune() == '\n' {
|
||||
in.Error("unexpected newline in string")
|
||||
}
|
||||
c := in.readRune()
|
||||
if c == quote {
|
||||
break
|
||||
}
|
||||
if c == '\\' && quote != '`' {
|
||||
if in.eof() {
|
||||
in.pos = in.token.pos
|
||||
in.Error("unexpected EOF in string")
|
||||
}
|
||||
in.readRune()
|
||||
}
|
||||
}
|
||||
in.endToken(_STRING)
|
||||
return
|
||||
}
|
||||
|
||||
// Checked all punctuation. Must be identifier token.
|
||||
if c := in.peekRune(); !isIdent(c) {
|
||||
in.Error(fmt.Sprintf("unexpected input character %#q", c))
|
||||
}
|
||||
|
||||
// Scan over identifier.
|
||||
for isIdent(in.peekRune()) {
|
||||
if in.peekPrefix("//") {
|
||||
break
|
||||
}
|
||||
if in.peekPrefix("/*") {
|
||||
in.Error("mod files must use // comments (not /* */ comments)")
|
||||
}
|
||||
in.readRune()
|
||||
}
|
||||
in.endToken(_IDENT)
|
||||
}
|
||||
|
||||
// isIdent reports whether c is an identifier rune.
|
||||
// We treat most printable runes as identifier runes, except for a handful of
|
||||
// ASCII punctuation characters.
|
||||
func isIdent(c int) bool {
|
||||
switch r := rune(c); r {
|
||||
case ' ', '(', ')', '[', ']', '{', '}', ',':
|
||||
return false
|
||||
default:
|
||||
return !unicode.IsSpace(r) && unicode.IsPrint(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Comment assignment.
|
||||
// We build two lists of all subexpressions, preorder and postorder.
|
||||
// The preorder list is ordered by start location, with outer expressions first.
|
||||
// The postorder list is ordered by end location, with outer expressions last.
|
||||
// We use the preorder list to assign each whole-line comment to the syntax
|
||||
// immediately following it, and we use the postorder list to assign each
|
||||
// end-of-line comment to the syntax immediately preceding it.
|
||||
|
||||
// order walks the expression adding it and its subexpressions to the
|
||||
// preorder and postorder lists.
|
||||
func (in *input) order(x Expr) {
|
||||
if x != nil {
|
||||
in.pre = append(in.pre, x)
|
||||
}
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
panic(fmt.Errorf("order: unexpected type %T", x))
|
||||
case nil:
|
||||
// nothing
|
||||
case *LParen, *RParen:
|
||||
// nothing
|
||||
case *CommentBlock:
|
||||
// nothing
|
||||
case *Line:
|
||||
// nothing
|
||||
case *FileSyntax:
|
||||
for _, stmt := range x.Stmt {
|
||||
in.order(stmt)
|
||||
}
|
||||
case *LineBlock:
|
||||
in.order(&x.LParen)
|
||||
for _, l := range x.Line {
|
||||
in.order(l)
|
||||
}
|
||||
in.order(&x.RParen)
|
||||
}
|
||||
if x != nil {
|
||||
in.post = append(in.post, x)
|
||||
}
|
||||
}
|
||||
|
||||
// assignComments attaches comments to nearby syntax.
|
||||
func (in *input) assignComments() {
|
||||
const debug = false
|
||||
|
||||
// Generate preorder and postorder lists.
|
||||
in.order(in.file)
|
||||
|
||||
// Split into whole-line comments and suffix comments.
|
||||
var line, suffix []Comment
|
||||
for _, com := range in.comments {
|
||||
if com.Suffix {
|
||||
suffix = append(suffix, com)
|
||||
} else {
|
||||
line = append(line, com)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
for _, c := range line {
|
||||
fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign line comments to syntax immediately following.
|
||||
for _, x := range in.pre {
|
||||
start, _ := x.Span()
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte)
|
||||
}
|
||||
xcom := x.Comment()
|
||||
for len(line) > 0 && start.Byte >= line[0].Start.Byte {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte)
|
||||
}
|
||||
xcom.Before = append(xcom.Before, line[0])
|
||||
line = line[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining line comments go at end of file.
|
||||
in.file.After = append(in.file.After, line...)
|
||||
|
||||
if debug {
|
||||
for _, c := range suffix {
|
||||
fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign suffix comments to syntax immediately before.
|
||||
for i := len(in.post) - 1; i >= 0; i-- {
|
||||
x := in.post[i]
|
||||
|
||||
start, end := x.Span()
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
|
||||
}
|
||||
|
||||
// Do not assign suffix comments to end of line block or whole file.
|
||||
// Instead assign them to the last element inside.
|
||||
switch x.(type) {
|
||||
case *FileSyntax:
|
||||
continue
|
||||
}
|
||||
|
||||
// Do not assign suffix comments to something that starts
|
||||
// on an earlier line, so that in
|
||||
//
|
||||
// x ( y
|
||||
// z ) // comment
|
||||
//
|
||||
// we assign the comment to z and not to x ( ... ).
|
||||
if start.Line != end.Line {
|
||||
continue
|
||||
}
|
||||
xcom := x.Comment()
|
||||
for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte)
|
||||
}
|
||||
xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1])
|
||||
suffix = suffix[:len(suffix)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// We assigned suffix comments in reverse.
|
||||
// If multiple suffix comments were appended to the same
|
||||
// expression node, they are now in reverse. Fix that.
|
||||
for _, x := range in.post {
|
||||
reverseComments(x.Comment().Suffix)
|
||||
}
|
||||
|
||||
// Remaining suffix comments go at beginning of file.
|
||||
in.file.Before = append(in.file.Before, suffix...)
|
||||
}
|
||||
|
||||
// reverseComments reverses the []Comment list.
|
||||
func reverseComments(list []Comment) {
|
||||
for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
|
||||
list[i], list[j] = list[j], list[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseFile() {
|
||||
in.file = new(FileSyntax)
|
||||
var cb *CommentBlock
|
||||
for {
|
||||
switch in.peek() {
|
||||
case '\n':
|
||||
in.lex()
|
||||
if cb != nil {
|
||||
in.file.Stmt = append(in.file.Stmt, cb)
|
||||
cb = nil
|
||||
}
|
||||
case _COMMENT:
|
||||
tok := in.lex()
|
||||
if cb == nil {
|
||||
cb = &CommentBlock{Start: tok.pos}
|
||||
}
|
||||
com := cb.Comment()
|
||||
com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text})
|
||||
case _EOF:
|
||||
if cb != nil {
|
||||
in.file.Stmt = append(in.file.Stmt, cb)
|
||||
}
|
||||
return
|
||||
default:
|
||||
in.parseStmt()
|
||||
if cb != nil {
|
||||
in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before
|
||||
cb = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseStmt() {
|
||||
tok := in.lex()
|
||||
start := tok.pos
|
||||
end := tok.endPos
|
||||
tokens := []string{tok.text}
|
||||
for {
|
||||
tok := in.lex()
|
||||
switch {
|
||||
case tok.kind.isEOL():
|
||||
in.file.Stmt = append(in.file.Stmt, &Line{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
End: end,
|
||||
})
|
||||
return
|
||||
|
||||
case tok.kind == '(':
|
||||
if next := in.peek(); next.isEOL() {
|
||||
// Start of block: no more tokens on this line.
|
||||
in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok))
|
||||
return
|
||||
} else if next == ')' {
|
||||
rparen := in.lex()
|
||||
if in.peek().isEOL() {
|
||||
// Empty block.
|
||||
in.lex()
|
||||
in.file.Stmt = append(in.file.Stmt, &LineBlock{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
LParen: LParen{Pos: tok.pos},
|
||||
RParen: RParen{Pos: rparen.pos},
|
||||
})
|
||||
return
|
||||
}
|
||||
// '( )' in the middle of the line, not a block.
|
||||
tokens = append(tokens, tok.text, rparen.text)
|
||||
} else {
|
||||
// '(' in the middle of the line, not a block.
|
||||
tokens = append(tokens, tok.text)
|
||||
}
|
||||
|
||||
default:
|
||||
tokens = append(tokens, tok.text)
|
||||
end = tok.endPos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock {
|
||||
x := &LineBlock{
|
||||
Start: start,
|
||||
Token: token,
|
||||
LParen: LParen{Pos: lparen.pos},
|
||||
}
|
||||
var comments []Comment
|
||||
for {
|
||||
switch in.peek() {
|
||||
case _EOLCOMMENT:
|
||||
// Suffix comment, will be attached later by assignComments.
|
||||
in.lex()
|
||||
case '\n':
|
||||
// Blank line. Add an empty comment to preserve it.
|
||||
in.lex()
|
||||
if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" {
|
||||
comments = append(comments, Comment{})
|
||||
}
|
||||
case _COMMENT:
|
||||
tok := in.lex()
|
||||
comments = append(comments, Comment{Start: tok.pos, Token: tok.text})
|
||||
case _EOF:
|
||||
in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune))
|
||||
case ')':
|
||||
rparen := in.lex()
|
||||
x.RParen.Before = comments
|
||||
x.RParen.Pos = rparen.pos
|
||||
if !in.peek().isEOL() {
|
||||
in.Error("syntax error (expected newline after closing paren)")
|
||||
}
|
||||
in.lex()
|
||||
return x
|
||||
default:
|
||||
l := in.parseLine()
|
||||
x.Line = append(x.Line, l)
|
||||
l.Comment().Before = comments
|
||||
comments = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseLine() *Line {
|
||||
tok := in.lex()
|
||||
if tok.kind.isEOL() {
|
||||
in.Error("internal parse error: parseLine at end of line")
|
||||
}
|
||||
start := tok.pos
|
||||
end := tok.endPos
|
||||
tokens := []string{tok.text}
|
||||
for {
|
||||
tok := in.lex()
|
||||
if tok.kind.isEOL() {
|
||||
return &Line{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
End: end,
|
||||
InBlock: true,
|
||||
}
|
||||
}
|
||||
tokens = append(tokens, tok.text)
|
||||
end = tok.endPos
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
slashSlash = []byte("//")
|
||||
moduleStr = []byte("module")
|
||||
)
|
||||
|
||||
// ModulePath returns the module path from the gomod file text.
|
||||
// If it cannot find a module path, it returns an empty string.
|
||||
// It is tolerant of unrelated problems in the go.mod file.
|
||||
func ModulePath(mod []byte) string {
|
||||
for len(mod) > 0 {
|
||||
line := mod
|
||||
mod = nil
|
||||
if i := bytes.IndexByte(line, '\n'); i >= 0 {
|
||||
line, mod = line[:i], line[i+1:]
|
||||
}
|
||||
if i := bytes.Index(line, slashSlash); i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if !bytes.HasPrefix(line, moduleStr) {
|
||||
continue
|
||||
}
|
||||
line = line[len(moduleStr):]
|
||||
n := len(line)
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == n || len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == '"' || line[0] == '`' {
|
||||
p, err := strconv.Unquote(string(line))
|
||||
if err != nil {
|
||||
return "" // malformed quoted string or multiline module path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
return string(line)
|
||||
}
|
||||
return "" // missing module path
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// exists reports whether the named file exists.
|
||||
func exists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Test that reading and then writing the golden files
|
||||
// does not change their output.
|
||||
func TestPrintGolden(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/*.golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := strings.TrimSuffix(filepath.Base(out), ".golden")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testPrint(t, out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testPrint is a helper for testing the printer.
|
||||
// It reads the file named in, reformats it, and compares
|
||||
// the result to the file named out.
|
||||
func testPrint(t *testing.T, in, out string) {
|
||||
data, err := os.ReadFile(in)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
golden, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
base := "testdata/" + filepath.Base(in)
|
||||
f, err := parse(in, data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePunctuation verifies that certain ASCII punctuation characters
|
||||
// (brackets, commas) are lexed as separate tokens, even when they're
|
||||
// surrounded by identifier characters.
|
||||
func TestParsePunctuation(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc, src, want string
|
||||
}{
|
||||
{"paren", "require ()", "require ( )"},
|
||||
{"brackets", "require []{},", "require [ ] { } ,"},
|
||||
{"mix", "require a[b]c{d}e,", "require a [ b ] c { d } e ,"},
|
||||
{"block_mix", "require (\n\ta[b]\n)", "require ( a [ b ] )"},
|
||||
{"interval", "require [v1.0.0, v1.1.0)", "require [ v1.0.0 , v1.1.0 )"},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
f, err := parse("go.mod", []byte(test.src))
|
||||
if err != nil {
|
||||
t.Fatalf("parsing %q: %v", test.src, err)
|
||||
}
|
||||
var tokens []string
|
||||
for _, stmt := range f.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
tokens = append(tokens, stmt.Token...)
|
||||
case *LineBlock:
|
||||
tokens = append(tokens, stmt.Token...)
|
||||
tokens = append(tokens, "(")
|
||||
for _, line := range stmt.Line {
|
||||
tokens = append(tokens, line.Token...)
|
||||
}
|
||||
tokens = append(tokens, ")")
|
||||
default:
|
||||
t.Fatalf("parsing %q: unexpected statement of type %T", test.src, stmt)
|
||||
}
|
||||
}
|
||||
got := strings.Join(tokens, " ")
|
||||
if got != test.want {
|
||||
t.Errorf("parsing %q: got %q, want %q", test.src, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLax(t *testing.T) {
|
||||
badFile := []byte(`module m
|
||||
surprise attack
|
||||
x y (
|
||||
z
|
||||
)
|
||||
exclude v1.2.3
|
||||
replace <-!!!
|
||||
retract v1.2.3 v1.2.4
|
||||
retract (v1.2.3, v1.2.4]
|
||||
retract v1.2.3 (
|
||||
key1 value1
|
||||
key2 value2
|
||||
)
|
||||
require good v1.0.0
|
||||
`)
|
||||
f, err := ParseLax("file", badFile, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLax did not ignore irrelevant errors: %v", err)
|
||||
}
|
||||
if f.Module == nil || f.Module.Mod.Path != "m" {
|
||||
t.Errorf("module directive was not parsed")
|
||||
}
|
||||
if len(f.Require) != 1 || f.Require[0].Mod.Path != "good" {
|
||||
t.Errorf("require directive at end of file was not parsed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when files in the testdata directory are parsed
|
||||
// and printed and parsed again, we get the same parse tree
|
||||
// both times.
|
||||
func TestPrintParse(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := filepath.Base(out)
|
||||
if !strings.HasSuffix(out, ".in") && !strings.HasSuffix(out, ".golden") {
|
||||
continue
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := "testdata/" + filepath.Base(out)
|
||||
f, err := parse(base, data)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing original: %v", err)
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
f2, err := parse(base, ndata)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing reformatted: %v", err)
|
||||
}
|
||||
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(f, f2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/parse): %v", err)
|
||||
}
|
||||
|
||||
pf1, err := Parse(base, data, nil)
|
||||
if err != nil {
|
||||
switch base {
|
||||
case "testdata/block.golden",
|
||||
"testdata/block.in",
|
||||
"testdata/comment.golden",
|
||||
"testdata/comment.in",
|
||||
"testdata/rule1.golden":
|
||||
// ignore
|
||||
default:
|
||||
t.Errorf("should parse %v: %v", base, err)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
pf2, err := Parse(base, ndata, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted: %v", err)
|
||||
}
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/Parse): %v", err)
|
||||
}
|
||||
|
||||
ndata2, err := pf1.Format()
|
||||
if err != nil {
|
||||
t.Errorf("reformat: %v", err)
|
||||
}
|
||||
pf3, err := Parse(base, ndata2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted2: %v", err)
|
||||
}
|
||||
eq = eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf3); err != nil {
|
||||
t.Errorf("not equal (Parse/Format/Parse): %v", err)
|
||||
}
|
||||
ndata = ndata2
|
||||
}
|
||||
|
||||
if strings.HasSuffix(out, ".in") {
|
||||
golden, err := os.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An eqchecker holds state for checking the equality of two parse trees.
|
||||
type eqchecker struct {
|
||||
file string
|
||||
pos Position
|
||||
}
|
||||
|
||||
// errorf returns an error described by the printf-style format and arguments,
|
||||
// inserting the current file position before the error text.
|
||||
func (eq *eqchecker) errorf(format string, args ...interface{}) error {
|
||||
return fmt.Errorf("%s:%d: %s", eq.file, eq.pos.Line,
|
||||
fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// check checks that v and w represent the same parse tree.
|
||||
// If not, it returns an error describing the first difference.
|
||||
func (eq *eqchecker) check(v, w interface{}) error {
|
||||
return eq.checkValue(reflect.ValueOf(v), reflect.ValueOf(w))
|
||||
}
|
||||
|
||||
var (
|
||||
posType = reflect.TypeOf(Position{})
|
||||
commentsType = reflect.TypeOf(Comments{})
|
||||
)
|
||||
|
||||
// checkValue checks that v and w represent the same parse tree.
|
||||
// If not, it returns an error describing the first difference.
|
||||
func (eq *eqchecker) checkValue(v, w reflect.Value) error {
|
||||
// inner returns the innermost expression for v.
|
||||
// if v is a non-nil interface value, it returns the concrete
|
||||
// value in the interface.
|
||||
inner := func(v reflect.Value) reflect.Value {
|
||||
for {
|
||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
v = inner(v)
|
||||
w = inner(w)
|
||||
if v.Kind() == reflect.Invalid && w.Kind() == reflect.Invalid {
|
||||
return nil
|
||||
}
|
||||
if v.Kind() == reflect.Invalid {
|
||||
return eq.errorf("nil interface became %s", w.Type())
|
||||
}
|
||||
if w.Kind() == reflect.Invalid {
|
||||
return eq.errorf("%s became nil interface", v.Type())
|
||||
}
|
||||
|
||||
if v.Type() != w.Type() {
|
||||
return eq.errorf("%s became %s", v.Type(), w.Type())
|
||||
}
|
||||
|
||||
if p, ok := v.Interface().(Expr); ok {
|
||||
eq.pos, _ = p.Span()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
default:
|
||||
return eq.errorf("unexpected type %s", v.Type())
|
||||
|
||||
case reflect.Bool, reflect.Int, reflect.String:
|
||||
vi := v.Interface()
|
||||
wi := w.Interface()
|
||||
if vi != wi {
|
||||
return eq.errorf("%v became %v", vi, wi)
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
vl := v.Len()
|
||||
wl := w.Len()
|
||||
for i := 0; i < vl || i < wl; i++ {
|
||||
if i >= vl {
|
||||
return eq.errorf("unexpected %s", w.Index(i).Type())
|
||||
}
|
||||
if i >= wl {
|
||||
return eq.errorf("missing %s", v.Index(i).Type())
|
||||
}
|
||||
if err := eq.checkValue(v.Index(i), w.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
// Fields in struct must match.
|
||||
t := v.Type()
|
||||
n := t.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
tf := t.Field(i)
|
||||
switch {
|
||||
default:
|
||||
if err := eq.checkValue(v.Field(i), w.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tf.Type == posType: // ignore positions
|
||||
case tf.Type == commentsType: // ignore comment assignment
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
if v.IsNil() != w.IsNil() {
|
||||
if v.IsNil() {
|
||||
return eq.errorf("unexpected %s", w.Elem().Type())
|
||||
}
|
||||
return eq.errorf("missing %s", v.Elem().Type())
|
||||
}
|
||||
if err := eq.checkValue(v.Elem(), w.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// diff returns the output of running diff on b1 and b2.
|
||||
func diff(b1, b2 []byte) (data []byte, err error) {
|
||||
f1, err := os.CreateTemp("", "testdiff")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f1.Name())
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.CreateTemp("", "testdiff")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f2.Name())
|
||||
defer f2.Close()
|
||||
|
||||
f1.Write(b1)
|
||||
f2.Write(b2)
|
||||
|
||||
data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
|
||||
if len(data) > 0 {
|
||||
// diff exits with a non-zero status when the files don't match.
|
||||
// Ignore that failure as long as we get output.
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// tdiff logs the diff output to t.Error.
|
||||
func tdiff(t *testing.T, a, b string) {
|
||||
data, err := diff([]byte(a), []byte(b))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Error(string(data))
|
||||
}
|
||||
|
||||
var modulePathTests = []struct {
|
||||
input []byte
|
||||
expected string
|
||||
}{
|
||||
{input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module `github.com/rsc/vgotest`"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module \"github.com/rsc/vgotest/v2\""), expected: "github.com/rsc/vgotest/v2"},
|
||||
{input: []byte("module github.com/rsc/vgotest/v2"), expected: "github.com/rsc/vgotest/v2"},
|
||||
{input: []byte("module \"gopkg.in/yaml.v2\""), expected: "gopkg.in/yaml.v2"},
|
||||
{input: []byte("module gopkg.in/yaml.v2"), expected: "gopkg.in/yaml.v2"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\n\""), expected: ""},
|
||||
{input: []byte("module gopkg.in/check.v1\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\r\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module gopkg.in/check.v1\r\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\n\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module gopkg.in/check.v1\n\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
|
||||
{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
|
||||
{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
|
||||
{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
|
||||
{input: []byte("module \nmodule a/b/c "), expected: "a/b/c"},
|
||||
{input: []byte("module \" \""), expected: " "},
|
||||
{input: []byte("module "), expected: ""},
|
||||
{input: []byte("module \" a/b/c \""), expected: " a/b/c "},
|
||||
{input: []byte("module \"github.com/rsc/vgotest1\" // with a comment"), expected: "github.com/rsc/vgotest1"},
|
||||
}
|
||||
|
||||
func TestModulePath(t *testing.T) {
|
||||
for _, test := range modulePathTests {
|
||||
t.Run(string(test.input), func(t *testing.T) {
|
||||
result := ModulePath(test.input)
|
||||
if result != test.expected {
|
||||
t.Fatalf("ModulePath(%q): %s, want %s", string(test.input), result, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc, input string
|
||||
ok bool
|
||||
laxOK bool // ok=true implies laxOK=true; only set if ok=false
|
||||
}{
|
||||
// go lines
|
||||
{desc: "empty", input: "module m\ngo \n", ok: false},
|
||||
{desc: "one", input: "module m\ngo 1\n", ok: false},
|
||||
{desc: "two", input: "module m\ngo 1.22\n", ok: true},
|
||||
{desc: "three", input: "module m\ngo 1.22.333", ok: true},
|
||||
{desc: "before", input: "module m\ngo v1.2\n", ok: false},
|
||||
{desc: "after", input: "module m\ngo 1.2rc1\n", ok: true},
|
||||
{desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false},
|
||||
{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: true},
|
||||
{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: true},
|
||||
{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: true},
|
||||
{desc: "alt4", input: "module m\ngo 1.2.beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt1", input: "module m\ngo v1.2.3\n", ok: false, laxOK: true},
|
||||
{desc: "alt2", input: "module m\ngo v1.2rc1\n", ok: false, laxOK: true},
|
||||
{desc: "alt3", input: "module m\ngo v1.2beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt4", input: "module m\ngo v1.2.beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt1", input: "module m\ngo v1.2\n", ok: false, laxOK: true},
|
||||
|
||||
// toolchain lines
|
||||
{desc: "tool", input: "module m\ntoolchain go1.2\n", ok: true},
|
||||
{desc: "tool1", input: "module m\ntoolchain go1.2.3\n", ok: true},
|
||||
{desc: "tool2", input: "module m\ntoolchain go1.2rc1\n", ok: true},
|
||||
{desc: "tool3", input: "module m\ntoolchain go1.2rc1-gccgo\n", ok: true},
|
||||
{desc: "tool4", input: "module m\ntoolchain default\n", ok: true},
|
||||
{desc: "tool5", input: "module m\ntoolchain inconceivable!\n", ok: false, laxOK: true},
|
||||
}
|
||||
t.Run("Strict", func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if _, err := Parse("go.mod", []byte(test.input), nil); err == nil && !test.ok {
|
||||
t.Error("unexpected success")
|
||||
} else if err != nil && test.ok {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("Lax", func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if _, err := ParseLax("go.mod", []byte(test.input), nil); err == nil && !(test.ok || test.laxOK) {
|
||||
t.Error("unexpected success")
|
||||
} else if err != nil && test.ok {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestComments(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc, input, want string
|
||||
}{
|
||||
{
|
||||
desc: "comment_only",
|
||||
input: `
|
||||
// a
|
||||
// b
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
comments before "// b"
|
||||
`,
|
||||
}, {
|
||||
desc: "line",
|
||||
input: `
|
||||
// a
|
||||
|
||||
// b
|
||||
module m // c
|
||||
// d
|
||||
|
||||
// e
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
line before "// b"
|
||||
line suffix "// c"
|
||||
comments before "// d"
|
||||
comments before "// e"
|
||||
`,
|
||||
}, {
|
||||
desc: "block",
|
||||
input: `
|
||||
// a
|
||||
|
||||
// b
|
||||
block ( // c
|
||||
// d
|
||||
|
||||
// e
|
||||
x // f
|
||||
// g
|
||||
|
||||
// h
|
||||
) // i
|
||||
// j
|
||||
|
||||
// k
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
block before "// b"
|
||||
lparen suffix "// c"
|
||||
blockline before "// d"
|
||||
blockline before ""
|
||||
blockline before "// e"
|
||||
blockline suffix "// f"
|
||||
rparen before "// g"
|
||||
rparen before ""
|
||||
rparen before "// h"
|
||||
rparen suffix "// i"
|
||||
comments before "// j"
|
||||
comments before "// k"
|
||||
`,
|
||||
}, {
|
||||
desc: "cr_removed",
|
||||
input: "// a\r\r\n",
|
||||
want: `comments before "// a\r"`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
f, err := ParseLax("go.mod", []byte(test.input), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
printComments := func(prefix string, cs *Comments) {
|
||||
for _, c := range cs.Before {
|
||||
fmt.Fprintf(buf, "%s before %q\n", prefix, c.Token)
|
||||
}
|
||||
for _, c := range cs.Suffix {
|
||||
fmt.Fprintf(buf, "%s suffix %q\n", prefix, c.Token)
|
||||
}
|
||||
for _, c := range cs.After {
|
||||
fmt.Fprintf(buf, "%s after %q\n", prefix, c.Token)
|
||||
}
|
||||
}
|
||||
|
||||
printComments("file", &f.Syntax.Comments)
|
||||
for _, stmt := range f.Syntax.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *CommentBlock:
|
||||
printComments("comments", stmt.Comment())
|
||||
case *Line:
|
||||
printComments("line", stmt.Comment())
|
||||
case *LineBlock:
|
||||
printComments("block", stmt.Comment())
|
||||
printComments("lparen", stmt.LParen.Comment())
|
||||
for _, line := range stmt.Line {
|
||||
printComments("blockline", line.Comment())
|
||||
}
|
||||
printComments("rparen", stmt.RParen.Comment())
|
||||
}
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(buf.String())
|
||||
want := strings.TrimSpace(test.want)
|
||||
if got != want {
|
||||
t.Errorf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
want string
|
||||
input []Expr
|
||||
}{
|
||||
{
|
||||
desc: "simple_lines",
|
||||
want: `line: module a
|
||||
line: require b v1.0.0
|
||||
`,
|
||||
input: []Expr{
|
||||
&Line{
|
||||
Token: []string{"module", "a"},
|
||||
},
|
||||
&Line{
|
||||
Token: []string{"require", "b", "v1.0.0"},
|
||||
},
|
||||
&Line{
|
||||
Token: nil,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
desc: "line_block",
|
||||
want: `line: module a
|
||||
block: require
|
||||
blockline: b v1.0.0
|
||||
blockline: c v1.0.0
|
||||
`,
|
||||
input: []Expr{
|
||||
&Line{
|
||||
Token: []string{"module", "a"},
|
||||
},
|
||||
&LineBlock{
|
||||
Token: []string{"require"},
|
||||
Line: []*Line{
|
||||
{
|
||||
Token: []string{"b", "v1.0.0"},
|
||||
InBlock: true,
|
||||
},
|
||||
{
|
||||
Token: nil,
|
||||
InBlock: true,
|
||||
},
|
||||
{
|
||||
Token: []string{"c", "v1.0.0"},
|
||||
InBlock: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
desc: "collapse",
|
||||
want: `line: module a
|
||||
line: require b v1.0.0
|
||||
`,
|
||||
input: []Expr{
|
||||
&Line{
|
||||
Token: []string{"module", "a"},
|
||||
},
|
||||
&LineBlock{
|
||||
Token: []string{"require"},
|
||||
Line: []*Line{
|
||||
{
|
||||
Token: []string{"b", "v1.0.0"},
|
||||
InBlock: true,
|
||||
},
|
||||
{
|
||||
Token: nil,
|
||||
InBlock: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
syntax := &FileSyntax{
|
||||
Stmt: test.input,
|
||||
}
|
||||
syntax.Cleanup()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for _, stmt := range syntax.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
fmt.Fprintf(buf, "line: %v\n", strings.Join(stmt.Token, " "))
|
||||
case *LineBlock:
|
||||
fmt.Fprintf(buf, "block: %v\n", strings.Join(stmt.Token, " "))
|
||||
for _, line := range stmt.Line {
|
||||
fmt.Fprintf(buf, "blockline: %v\n", strings.Join(line.Token, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(buf.String())
|
||||
want := strings.TrimSpace(test.want)
|
||||
if got != want {
|
||||
t.Errorf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 45130: File.Cleanup breaks references so future edits do nothing
|
||||
func TestCleanupMaintainsRefs(t *testing.T) {
|
||||
lineB := &Line{
|
||||
Token: []string{"b", "v1.0.0"},
|
||||
InBlock: true,
|
||||
}
|
||||
syntax := &FileSyntax{
|
||||
Stmt: []Expr{
|
||||
&LineBlock{
|
||||
Token: []string{"require"},
|
||||
Line: []*Line{
|
||||
lineB,
|
||||
{
|
||||
Token: nil,
|
||||
InBlock: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
syntax.Cleanup()
|
||||
|
||||
if syntax.Stmt[0] != lineB {
|
||||
t.Errorf("got:\n%v\nwant:\n%v", syntax.Stmt[0], lineB)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// comment
|
||||
x "y" z
|
||||
|
||||
// block
|
||||
block ( // block-eol
|
||||
// x-before-line
|
||||
|
||||
"x" (y // x-eol
|
||||
"x") y // y-eol
|
||||
"x1"
|
||||
"x2"
|
||||
// line
|
||||
"x3"
|
||||
"x4"
|
||||
|
||||
"x5"
|
||||
|
||||
// y-line
|
||||
"y" // y-eol
|
||||
|
||||
"z" // z-eol
|
||||
) // block-eol2
|
||||
|
||||
block1 (
|
||||
)
|
||||
|
||||
block2 (x y z)
|
||||
|
||||
block3 "w" (
|
||||
) // empty block
|
||||
|
||||
block4 "x" () "y" // not a block
|
||||
|
||||
block5 ("z" // also not a block
|
||||
|
||||
// eof
|
||||
@@ -0,0 +1,33 @@
|
||||
// comment
|
||||
x "y" z
|
||||
|
||||
// block
|
||||
block ( // block-eol
|
||||
// x-before-line
|
||||
|
||||
"x" ( y // x-eol
|
||||
"x" ) y // y-eol
|
||||
"x1"
|
||||
"x2"
|
||||
// line
|
||||
"x3"
|
||||
"x4"
|
||||
|
||||
"x5"
|
||||
|
||||
// y-line
|
||||
"y" // y-eol
|
||||
|
||||
"z" // z-eol
|
||||
) // block-eol2
|
||||
|
||||
|
||||
block1()
|
||||
|
||||
block2 (x y z)
|
||||
|
||||
block3 "w" ( ) // empty block
|
||||
block4 "x" ( ) "y" // not a block
|
||||
block5 ( "z" // also not a block
|
||||
|
||||
// eof
|
||||
@@ -0,0 +1,10 @@
|
||||
// comment
|
||||
module "x" // eol
|
||||
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
module "y" // eoy
|
||||
|
||||
// comment 3
|
||||
@@ -0,0 +1,8 @@
|
||||
// comment
|
||||
module "x" // eol
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
module "y" // eoy
|
||||
// comment 3
|
||||
@@ -0,0 +1,3 @@
|
||||
go 1.2.3
|
||||
|
||||
toolchain default
|
||||
@@ -0,0 +1,2 @@
|
||||
go 1.2.3
|
||||
toolchain default
|
||||
@@ -0,0 +1,6 @@
|
||||
module x
|
||||
|
||||
require (
|
||||
gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
module abc
|
||||
@@ -0,0 +1 @@
|
||||
module "abc"
|
||||
@@ -0,0 +1,12 @@
|
||||
module abc
|
||||
|
||||
replace xyz v1.2.3 => /tmp/z
|
||||
|
||||
replace xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
|
||||
replace (
|
||||
w v1.0.0 => "./a,"
|
||||
w v1.0.1 => "./a()"
|
||||
w v1.0.2 => "./a[]"
|
||||
w v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
module "abc"
|
||||
|
||||
replace "xyz" v1.2.3 => "/tmp/z"
|
||||
|
||||
replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
|
||||
|
||||
replace (
|
||||
"w" v1.0.0 => "./a,"
|
||||
"w" v1.0.1 => "./a()"
|
||||
"w" v1.0.2 => "./a[]"
|
||||
"w" v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
module abc
|
||||
|
||||
replace (
|
||||
xyz v1.2.3 => /tmp/z
|
||||
xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
xyz v1.4.5 => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
module "abc"
|
||||
|
||||
replace (
|
||||
"xyz" v1.2.3 => "/tmp/z"
|
||||
"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
|
||||
xyz "v1.4.5" => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
module abc
|
||||
|
||||
retract v1.2.3
|
||||
|
||||
retract [v1.2.3, v1.2.4]
|
||||
|
||||
retract (
|
||||
v1.2.3
|
||||
|
||||
[v1.2.3, v1.2.4]
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
module abc
|
||||
|
||||
retract "v1.2.3"
|
||||
|
||||
retract [ "v1.2.3" , "v1.2.4" ]
|
||||
|
||||
retract (
|
||||
"v1.2.3"
|
||||
|
||||
[ "v1.2.3" , "v1.2.4" ]
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
module "x"
|
||||
|
||||
module "y"
|
||||
|
||||
require "x"
|
||||
|
||||
require x
|
||||
@@ -0,0 +1,10 @@
|
||||
// comment
|
||||
use x // eol
|
||||
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
use y // eoy
|
||||
|
||||
// comment 3
|
||||
@@ -0,0 +1,8 @@
|
||||
// comment
|
||||
use "x" // eol
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
use "y" // eoy
|
||||
// comment 3
|
||||
@@ -0,0 +1,3 @@
|
||||
go 1.2.3
|
||||
|
||||
toolchain default
|
||||
@@ -0,0 +1,2 @@
|
||||
go 1.2.3
|
||||
toolchain default
|
||||
@@ -0,0 +1,12 @@
|
||||
use abc
|
||||
|
||||
replace xyz v1.2.3 => /tmp/z
|
||||
|
||||
replace xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
|
||||
replace (
|
||||
w v1.0.0 => "./a,"
|
||||
w v1.0.1 => "./a()"
|
||||
w v1.0.2 => "./a[]"
|
||||
w v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
use "abc"
|
||||
|
||||
replace "xyz" v1.2.3 => "/tmp/z"
|
||||
|
||||
replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
|
||||
|
||||
replace (
|
||||
"w" v1.0.0 => "./a,"
|
||||
"w" v1.0.1 => "./a()"
|
||||
"w" v1.0.2 => "./a[]"
|
||||
"w" v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
use abc
|
||||
|
||||
replace (
|
||||
xyz v1.2.3 => /tmp/z
|
||||
xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
xyz v1.4.5 => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
use "abc"
|
||||
|
||||
replace (
|
||||
"xyz" v1.2.3 => "/tmp/z"
|
||||
"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
|
||||
xyz "v1.4.5" => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
use ../foo
|
||||
|
||||
use (
|
||||
/bar
|
||||
|
||||
baz
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
use "../foo"
|
||||
|
||||
use (
|
||||
"/bar"
|
||||
|
||||
"baz"
|
||||
)
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A WorkFile is the parsed, interpreted form of a go.work file.
|
||||
type WorkFile struct {
|
||||
Go *Go
|
||||
Toolchain *Toolchain
|
||||
Godebug []*Godebug
|
||||
Use []*Use
|
||||
Replace []*Replace
|
||||
|
||||
Syntax *FileSyntax
|
||||
}
|
||||
|
||||
// A Use is a single directory statement.
|
||||
type Use struct {
|
||||
Path string // Use path of module.
|
||||
ModulePath string // Module path in the comment.
|
||||
Syntax *Line
|
||||
}
|
||||
|
||||
// ParseWork parses and returns a go.work file.
|
||||
//
|
||||
// file is the name of the file, used in positions and errors.
|
||||
//
|
||||
// data is the content of the file.
|
||||
//
|
||||
// fix is an optional function that canonicalizes module versions.
|
||||
// If fix is nil, all module versions must be canonical ([module.CanonicalVersion]
|
||||
// must return the same string).
|
||||
func ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) {
|
||||
fs, err := parse(file, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := &WorkFile{
|
||||
Syntax: fs,
|
||||
}
|
||||
var errs ErrorList
|
||||
|
||||
for _, x := range fs.Stmt {
|
||||
switch x := x.(type) {
|
||||
case *Line:
|
||||
f.add(&errs, x, x.Token[0], x.Token[1:], fix)
|
||||
|
||||
case *LineBlock:
|
||||
if len(x.Token) > 1 {
|
||||
errs = append(errs, Error{
|
||||
Filename: file,
|
||||
Pos: x.Start,
|
||||
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch x.Token[0] {
|
||||
default:
|
||||
errs = append(errs, Error{
|
||||
Filename: file,
|
||||
Pos: x.Start,
|
||||
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
|
||||
})
|
||||
continue
|
||||
case "godebug", "use", "replace":
|
||||
for _, l := range x.Line {
|
||||
f.add(&errs, l, x.Token[0], l.Token, fix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the file f after any edit operations.
|
||||
// To avoid quadratic behavior, modifications like [WorkFile.DropRequire]
|
||||
// clear the entry but do not remove it from the slice.
|
||||
// Cleanup cleans out all the cleared entries.
|
||||
func (f *WorkFile) Cleanup() {
|
||||
w := 0
|
||||
for _, r := range f.Use {
|
||||
if r.Path != "" {
|
||||
f.Use[w] = r
|
||||
w++
|
||||
}
|
||||
}
|
||||
f.Use = f.Use[:w]
|
||||
|
||||
w = 0
|
||||
for _, r := range f.Replace {
|
||||
if r.Old.Path != "" {
|
||||
f.Replace[w] = r
|
||||
w++
|
||||
}
|
||||
}
|
||||
f.Replace = f.Replace[:w]
|
||||
|
||||
f.Syntax.Cleanup()
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddGoStmt(version string) error {
|
||||
if !GoVersionRE.MatchString(version) {
|
||||
return fmt.Errorf("invalid language version %q", version)
|
||||
}
|
||||
if f.Go == nil {
|
||||
stmt := &Line{Token: []string{"go", version}}
|
||||
f.Go = &Go{
|
||||
Version: version,
|
||||
Syntax: stmt,
|
||||
}
|
||||
// Find the first non-comment-only block and add
|
||||
// the go statement before it. That will keep file comments at the top.
|
||||
i := 0
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
||||
} else {
|
||||
f.Go.Version = version
|
||||
f.Syntax.updateLine(f.Go.Syntax, "go", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddToolchainStmt(name string) error {
|
||||
if !ToolchainRE.MatchString(name) {
|
||||
return fmt.Errorf("invalid toolchain name %q", name)
|
||||
}
|
||||
if f.Toolchain == nil {
|
||||
stmt := &Line{Token: []string{"toolchain", name}}
|
||||
f.Toolchain = &Toolchain{
|
||||
Name: name,
|
||||
Syntax: stmt,
|
||||
}
|
||||
// Find the go line and add the toolchain line after it.
|
||||
// Or else find the first non-comment-only block and add
|
||||
// the toolchain line before it. That will keep file comments at the top.
|
||||
i := 0
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if line, ok := f.Syntax.Stmt[i].(*Line); ok && len(line.Token) > 0 && line.Token[0] == "go" {
|
||||
i++
|
||||
goto Found
|
||||
}
|
||||
}
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
Found:
|
||||
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
||||
} else {
|
||||
f.Toolchain.Name = name
|
||||
f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropGoStmt deletes the go statement from the file.
|
||||
func (f *WorkFile) DropGoStmt() {
|
||||
if f.Go != nil {
|
||||
f.Go.Syntax.markRemoved()
|
||||
f.Go = nil
|
||||
}
|
||||
}
|
||||
|
||||
// DropToolchainStmt deletes the toolchain statement from the file.
|
||||
func (f *WorkFile) DropToolchainStmt() {
|
||||
if f.Toolchain != nil {
|
||||
f.Toolchain.Syntax.markRemoved()
|
||||
f.Toolchain = nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddGodebug sets the first godebug line for key to value,
|
||||
// preserving any existing comments for that line and removing all
|
||||
// other godebug lines for key.
|
||||
//
|
||||
// If no line currently exists for key, AddGodebug adds a new line
|
||||
// at the end of the last godebug block.
|
||||
func (f *WorkFile) AddGodebug(key, value string) error {
|
||||
need := true
|
||||
for _, g := range f.Godebug {
|
||||
if g.Key == key {
|
||||
if need {
|
||||
g.Value = value
|
||||
f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value)
|
||||
need = false
|
||||
} else {
|
||||
g.Syntax.markRemoved()
|
||||
*g = Godebug{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if need {
|
||||
f.addNewGodebug(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNewGodebug adds a new godebug key=value line at the end
|
||||
// of the last godebug block, regardless of any existing godebug lines for key.
|
||||
func (f *WorkFile) addNewGodebug(key, value string) {
|
||||
line := f.Syntax.addLine(nil, "godebug", key+"="+value)
|
||||
g := &Godebug{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Syntax: line,
|
||||
}
|
||||
f.Godebug = append(f.Godebug, g)
|
||||
}
|
||||
|
||||
func (f *WorkFile) DropGodebug(key string) error {
|
||||
for _, g := range f.Godebug {
|
||||
if g.Key == key {
|
||||
g.Syntax.markRemoved()
|
||||
*g = Godebug{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddUse(diskPath, modulePath string) error {
|
||||
need := true
|
||||
for _, d := range f.Use {
|
||||
if d.Path == diskPath {
|
||||
if need {
|
||||
d.ModulePath = modulePath
|
||||
f.Syntax.updateLine(d.Syntax, "use", AutoQuote(diskPath))
|
||||
need = false
|
||||
} else {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if need {
|
||||
f.AddNewUse(diskPath, modulePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddNewUse(diskPath, modulePath string) {
|
||||
line := f.Syntax.addLine(nil, "use", AutoQuote(diskPath))
|
||||
f.Use = append(f.Use, &Use{Path: diskPath, ModulePath: modulePath, Syntax: line})
|
||||
}
|
||||
|
||||
func (f *WorkFile) SetUse(dirs []*Use) {
|
||||
need := make(map[string]string)
|
||||
for _, d := range dirs {
|
||||
need[d.Path] = d.ModulePath
|
||||
}
|
||||
|
||||
for _, d := range f.Use {
|
||||
if modulePath, ok := need[d.Path]; ok {
|
||||
d.ModulePath = modulePath
|
||||
} else {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#45713): Add module path to comment.
|
||||
|
||||
for diskPath, modulePath := range need {
|
||||
f.AddNewUse(diskPath, modulePath)
|
||||
}
|
||||
f.SortBlocks()
|
||||
}
|
||||
|
||||
func (f *WorkFile) DropUse(path string) error {
|
||||
for _, d := range f.Use {
|
||||
if d.Path == path {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddReplace(oldPath, oldVers, newPath, newVers string) error {
|
||||
return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
|
||||
}
|
||||
|
||||
func (f *WorkFile) DropReplace(oldPath, oldVers string) error {
|
||||
for _, r := range f.Replace {
|
||||
if r.Old.Path == oldPath && r.Old.Version == oldVers {
|
||||
r.Syntax.markRemoved()
|
||||
*r = Replace{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) SortBlocks() {
|
||||
f.removeDups() // otherwise sorting is unsafe
|
||||
|
||||
for _, stmt := range f.Syntax.Stmt {
|
||||
block, ok := stmt.(*LineBlock)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sort.SliceStable(block.Line, func(i, j int) bool {
|
||||
return lineLess(block.Line[i], block.Line[j])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// removeDups removes duplicate replace directives.
|
||||
//
|
||||
// Later replace directives take priority.
|
||||
//
|
||||
// require directives are not de-duplicated. That's left up to higher-level
|
||||
// logic (MVS).
|
||||
//
|
||||
// retract directives are not de-duplicated since comments are
|
||||
// meaningful, and versions may be retracted multiple times.
|
||||
func (f *WorkFile) removeDups() {
|
||||
removeDups(f.Syntax, nil, &f.Replace, nil)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TODO(#45713): Update these tests once AddUse sets the module path.
|
||||
var workAddUseTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
path string
|
||||
modulePath string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`foo`, `bar`,
|
||||
`use foo`,
|
||||
},
|
||||
{
|
||||
`go_stmt_only`,
|
||||
`go 1.17
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_line_present`,
|
||||
`go 1.17
|
||||
use baz`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
foo
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_block_present`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
quux
|
||||
)
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
quux
|
||||
foo
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_and_replace_present`,
|
||||
`go 1.17
|
||||
use baz
|
||||
replace a => ./b
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
foo
|
||||
)
|
||||
replace a => ./b
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workDropUseTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
path string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`foo`,
|
||||
``,
|
||||
},
|
||||
{
|
||||
`go_stmt_only`,
|
||||
`go 1.17
|
||||
`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`single_use`,
|
||||
`go 1.17
|
||||
use foo`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_block`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
)`,
|
||||
`bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
baz
|
||||
)`,
|
||||
},
|
||||
{
|
||||
`use_multi`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
)
|
||||
use foo
|
||||
use quux
|
||||
use foo`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
use (
|
||||
bar
|
||||
baz
|
||||
)
|
||||
use quux`,
|
||||
},
|
||||
}
|
||||
|
||||
var workAddGoTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
version string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`1.17`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`comment`,
|
||||
`// this is a comment`,
|
||||
`1.17`,
|
||||
`// this is a comment
|
||||
|
||||
go 1.17`,
|
||||
},
|
||||
{
|
||||
`use_after_replace`,
|
||||
`
|
||||
replace example.com/foo => ../bar
|
||||
use foo
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
replace example.com/foo => ../bar
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_before_replace`,
|
||||
`use foo
|
||||
replace example.com/foo => ../bar
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
use foo
|
||||
replace example.com/foo => ../bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_only`,
|
||||
`use foo
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`already_have_go`,
|
||||
`go 1.17
|
||||
`,
|
||||
`1.18`,
|
||||
`
|
||||
go 1.18
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workAddToolchainTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
version string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`go1.17`,
|
||||
`toolchain go1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`aftergo`,
|
||||
`// this is a comment
|
||||
use foo
|
||||
|
||||
go 1.17
|
||||
|
||||
use bar
|
||||
`,
|
||||
`go1.17`,
|
||||
`// this is a comment
|
||||
use foo
|
||||
|
||||
go 1.17
|
||||
|
||||
toolchain go1.17
|
||||
|
||||
use bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
`already_have_toolchain`,
|
||||
`go 1.17
|
||||
|
||||
toolchain go1.18
|
||||
`,
|
||||
`go1.19`,
|
||||
`go 1.17
|
||||
|
||||
toolchain go1.19
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workSortBlocksTests = []struct {
|
||||
desc, in, out string
|
||||
}{
|
||||
{
|
||||
`use_duplicates_not_removed`,
|
||||
`go 1.17
|
||||
use foo
|
||||
use bar
|
||||
use (
|
||||
foo
|
||||
)`,
|
||||
`go 1.17
|
||||
use foo
|
||||
use bar
|
||||
use (
|
||||
foo
|
||||
)`,
|
||||
},
|
||||
{
|
||||
`replace_duplicates_removed`,
|
||||
`go 1.17
|
||||
use foo
|
||||
replace x.y/z v1.0.0 => ./a
|
||||
replace x.y/z v1.1.0 => ./b
|
||||
replace (
|
||||
x.y/z v1.0.0 => ./c
|
||||
)
|
||||
`,
|
||||
`go 1.17
|
||||
use foo
|
||||
replace x.y/z v1.1.0 => ./b
|
||||
replace (
|
||||
x.y/z v1.0.0 => ./c
|
||||
)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddUse(t *testing.T) {
|
||||
for _, tt := range workAddUseTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddUse(tt.path, tt.modulePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropUse(t *testing.T) {
|
||||
for _, tt := range workDropUseTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
if err := f.DropUse(tt.path); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Cleanup()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkAddGo(t *testing.T) {
|
||||
for _, tt := range workAddGoTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddGoStmt(tt.version)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkAddToolchain(t *testing.T) {
|
||||
for _, tt := range workAddToolchainTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddToolchainStmt(tt.version)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkSortBlocks(t *testing.T) {
|
||||
for _, tt := range workSortBlocksTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
f.SortBlocks()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkAddGodebug(t *testing.T) {
|
||||
for _, tt := range addGodebugTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
in := strings.ReplaceAll(tt.in, "module m", "use foo")
|
||||
out := strings.ReplaceAll(tt.out, "module m", "use foo")
|
||||
testWorkEdit(t, in, out, func(f *WorkFile) error {
|
||||
err := f.AddGodebug(tt.key, tt.value)
|
||||
f.Cleanup()
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkDropGodebug(t *testing.T) {
|
||||
for _, tt := range dropGodebugTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
in := strings.ReplaceAll(tt.in, "module m", "use foo")
|
||||
out := strings.ReplaceAll(tt.out, "module m", "use foo")
|
||||
testWorkEdit(t, in, out, func(f *WorkFile) error {
|
||||
f.DropGodebug(tt.key)
|
||||
f.Cleanup()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when files in the testdata directory are parsed
|
||||
// and printed and parsed again, we get the same parse tree
|
||||
// both times.
|
||||
func TestWorkPrintParse(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/work/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := filepath.Base(out)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := "testdata/work/" + filepath.Base(out)
|
||||
f, err := parse(base, data)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing original: %v", err)
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
f2, err := parse(base, ndata)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing reformatted: %v", err)
|
||||
}
|
||||
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(f, f2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/parse): %v", err)
|
||||
}
|
||||
|
||||
pf1, err := ParseWork(base, data, nil)
|
||||
if err != nil {
|
||||
t.Errorf("should parse %v: %v", base, err)
|
||||
}
|
||||
if err == nil {
|
||||
pf2, err := ParseWork(base, ndata, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted: %v", err)
|
||||
}
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/Parse): %v", err)
|
||||
}
|
||||
|
||||
ndata2 := Format(pf1.Syntax)
|
||||
pf3, err := ParseWork(base, ndata2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted2: %v", err)
|
||||
}
|
||||
eq = eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf3); err != nil {
|
||||
t.Errorf("not equal (Parse/Format/Parse): %v", err)
|
||||
}
|
||||
ndata = ndata2
|
||||
}
|
||||
|
||||
if strings.HasSuffix(out, ".in") {
|
||||
golden, err := os.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testWorkEdit(t *testing.T, in, want string, transform func(f *WorkFile) error) *WorkFile {
|
||||
t.Helper()
|
||||
parse := ParseWork
|
||||
f, err := parse("in", []byte(in), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g, err := parse("out", []byte(want), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
golden := Format(g.Syntax)
|
||||
|
||||
if err := transform(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := Format(f.Syntax)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(out, golden) {
|
||||
t.Errorf("have:\n%s\nwant:\n%s", out, golden)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
Reference in New Issue
Block a user