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 }