github.com/goplus/gop@v1.2.6/load.go (about)

     1  /*
     2   * Copyright (c) 2022 The GoPlus Authors (goplus.org). All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package gop
    18  
    19  import (
    20  	"fmt"
    21  	"io/fs"
    22  	"os"
    23  	"path"
    24  	"strings"
    25  
    26  	"github.com/goplus/gogen"
    27  	"github.com/goplus/gop/ast"
    28  	"github.com/goplus/gop/cl"
    29  	"github.com/goplus/gop/parser"
    30  	"github.com/goplus/gop/token"
    31  	"github.com/goplus/gop/x/c2go"
    32  	"github.com/goplus/gop/x/gopenv"
    33  	"github.com/goplus/mod/env"
    34  	"github.com/goplus/mod/gopmod"
    35  	"github.com/qiniu/x/errors"
    36  )
    37  
    38  var (
    39  	ErrNotFound      = gopmod.ErrNotFound
    40  	ErrIgnoreNotated = errors.New("notated error ignored")
    41  )
    42  
    43  // NotFound returns if cause err is ErrNotFound or not
    44  func NotFound(err error) bool {
    45  	return gopmod.IsNotFound(err)
    46  }
    47  
    48  // IgnoreNotated returns if cause err is ErrIgnoreNotated or not.
    49  func IgnoreNotated(err error) bool {
    50  	return errors.Err(err) == ErrIgnoreNotated
    51  }
    52  
    53  // ErrorPos returns where the error occurs.
    54  func ErrorPos(err error) token.Pos {
    55  	switch v := err.(type) {
    56  	case *gogen.CodeError:
    57  		return v.Pos
    58  	case *gogen.MatchError:
    59  		return v.Pos()
    60  	case *gogen.ImportError:
    61  		return v.Pos
    62  	}
    63  	return token.NoPos
    64  }
    65  
    66  func ignNotatedErrs(err error, pkg *ast.Package, fset *token.FileSet) error {
    67  	switch v := err.(type) {
    68  	case errors.List:
    69  		var ret errors.List
    70  		for _, e := range v {
    71  			if isNotatedErr(e, pkg, fset) {
    72  				continue
    73  			}
    74  			ret = append(ret, e)
    75  		}
    76  		if len(ret) == 0 {
    77  			return ErrIgnoreNotated
    78  		}
    79  		return ret
    80  	default:
    81  		if isNotatedErr(err, pkg, fset) {
    82  			return ErrIgnoreNotated
    83  		}
    84  		return err
    85  	}
    86  }
    87  
    88  func isNotatedErr(err error, pkg *ast.Package, fset *token.FileSet) (notatedErr bool) {
    89  	pos := ErrorPos(err)
    90  	f := fset.File(pos)
    91  	if f == nil {
    92  		return
    93  	}
    94  	gopf, ok := pkg.Files[f.Name()]
    95  	if !ok {
    96  		return
    97  	}
    98  	lines := token.Lines(f)
    99  	i := f.Line(pos) - 1 // base 0
   100  	start := lines[i]
   101  	var end int
   102  	if i+1 < len(lines) {
   103  		end = lines[i+1]
   104  	} else {
   105  		end = f.Size()
   106  	}
   107  	text := string(gopf.Code[start:end])
   108  	commentOff := strings.Index(text, "//")
   109  	if commentOff < 0 {
   110  		return
   111  	}
   112  	return strings.Contains(text[commentOff+2:], "compile error:")
   113  }
   114  
   115  // -----------------------------------------------------------------------------
   116  
   117  // Config represents a configuration for loading Go+ packages.
   118  type Config struct {
   119  	Gop      *env.Gop
   120  	Fset     *token.FileSet
   121  	Mod      *gopmod.Module
   122  	Importer *Importer
   123  
   124  	Filter func(fs.FileInfo) bool
   125  
   126  	// If not nil, it is used for returning result of checks Go+ dependencies.
   127  	// see https://pkg.go.dev/github.com/goplus/gogen#File.CheckGopDeps
   128  	GopDeps *int
   129  
   130  	// CacheFile specifies the file path of the cache.
   131  	CacheFile string
   132  
   133  	IgnoreNotatedError bool
   134  	DontUpdateGoMod    bool
   135  }
   136  
   137  // ConfFlags represents configuration flags.
   138  type ConfFlags int
   139  
   140  const (
   141  	ConfFlagIgnoreNotatedError ConfFlags = 1 << iota
   142  	ConfFlagDontUpdateGoMod
   143  	ConfFlagNoTestFiles
   144  	ConfFlagNoCacheFile
   145  )
   146  
   147  // NewDefaultConf creates a dfault configuration for common cases.
   148  func NewDefaultConf(dir string, flags ConfFlags) (conf *Config, err error) {
   149  	mod, err := LoadMod(dir)
   150  	if err != nil {
   151  		return
   152  	}
   153  	gop := gopenv.Get()
   154  	fset := token.NewFileSet()
   155  	imp := NewImporter(mod, gop, fset)
   156  	conf = &Config{
   157  		Gop: gop, Fset: fset, Mod: mod, Importer: imp,
   158  		IgnoreNotatedError: flags&ConfFlagIgnoreNotatedError != 0,
   159  		DontUpdateGoMod:    flags&ConfFlagDontUpdateGoMod != 0,
   160  	}
   161  	if flags&ConfFlagNoCacheFile == 0 {
   162  		conf.CacheFile = imp.CacheFile()
   163  		imp.Cache().Load(conf.CacheFile)
   164  	}
   165  	if flags&ConfFlagNoTestFiles != 0 {
   166  		conf.Filter = FilterNoTestFiles
   167  	}
   168  	return
   169  }
   170  
   171  // UpdateCache updates the cache.
   172  func (conf *Config) UpdateCache(verbose ...bool) {
   173  	if conf.CacheFile != "" {
   174  		c := conf.Importer.Cache()
   175  		c.Save(conf.CacheFile)
   176  		if verbose != nil && verbose[0] {
   177  			fmt.Println("Times of calling go list:", c.ListTimes())
   178  		}
   179  	}
   180  }
   181  
   182  // LoadMod loads a Go+ module from a specified directory.
   183  func LoadMod(dir string) (mod *gopmod.Module, err error) {
   184  	mod, err = gopmod.Load(dir)
   185  	if err != nil && !gopmod.IsNotFound(err) {
   186  		err = errors.NewWith(err, `gopmod.Load(dir, 0)`, -2, "gopmod.Load", dir, 0)
   187  		return
   188  	}
   189  	if mod == nil {
   190  		mod = gopmod.Default
   191  	}
   192  	err = mod.ImportClasses()
   193  	if err != nil {
   194  		err = errors.NewWith(err, `mod.ImportClasses()`, -2, "(*gopmod.Module).ImportClasses", mod)
   195  	}
   196  	return
   197  }
   198  
   199  // FilterNoTestFiles filters to skip all testing files.
   200  func FilterNoTestFiles(fi fs.FileInfo) bool {
   201  	fname := fi.Name()
   202  	suffix := ""
   203  	switch path.Ext(fname) {
   204  	case ".gox":
   205  		suffix = "test.gox"
   206  	case ".gop":
   207  		suffix = "_test.gop"
   208  	case ".go":
   209  		suffix = "_test.go"
   210  	default:
   211  		return true
   212  	}
   213  	return !strings.HasSuffix(fname, suffix)
   214  }
   215  
   216  // -----------------------------------------------------------------------------
   217  
   218  // LoadDir loads Go+ packages from a specified directory.
   219  func LoadDir(dir string, conf *Config, genTestPkg bool, promptGenGo ...bool) (out, test *gogen.Package, err error) {
   220  	if conf == nil {
   221  		conf = new(Config)
   222  	}
   223  
   224  	mod := conf.Mod
   225  	if mod == nil {
   226  		if mod, err = LoadMod(dir); err != nil {
   227  			err = errors.NewWith(err, `LoadMod(dir)`, -2, "gop.LoadMod", dir)
   228  			return
   229  		}
   230  	}
   231  
   232  	fset := conf.Fset
   233  	if fset == nil {
   234  		fset = token.NewFileSet()
   235  	}
   236  	pkgs, err := parser.ParseDirEx(fset, dir, parser.Config{
   237  		ClassKind: mod.ClassKind,
   238  		Filter:    conf.Filter,
   239  		Mode:      parser.ParseComments | parser.SaveAbsFile,
   240  	})
   241  	if err != nil {
   242  		return
   243  	}
   244  	if len(pkgs) == 0 {
   245  		return nil, nil, ErrNotFound
   246  	}
   247  
   248  	gop := conf.Gop
   249  	if gop == nil {
   250  		gop = gopenv.Get()
   251  	}
   252  	imp := conf.Importer
   253  	if imp == nil {
   254  		imp = NewImporter(mod, gop, fset)
   255  	}
   256  
   257  	var pkgTest *ast.Package
   258  	var clConf = &cl.Config{
   259  		Fset:         fset,
   260  		RelativeBase: relativeBaseOf(mod),
   261  		Importer:     imp,
   262  		LookupClass:  mod.LookupClass,
   263  		LookupPub:    c2go.LookupPub(mod),
   264  	}
   265  
   266  	for name, pkg := range pkgs {
   267  		if strings.HasSuffix(name, "_test") {
   268  			if pkgTest != nil {
   269  				return nil, nil, ErrMultiTestPackges
   270  			}
   271  			pkgTest = pkg
   272  			continue
   273  		}
   274  		if out != nil {
   275  			return nil, nil, ErrMultiPackges
   276  		}
   277  		if len(pkg.Files) == 0 { // no Go+ source files
   278  			continue
   279  		}
   280  		if promptGenGo != nil && promptGenGo[0] {
   281  			fmt.Fprintln(os.Stderr, "GenGo", dir, "...")
   282  		}
   283  		out, err = cl.NewPackage("", pkg, clConf)
   284  		if err != nil {
   285  			if conf.IgnoreNotatedError {
   286  				err = ignNotatedErrs(err, pkg, fset)
   287  			}
   288  			return
   289  		}
   290  	}
   291  	if out == nil {
   292  		return nil, nil, ErrNotFound
   293  	}
   294  	if genTestPkg && pkgTest != nil {
   295  		test, err = cl.NewPackage("", pkgTest, clConf)
   296  	}
   297  	afterLoad(mod, gop, out, test, conf)
   298  	return
   299  }
   300  
   301  func afterLoad(mod *gopmod.Module, gop *env.Gop, out, test *gogen.Package, conf *Config) {
   302  	if mod.Path() == gopMod { // nothing to do for Go+ itself
   303  		return
   304  	}
   305  	updateMod := !conf.DontUpdateGoMod && mod.HasModfile()
   306  	if updateMod || conf.GopDeps != nil {
   307  		flags := checkGopDeps(out)
   308  		if conf.GopDeps != nil { // for `gop run`
   309  			*conf.GopDeps = flags
   310  		}
   311  		if updateMod {
   312  			if test != nil {
   313  				flags |= checkGopDeps(test)
   314  			}
   315  			if flags != 0 {
   316  				mod.SaveWithGopMod(gop, flags)
   317  			}
   318  		}
   319  	}
   320  }
   321  
   322  func checkGopDeps(pkg *gogen.Package) (flags int) {
   323  	pkg.ForEachFile(func(fname string, file *gogen.File) {
   324  		flags |= file.CheckGopDeps(pkg)
   325  	})
   326  	return
   327  }
   328  
   329  func relativeBaseOf(mod *gopmod.Module) string {
   330  	if mod.HasModfile() {
   331  		return mod.Root()
   332  	}
   333  	dir, _ := os.Getwd()
   334  	return dir
   335  }
   336  
   337  // -----------------------------------------------------------------------------
   338  
   339  // LoadDir loads a Go+ package from specified files.
   340  func LoadFiles(dir string, files []string, conf *Config) (out *gogen.Package, err error) {
   341  	if conf == nil {
   342  		conf = new(Config)
   343  	}
   344  
   345  	mod := conf.Mod
   346  	if mod == nil {
   347  		if mod, err = LoadMod(dir); err != nil {
   348  			err = errors.NewWith(err, `LoadMod(dir)`, -2, "gop.LoadMod", dir)
   349  			return
   350  		}
   351  	}
   352  
   353  	fset := conf.Fset
   354  	if fset == nil {
   355  		fset = token.NewFileSet()
   356  	}
   357  	pkgs, err := parser.ParseEntries(fset, files, parser.Config{
   358  		ClassKind: mod.ClassKind,
   359  		Filter:    conf.Filter,
   360  		Mode:      parser.ParseComments | parser.SaveAbsFile,
   361  	})
   362  	if err != nil {
   363  		err = errors.NewWith(err, `parser.ParseFiles(fset, files, parser.ParseComments)`, -2, "parser.ParseFiles", fset, files, parser.ParseComments)
   364  		return
   365  	}
   366  	if len(pkgs) != 1 {
   367  		err = errors.NewWith(ErrMultiPackges, `len(pkgs) != 1`, -1, "!=", len(pkgs), 1)
   368  		return
   369  	}
   370  	gop := conf.Gop
   371  	if gop == nil {
   372  		gop = gopenv.Get()
   373  	}
   374  	for _, pkg := range pkgs {
   375  		imp := conf.Importer
   376  		if imp == nil {
   377  			imp = NewImporter(mod, gop, fset)
   378  		}
   379  		clConf := &cl.Config{
   380  			Fset:         fset,
   381  			RelativeBase: relativeBaseOf(mod),
   382  			Importer:     imp,
   383  			LookupClass:  mod.LookupClass,
   384  			LookupPub:    c2go.LookupPub(mod),
   385  		}
   386  		out, err = cl.NewPackage("", pkg, clConf)
   387  		if err != nil {
   388  			if conf.IgnoreNotatedError {
   389  				err = ignNotatedErrs(err, pkg, fset)
   390  			}
   391  		}
   392  		break
   393  	}
   394  	afterLoad(mod, gop, out, nil, conf)
   395  	return
   396  }
   397  
   398  // -----------------------------------------------------------------------------
   399  
   400  var (
   401  	ErrMultiPackges     = errors.New("multiple packages")
   402  	ErrMultiTestPackges = errors.New("multiple test packages")
   403  )
   404  
   405  // -----------------------------------------------------------------------------
   406  
   407  // GetFileClassType get gop module file classType.
   408  func GetFileClassType(mod *gopmod.Module, file *ast.File, filename string) (classType string, isTest bool, ok bool) {
   409  	return cl.GetFileClassType(file, filename, mod.LookupClass)
   410  }
   411  
   412  // -----------------------------------------------------------------------------