1 package main 2 3 import ( 4 "archive/zip" 5 "errors" 6 "go/ast" 7 "go/build" 8 "go/importer" 9 "go/parser" 10 "go/token" 11 "go/types" 12 "io" 13 "io/fs" 14 "iter" 15 "os" 16 "path" 17 "path/filepath" 18 "runtime" 19 "slices" 20 "strings" 21 22 "golang.org/x/mod/modfile" 23 "golang.org/x/mod/module" 24 "vimagination.zapto.org/httpreaderat" 25 ) 26 27 type filesystem interface { 28 OpenFile(string) (io.ReadCloser, error) 29 IsDir(string) bool 30 ReadDir(string) ([]fs.FileInfo, error) 31 ReadFile(name string) ([]byte, error) 32 } 33 34 func ParsePackage(modulePath string, ignore ...string) (*types.Package, error) { 35 var ( 36 m *moduleDetails 37 sd string 38 ) 39 40 for path, sub := range splitPath(modulePath) { 41 var err error 42 43 if m, err = parseModFile(&osFS{os.DirFS(path).(statReadDirFileFS)}, path); err == nil { 44 sd = sub 45 46 break 47 } 48 } 49 50 if m == nil { 51 return nil, errNoModFile 52 } 53 54 return m.importPath(path.Join(m.Module, sd), ignore...) 55 } 56 57 func splitPath(path string) iter.Seq2[string, string] { 58 return func(yield func(string, string) bool) { 59 left := strings.TrimSuffix(path, "/") 60 right := "" 61 62 for { 63 if !yield(left, right) { 64 return 65 } 66 67 pos := strings.LastIndexByte(left, '/') 68 if pos == -1 { 69 return 70 } 71 72 left = path[:pos] 73 right = path[pos+1:] 74 } 75 } 76 } 77 78 func hasSubdir(root, dir string) (string, bool) { 79 if strings.HasPrefix(dir, root) { 80 return strings.TrimPrefix(dir, root), true 81 } 82 83 return "", false 84 } 85 86 func listGoFiles(fsys filesystem) ([]string, error) { 87 ctx := build.Context{ 88 GOARCH: runtime.GOARCH, 89 GOOS: runtime.GOOS, 90 Compiler: runtime.Compiler, 91 IsDir: fsys.IsDir, 92 HasSubdir: hasSubdir, 93 ReadDir: fsys.ReadDir, 94 OpenFile: fsys.OpenFile, 95 } 96 97 pkg, err := ctx.ImportDir(".", 0) 98 if err != nil { 99 return nil, err 100 } 101 102 return pkg.GoFiles, nil 103 } 104 105 type moduleDetails struct { 106 Module string 107 Path string 108 Imports map[string]module.Version 109 fset *token.FileSet 110 defaultImporter types.Importer 111 cache map[string]*types.Package 112 } 113 114 func parseModFile(fsys filesystem, path string) (*moduleDetails, error) { 115 data, err := fsys.ReadFile("go.mod") 116 if err != nil { 117 return nil, err 118 } 119 120 f, err := modfile.Parse("go.mod", data, nil) 121 if err != nil { 122 return nil, err 123 } 124 125 imports := make(map[string]module.Version, len(f.Require)) 126 127 for _, r := range f.Require { 128 imports[r.Mod.Path] = r.Mod 129 } 130 131 for _, r := range f.Replace { 132 if m, ok := imports[r.Old.Path]; ok && (r.Old.Version == "" || r.Old.Version == m.Version) { 133 imports[r.Old.Path] = r.New 134 } 135 } 136 137 fset := token.NewFileSet() 138 139 return &moduleDetails{ 140 Module: f.Module.Mod.Path, 141 Path: path, 142 Imports: imports, 143 fset: fset, 144 defaultImporter: importer.ForCompiler(fset, runtime.Compiler, nil), 145 cache: make(map[string]*types.Package), 146 }, nil 147 } 148 149 func (m *moduleDetails) ParsePackage(fsys filesystem, pkgPath string, ignore ...string) (*types.Package, error) { 150 files, err := listGoFiles(fsys) 151 if err != nil { 152 return nil, err 153 } 154 155 if len(ignore) > 0 { 156 filtered := make([]string, 0, len(files)) 157 158 for _, file := range files { 159 if !slices.Contains(ignore, file) { 160 filtered = append(filtered, file) 161 } 162 } 163 164 files = filtered 165 } 166 167 parsedFiles, err := m.parseFiles(pkgPath, fsys, files) 168 if err != nil { 169 return nil, err 170 } 171 172 var ( 173 conf = types.Config{ 174 GoVersion: runtime.Version(), 175 Importer: m, 176 } 177 info = types.Info{ 178 Types: make(map[ast.Expr]types.TypeAndValue), 179 } 180 ) 181 182 return conf.Check(pkgPath, m.fset, parsedFiles, &info) 183 } 184 185 func (m *moduleDetails) parseFiles(pkgPath string, fsys filesystem, files []string) ([]*ast.File, error) { 186 var pkg string 187 188 parsedFiles := make([]*ast.File, len(files)) 189 190 for n, file := range files { 191 f, err := fsys.OpenFile(file) 192 if err != nil { 193 return nil, err 194 } 195 196 pf, err := parser.ParseFile(m.fset, path.Join(pkgPath, file), f, parser.AllErrors|parser.ParseComments) 197 if err != nil { 198 return nil, err 199 } 200 201 if pkg == "" { 202 pkg = pf.Name.Name 203 } else if pkg != pf.Name.Name { 204 return nil, errMultiplePackages 205 } 206 207 parsedFiles[n] = pf 208 } 209 210 return parsedFiles, nil 211 } 212 213 func (m *moduleDetails) Import(path string) (*types.Package, error) { 214 if pkg, ok := m.cache[path]; ok { 215 return pkg, nil 216 } 217 218 pkg, err := m.importPath(path) 219 if err != nil { 220 return nil, err 221 } 222 223 m.cache[path] = pkg 224 225 return pkg, nil 226 } 227 228 func (m *moduleDetails) importPath(path string, ignore ...string) (*types.Package, error) { 229 im := m.Resolve(path) 230 if im == nil { 231 return m.defaultImporter.Import(path) 232 } 233 234 fs, err := im.AsFS() 235 if err != nil { 236 return nil, err 237 } 238 239 return m.ParsePackage(fs, path, ignore...) 240 } 241 242 type importDetails struct { 243 Base, Version, Path string 244 } 245 246 func (m *moduleDetails) Resolve(importURL string) *importDetails { 247 if strings.HasPrefix(importURL, m.Module+"/") || importURL == m.Module { 248 return &importDetails{Base: m.Path, Version: "", Path: strings.TrimPrefix(strings.TrimPrefix(importURL, m.Module), "/")} 249 } 250 251 for url, mod := range m.Imports { 252 if url == importURL { 253 return &importDetails{Base: mod.Path, Version: mod.Version, Path: "."} 254 } else if strings.HasPrefix(importURL, url) { 255 base := strings.TrimPrefix(importURL, url) 256 257 if strings.HasPrefix(base, "/") { 258 return &importDetails{Base: mod.Path, Version: mod.Version, Path: strings.TrimPrefix(base, "/")} 259 } 260 } 261 } 262 263 return nil 264 } 265 266 func (i *importDetails) CachedModPath() (string, error) { 267 if modfile.IsDirectoryPath(i.Base) { 268 return i.Base, nil 269 } 270 271 dir, err := i.Dir() 272 if err != nil { 273 return "", err 274 } 275 276 return filepath.Join(build.Default.GOPATH, "pkg", "mod", dir), nil 277 } 278 279 func (i *importDetails) ModCacheURL() (string, error) { 280 p, err := module.EscapePath(i.Base) 281 if err != nil { 282 return "", err 283 } 284 285 ver, err := module.EscapeVersion(i.Version) 286 if err != nil { 287 return "", err 288 } 289 290 return "https://proxy.golang.org" + path.Join("/", p, "@v", ver+".zip"), nil 291 } 292 293 func (i *importDetails) Dir() (string, error) { 294 path, err := module.EscapePath(i.Base) 295 if err != nil { 296 return "", err 297 } 298 299 ver, err := module.EscapeVersion(i.Version) 300 if err != nil { 301 return "", err 302 } 303 304 return path + "@" + ver, nil 305 } 306 307 func (i *importDetails) AsFS() (filesystem, error) { 308 local, err := i.CachedModPath() 309 if err != nil { 310 return nil, err 311 } 312 313 if s, err := os.Stat(local); err == nil { 314 if s.IsDir() { 315 return &osFS{os.DirFS(filepath.Join(local, i.Path)).(statReadDirFileFS)}, nil 316 } 317 } else if !errors.Is(err, fs.ErrNotExist) { 318 return nil, err 319 } 320 321 return i.remotePackageFS() 322 } 323 324 func (i *importDetails) remotePackageFS() (filesystem, error) { 325 remote, err := i.ModCacheURL() 326 if err != nil { 327 return nil, err 328 } 329 330 r, err := httpreaderat.NewRequest(remote) 331 if err != nil { 332 return nil, err 333 } 334 335 z, err := zip.NewReader(r, r.Length()) 336 if err != nil { 337 return nil, err 338 } 339 340 dir, err := i.Dir() 341 if err != nil { 342 return nil, err 343 } 344 345 return &zipFS{z, dir}, nil 346 } 347 348 type statReadDirFileFS interface { 349 fs.StatFS 350 fs.ReadDirFS 351 fs.ReadFileFS 352 } 353 354 type osFS struct { 355 statReadDirFileFS 356 } 357 358 func (o *osFS) OpenFile(path string) (io.ReadCloser, error) { 359 return o.Open(path) 360 } 361 362 func (o *osFS) IsDir(path string) bool { 363 s, err := o.Stat(path) 364 if err != nil { 365 return false 366 } 367 368 return s.IsDir() 369 } 370 371 func (o *osFS) ReadDir(path string) ([]fs.FileInfo, error) { 372 f, err := o.Open(path) 373 if err != nil { 374 return nil, err 375 } 376 377 return f.(*os.File).Readdir(-1) 378 } 379 380 var ( 381 errMultiplePackages = errors.New("multiple packages found") 382 errNoModFile = errors.New("no module file found") 383 ) 384