github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/gnomod/parse.go (about)

     1  package gnomod
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"reflect"
     8  	"strings"
     9  
    10  	"golang.org/x/mod/modfile"
    11  	"golang.org/x/mod/module"
    12  )
    13  
    14  // ParseAt parses, validates and returns a gno.mod file located at dir or at
    15  // dir's parents.
    16  func ParseAt(dir string) (*File, error) {
    17  	ferr := func(err error) (*File, error) {
    18  		return nil, fmt.Errorf("parsing gno.mod at %s: %w", dir, err)
    19  	}
    20  
    21  	// FindRootDir requires absolute path, make sure its the case
    22  	absDir, err := filepath.Abs(dir)
    23  	if err != nil {
    24  		return ferr(err)
    25  	}
    26  	rd, err := FindRootDir(absDir)
    27  	if err != nil {
    28  		return ferr(err)
    29  	}
    30  	fname := filepath.Join(rd, "gno.mod")
    31  	b, err := os.ReadFile(fname)
    32  	if err != nil {
    33  		return ferr(err)
    34  	}
    35  	gm, err := Parse(fname, b)
    36  	if err != nil {
    37  		return ferr(err)
    38  	}
    39  	if err := gm.Validate(); err != nil {
    40  		return ferr(err)
    41  	}
    42  	return gm, nil
    43  }
    44  
    45  // tries to parse gno mod file given the filename, using Parse and Validate from
    46  // the gnomod package
    47  //
    48  // TODO(tb): replace by `gnomod.ParseAt` ? The key difference is the latter
    49  // looks for gno.mod in parent directories, while this function doesn't.
    50  func ParseGnoMod(fname string) (*File, error) {
    51  	file, err := os.Stat(fname)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("could not read gno.mod file: %w", err)
    54  	}
    55  	if file.IsDir() {
    56  		return nil, fmt.Errorf("invalid gno.mod at %q: is a directory", fname)
    57  	}
    58  
    59  	b, err := os.ReadFile(fname)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("could not read gno.mod file: %w", err)
    62  	}
    63  	gm, err := Parse(fname, b)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("error parsing gno.mod file at %q: %w", fname, err)
    66  	}
    67  	if err := gm.Validate(); err != nil {
    68  		return nil, fmt.Errorf("error validating gno.mod file at %q: %w", fname, err)
    69  	}
    70  	return gm, nil
    71  }
    72  
    73  // Parse parses and returns a gno.mod file.
    74  //
    75  // - file is the name of the file, used in positions and errors.
    76  // - data is the content of the file.
    77  func Parse(file string, data []byte) (*File, error) {
    78  	fs, err := parse(file, data)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	f := &File{
    83  		Syntax: fs,
    84  	}
    85  	var errs modfile.ErrorList
    86  
    87  	for _, x := range fs.Stmt {
    88  		switch x := x.(type) {
    89  		case *modfile.Line:
    90  			f.add(&errs, nil, x, x.Token[0], x.Token[1:])
    91  		case *modfile.LineBlock:
    92  			if len(x.Token) > 1 {
    93  				errs = append(errs, modfile.Error{
    94  					Filename: file,
    95  					Pos:      x.Start,
    96  					Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
    97  				})
    98  				continue
    99  			}
   100  			switch x.Token[0] {
   101  			default:
   102  				errs = append(errs, modfile.Error{
   103  					Filename: file,
   104  					Pos:      x.Start,
   105  					Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
   106  				})
   107  				continue
   108  			case "module", "require", "replace":
   109  				for _, l := range x.Line {
   110  					f.add(&errs, x, l, x.Token[0], l.Token)
   111  				}
   112  			}
   113  		case *modfile.CommentBlock:
   114  			if x.Start.Line == 1 {
   115  				f.Draft = parseDraft(x)
   116  			}
   117  		}
   118  	}
   119  
   120  	if len(errs) > 0 {
   121  		return nil, errs
   122  	}
   123  	return f, nil
   124  }
   125  
   126  func (f *File) add(errs *modfile.ErrorList, block *modfile.LineBlock, line *modfile.Line, verb string, args []string) {
   127  	wrapError := func(err error) {
   128  		*errs = append(*errs, modfile.Error{
   129  			Filename: f.Syntax.Name,
   130  			Pos:      line.Start,
   131  			Err:      err,
   132  		})
   133  	}
   134  	errorf := func(format string, args ...interface{}) {
   135  		wrapError(fmt.Errorf(format, args...))
   136  	}
   137  
   138  	switch verb {
   139  	default:
   140  		errorf("unknown directive: %s", verb)
   141  
   142  	case "go":
   143  		if f.Go != nil {
   144  			errorf("repeated go statement")
   145  			return
   146  		}
   147  		if len(args) != 1 {
   148  			errorf("go directive expects exactly one argument")
   149  			return
   150  		} else if !modfile.GoVersionRE.MatchString(args[0]) {
   151  			fixed := false
   152  			if !fixed {
   153  				errorf("invalid go version '%s': must match format 1.23", args[0])
   154  				return
   155  			}
   156  		}
   157  
   158  		line := reflect.ValueOf(line).Interface().(*modfile.Line)
   159  		f.Go = &modfile.Go{Syntax: line}
   160  		f.Go.Version = args[0]
   161  
   162  	case "module":
   163  		if f.Module != nil {
   164  			errorf("repeated module statement")
   165  			return
   166  		}
   167  		deprecated := parseDeprecation(block, line)
   168  		f.Module = &modfile.Module{
   169  			Syntax:     line,
   170  			Deprecated: deprecated,
   171  		}
   172  		if len(args) != 1 {
   173  			errorf("usage: module module/path")
   174  			return
   175  		}
   176  		s, err := parseString(&args[0])
   177  		if err != nil {
   178  			errorf("invalid quoted string: %v", err)
   179  			return
   180  		}
   181  		f.Module.Mod = module.Version{Path: s}
   182  
   183  	case "require":
   184  		if len(args) != 2 {
   185  			errorf("usage: %s module/path v1.2.3", verb)
   186  			return
   187  		}
   188  		s, err := parseString(&args[0])
   189  		if err != nil {
   190  			errorf("invalid quoted string: %v", err)
   191  			return
   192  		}
   193  		v, err := parseVersion(verb, s, &args[1])
   194  		if err != nil {
   195  			wrapError(err)
   196  			return
   197  		}
   198  		f.Require = append(f.Require, &modfile.Require{
   199  			Mod:    module.Version{Path: s, Version: v},
   200  			Syntax: line,
   201  		})
   202  
   203  	case "replace":
   204  		replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args)
   205  		if wrappederr != nil {
   206  			*errs = append(*errs, *wrappederr)
   207  			return
   208  		}
   209  		f.Replace = append(f.Replace, replace)
   210  	}
   211  }