cuelang.org/go@v0.13.0/mod/modfile/modfile.go (about)

     1  // Copyright 2023 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package modfile provides functionality for reading and parsing
    16  // the CUE module file, cue.mod/module.cue.
    17  //
    18  // WARNING: THIS PACKAGE IS EXPERIMENTAL.
    19  // ITS API MAY CHANGE AT ANY TIME.
    20  package modfile
    21  
    22  import (
    23  	_ "embed"
    24  	"fmt"
    25  	"path"
    26  	"slices"
    27  	"strings"
    28  	"sync"
    29  
    30  	"cuelang.org/go/internal/mod/semver"
    31  
    32  	"cuelang.org/go/cue"
    33  	"cuelang.org/go/cue/ast"
    34  	"cuelang.org/go/cue/build"
    35  	"cuelang.org/go/cue/cuecontext"
    36  	"cuelang.org/go/cue/errors"
    37  	"cuelang.org/go/cue/format"
    38  	"cuelang.org/go/cue/token"
    39  	"cuelang.org/go/internal/cueversion"
    40  	"cuelang.org/go/internal/encoding"
    41  	"cuelang.org/go/internal/filetypes"
    42  	"cuelang.org/go/mod/module"
    43  )
    44  
    45  //go:embed schema.cue
    46  var moduleSchemaData string
    47  
    48  const schemaFile = "cuelang.org/go/mod/modfile/schema.cue"
    49  
    50  // File represents the contents of a cue.mod/module.cue file.
    51  type File struct {
    52  	// Module holds the module path, which may
    53  	// not contain a major version suffix.
    54  	// Use the [File.QualifiedModule] method to obtain a module
    55  	// path that's always qualified. See also the
    56  	// [File.ModulePath] and [File.MajorVersion] methods.
    57  	Module          string                    `json:"module"`
    58  	Language        *Language                 `json:"language,omitempty"`
    59  	Source          *Source                   `json:"source,omitempty"`
    60  	Deps            map[string]*Dep           `json:"deps,omitempty"`
    61  	Custom          map[string]map[string]any `json:"custom,omitempty"`
    62  	versions        []module.Version
    63  	versionByModule map[string]module.Version
    64  	// defaultMajorVersions maps from module base path to the
    65  	// major version default for that path.
    66  	defaultMajorVersions map[string]string
    67  	// actualSchemaVersion holds the actual schema version
    68  	// that was used to validate the file. This will be one of the
    69  	// entries in the versions field in schema.cue and
    70  	// is set by the Parse functions.
    71  	actualSchemaVersion string
    72  }
    73  
    74  // Module returns the fully qualified module path
    75  // if is one. It returns the empty string when [ParseLegacy]
    76  // is used and the module field is empty.
    77  //
    78  // Note that when the module field does not contain a major
    79  // version suffix, "@v0" is assumed.
    80  func (f *File) QualifiedModule() string {
    81  	if strings.Contains(f.Module, "@") {
    82  		return f.Module
    83  	}
    84  	if f.Module == "" {
    85  		return ""
    86  	}
    87  	return f.Module + "@v0"
    88  }
    89  
    90  // ModulePath returns the path part of the module without
    91  // its major version suffix.
    92  func (f *File) ModulePath() string {
    93  	path, _, _ := ast.SplitPackageVersion(f.QualifiedModule())
    94  	return path
    95  }
    96  
    97  // MajorVersion returns the major version of the module,
    98  // not including the "@".
    99  // If there is no module (which can happen when [ParseLegacy]
   100  // is used or if Module is explicitly set to an empty string),
   101  // it returns the empty string.
   102  func (f *File) MajorVersion() string {
   103  	_, vers, _ := ast.SplitPackageVersion(f.QualifiedModule())
   104  	return vers
   105  }
   106  
   107  // baseFileVersion is used to decode the language version
   108  // to decide how to decode the rest of the file.
   109  type baseFileVersion struct {
   110  	Language struct {
   111  		Version string `json:"version"`
   112  	} `json:"language"`
   113  }
   114  
   115  // Source represents how to transform from a module's
   116  // source to its actual contents.
   117  type Source struct {
   118  	Kind string `json:"kind"`
   119  }
   120  
   121  // Validate checks that src is well formed.
   122  func (src *Source) Validate() error {
   123  	switch src.Kind {
   124  	case "git", "self":
   125  		return nil
   126  	}
   127  	return fmt.Errorf("unrecognized source kind %q", src.Kind)
   128  }
   129  
   130  // Format returns a formatted representation of f
   131  // in CUE syntax.
   132  func (f *File) Format() ([]byte, error) {
   133  	if len(f.Deps) == 0 && f.Deps != nil {
   134  		// There's no way to get the CUE encoder to omit an empty
   135  		// but non-nil slice (despite the current doc comment on
   136  		// [cue.Context.Encode], so make a copy of f to allow us
   137  		// to do that.
   138  		f1 := *f
   139  		f1.Deps = nil
   140  		f = &f1
   141  	}
   142  	// TODO this could be better:
   143  	// - it should omit the outer braces
   144  	v := cuecontext.New().Encode(f)
   145  	if err := v.Validate(cue.Concrete(true)); err != nil {
   146  		return nil, err
   147  	}
   148  	n := v.Syntax(cue.Concrete(true)).(*ast.StructLit)
   149  
   150  	data, err := format.Node(&ast.File{
   151  		Decls: n.Elts,
   152  	})
   153  	if err != nil {
   154  		return nil, fmt.Errorf("cannot format: %v", err)
   155  	}
   156  	// Sanity check that it can be parsed.
   157  	// TODO this could be more efficient by checking all the file fields
   158  	// before formatting the output.
   159  	f1, err := ParseNonStrict(data, "-")
   160  	if err != nil {
   161  		return nil, fmt.Errorf("cannot parse result: %v", strings.TrimSuffix(errors.Details(err, nil), "\n"))
   162  	}
   163  	if f.Language != nil && f1.actualSchemaVersion == "v0.0.0" {
   164  		// It's not a legacy module file (because the language field is present)
   165  		// but we've used the legacy schema to parse it, which means that
   166  		// it's almost certainly a bogus version because all versions
   167  		// we care about fail when there are unknown fields, but the
   168  		// original schema allowed all fields.
   169  		return nil, fmt.Errorf("language version %v is too early for module.cue (need at least %v)", f.Language.Version, EarliestClosedSchemaVersion())
   170  	}
   171  	return data, err
   172  }
   173  
   174  type Language struct {
   175  	Version string `json:"version,omitempty"`
   176  }
   177  
   178  type Dep struct {
   179  	Version string `json:"v"`
   180  	Default bool   `json:"default,omitempty"`
   181  }
   182  
   183  type noDepsFile struct {
   184  	Module string `json:"module"`
   185  }
   186  
   187  var (
   188  	moduleSchemaOnce sync.Once // guards the creation of _moduleSchema
   189  	// TODO remove this mutex when https://cuelang.org/issue/2733 is fixed.
   190  	moduleSchemaMutex sync.Mutex // guards any use of _moduleSchema
   191  	_schemas          schemaInfo
   192  )
   193  
   194  type schemaInfo struct {
   195  	Versions                    map[string]cue.Value `json:"versions"`
   196  	EarliestClosedSchemaVersion string               `json:"earliestClosedSchemaVersion"`
   197  }
   198  
   199  // moduleSchemaDo runs f with information about all the schema versions
   200  // present in schema.cue. It does this within a mutex because it is
   201  // not currently allowed to use cue.Value concurrently.
   202  // TODO remove the mutex when https://cuelang.org/issue/2733 is fixed.
   203  func moduleSchemaDo[T any](f func(*schemaInfo) (T, error)) (T, error) {
   204  	moduleSchemaOnce.Do(func() {
   205  		// It is important that this cue.Context not be used for building any other cue.Value,
   206  		// such as in [Parse] or [ParseLegacy].
   207  		// A value holds memory as long as the context it was built with is kept alive for,
   208  		// and this context is alive forever via the _schemas global.
   209  		//
   210  		// TODO(mvdan): this violates the documented API rules in the cue package:
   211  		//
   212  		//    Only values created from the same Context can be involved in the same operation.
   213  		//
   214  		// However, this appears to work in practice, and all alternatives right now would be
   215  		// either too costly or awkward. We want to lift that API restriction, and this works OK,
   216  		// so leave it as-is for the time being.
   217  		ctx := cuecontext.New()
   218  		schemav := ctx.CompileString(moduleSchemaData, cue.Filename(schemaFile))
   219  		if err := schemav.Decode(&_schemas); err != nil {
   220  			panic(fmt.Errorf("internal error: invalid CUE module.cue schema: %v", errors.Details(err, nil)))
   221  		}
   222  	})
   223  	moduleSchemaMutex.Lock()
   224  	defer moduleSchemaMutex.Unlock()
   225  	return f(&_schemas)
   226  }
   227  
   228  func lookup(v cue.Value, sels ...cue.Selector) cue.Value {
   229  	return v.LookupPath(cue.MakePath(sels...))
   230  }
   231  
   232  // EarliestClosedSchemaVersion returns the earliest module.cue schema version
   233  // that excludes unknown fields. Any version declared in a module.cue file
   234  // should be at least this, because that's when we added the language.version
   235  // field itself.
   236  func EarliestClosedSchemaVersion() string {
   237  	return earliestClosedSchemaVersion()
   238  }
   239  
   240  var earliestClosedSchemaVersion = sync.OnceValue(func() string {
   241  	earliest, _ := moduleSchemaDo(func(info *schemaInfo) (string, error) {
   242  		earliest := ""
   243  		for v := range info.Versions {
   244  			if earliest == "" || semver.Compare(v, earliest) < 0 {
   245  				earliest = v
   246  			}
   247  		}
   248  		return earliest, nil
   249  	})
   250  	return earliest
   251  })
   252  
   253  // Parse verifies that the module file has correct syntax
   254  // and follows the schema following the required language.version field.
   255  // The file name is used for error messages.
   256  // All dependencies must be specified correctly: with major
   257  // versions in the module paths and canonical dependency versions.
   258  func Parse(modfile []byte, filename string) (*File, error) {
   259  	return parse(modfile, filename, true)
   260  }
   261  
   262  // ParseLegacy parses the legacy version of the module file
   263  // that only supports the single field "module" and ignores all other
   264  // fields.
   265  func ParseLegacy(modfile []byte, filename string) (*File, error) {
   266  	ctx := cuecontext.New()
   267  	file, err := parseDataOnlyCUE(ctx, modfile, filename)
   268  	if err != nil {
   269  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax")
   270  	}
   271  	// Unfortunately we need a new context. See the note inside [moduleSchemaDo].
   272  	v := ctx.BuildFile(file)
   273  	if err := v.Err(); err != nil {
   274  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file")
   275  	}
   276  	var f noDepsFile
   277  	if err := v.Decode(&f); err != nil {
   278  		return nil, newCUEError(err, filename)
   279  	}
   280  	return &File{
   281  		Module:              f.Module,
   282  		actualSchemaVersion: "v0.0.0",
   283  	}, nil
   284  }
   285  
   286  // ParseNonStrict is like Parse but allows some laxity in the parsing:
   287  //   - if a module path lacks a version, it's taken from the version.
   288  //   - if a non-canonical version is used, it will be canonicalized.
   289  //
   290  // The file name is used for error messages.
   291  func ParseNonStrict(modfile []byte, filename string) (*File, error) {
   292  	return parse(modfile, filename, false)
   293  }
   294  
   295  // FixLegacy converts a legacy module.cue file as parsed by [ParseLegacy]
   296  // into a format suitable for parsing with [Parse]. It adds a language.version
   297  // field and moves all unrecognized fields into custom.legacy.
   298  //
   299  // If there is no module field or it is empty, it is set to "test.example".
   300  //
   301  // If the file already parses OK with [ParseNonStrict], it returns the
   302  // result of that.
   303  func FixLegacy(modfile []byte, filename string) (*File, error) {
   304  	f, err := ParseNonStrict(modfile, filename)
   305  	if err == nil {
   306  		// It parses OK so it doesn't need fixing.
   307  		return f, nil
   308  	}
   309  	ctx := cuecontext.New()
   310  	file, err := parseDataOnlyCUE(ctx, modfile, filename)
   311  	if err != nil {
   312  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax")
   313  	}
   314  	v := ctx.BuildFile(file)
   315  	if err := v.Validate(cue.Concrete(true)); err != nil {
   316  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file value")
   317  	}
   318  	var allFields map[string]any
   319  	if err := v.Decode(&allFields); err != nil {
   320  		return nil, err
   321  	}
   322  	mpath := "test.example"
   323  	if m, ok := allFields["module"]; ok {
   324  		if mpath1, ok := m.(string); ok && mpath1 != "" {
   325  			mpath = mpath1
   326  		} else if !ok {
   327  			return nil, fmt.Errorf("module field has unexpected type %T", m)
   328  		}
   329  		// TODO decide what to do if the module path isn't OK according to the new rules.
   330  	}
   331  	customLegacy := make(map[string]any)
   332  	for k, v := range allFields {
   333  		if k != "module" {
   334  			customLegacy[k] = v
   335  		}
   336  	}
   337  	var custom map[string]map[string]any
   338  	if len(customLegacy) > 0 {
   339  		custom = map[string]map[string]any{
   340  			"legacy": customLegacy,
   341  		}
   342  	}
   343  	f = &File{
   344  		Module: mpath,
   345  		Language: &Language{
   346  			// If there's a legacy module file, the CUE code
   347  			// is unlikely to be using new language features,
   348  			// so keep the language version fixed rather than
   349  			// using [cueversion.LanguageVersion].
   350  			// See https://cuelang.org/issue/3222.
   351  			Version: "v0.9.0",
   352  		},
   353  		Custom: custom,
   354  	}
   355  	// Round-trip through [Parse] so that we get exactly the same
   356  	// result as a later parse of the same data will. This also
   357  	// adds a major version to the module path if needed.
   358  	data, err := f.Format()
   359  	if err != nil {
   360  		return nil, fmt.Errorf("cannot format fixed file: %v", err)
   361  	}
   362  	f, err = ParseNonStrict(data, "fixed-"+filename)
   363  	if err != nil {
   364  		return nil, fmt.Errorf("cannot parse resulting module file %q: %v", data, err)
   365  	}
   366  	return f, nil
   367  }
   368  
   369  func parse(modfile []byte, filename string, strict bool) (*File, error) {
   370  	// Unfortunately we need a new context. See the note inside [moduleSchemaDo].
   371  	ctx := cuecontext.New()
   372  	file, err := parseDataOnlyCUE(ctx, modfile, filename)
   373  	if err != nil {
   374  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file syntax")
   375  	}
   376  
   377  	v := ctx.BuildFile(file)
   378  	if err := v.Validate(cue.Concrete(true)); err != nil {
   379  		return nil, errors.Wrapf(err, token.NoPos, "invalid module.cue file value")
   380  	}
   381  	// First determine the declared version of the module file.
   382  	var base baseFileVersion
   383  	if err := v.Decode(&base); err != nil {
   384  		return nil, errors.Wrapf(err, token.NoPos, "cannot determine language version")
   385  	}
   386  	if base.Language.Version == "" {
   387  		return nil, ErrNoLanguageVersion
   388  	}
   389  	if !semver.IsValid(base.Language.Version) {
   390  		return nil, fmt.Errorf("language version %q in module.cue is not valid semantic version", base.Language.Version)
   391  	}
   392  	if mv, lv := base.Language.Version, cueversion.LanguageVersion(); semver.Compare(mv, lv) > 0 {
   393  		return nil, fmt.Errorf("language version %q declared in module.cue is too new for current language version %q", mv, lv)
   394  	}
   395  
   396  	mf, err := moduleSchemaDo(func(schemas *schemaInfo) (*File, error) {
   397  		// Now that we're happy we're within bounds, find the latest
   398  		// schema that applies to the declared version.
   399  		latest := ""
   400  		var latestSchema cue.Value
   401  		for vers, schema := range schemas.Versions {
   402  			if semver.Compare(vers, base.Language.Version) > 0 {
   403  				continue
   404  			}
   405  			if latest == "" || semver.Compare(vers, latest) > 0 {
   406  				latest = vers
   407  				latestSchema = schema
   408  			}
   409  		}
   410  		if latest == "" {
   411  			// Should never happen, because there should always
   412  			// be some applicable schema.
   413  			return nil, fmt.Errorf("cannot find schema suitable for reading module file with language version %q", base.Language.Version)
   414  		}
   415  		schema := latestSchema
   416  		v = v.Unify(lookup(schema, cue.Def("#File")))
   417  		if err := v.Validate(); err != nil {
   418  			return nil, newCUEError(err, filename)
   419  		}
   420  		if latest == "v0.0.0" {
   421  			// The chosen schema is the earliest schema which allowed
   422  			// all fields. We don't actually want a module.cue file with
   423  			// an old version to treat those fields as special, so don't try
   424  			// to decode into *File because that will do so.
   425  			// This mirrors the behavior of [ParseLegacy].
   426  			var f noDepsFile
   427  			if err := v.Decode(&f); err != nil {
   428  				return nil, newCUEError(err, filename)
   429  			}
   430  			return &File{
   431  				Module:              f.Module,
   432  				actualSchemaVersion: "v0.0.0",
   433  			}, nil
   434  		}
   435  		var mf File
   436  		if err := v.Decode(&mf); err != nil {
   437  			return nil, errors.Wrapf(err, token.NoPos, "internal error: cannot decode into modFile struct")
   438  		}
   439  		mf.actualSchemaVersion = latest
   440  		return &mf, nil
   441  	})
   442  	if err != nil {
   443  		return nil, err
   444  	}
   445  	mainPath, mainMajor, ok := ast.SplitPackageVersion(mf.Module)
   446  	if ok {
   447  		if semver.Major(mainMajor) != mainMajor {
   448  			return nil, fmt.Errorf("module path %s in %q should contain the major version only", mf.Module, filename)
   449  		}
   450  	} else if mainPath != "" {
   451  		if err := module.CheckPathWithoutVersion(mainPath); err != nil {
   452  			return nil, fmt.Errorf("module path %q in %q is not valid: %v", mainPath, filename, err)
   453  		}
   454  		// There's no main module major version: default to v0.
   455  		mainMajor = "v0"
   456  	} else {
   457  		return nil, fmt.Errorf("empty module path in %q", filename)
   458  	}
   459  	if mf.Language != nil {
   460  		vers := mf.Language.Version
   461  		if !semver.IsValid(vers) {
   462  			return nil, fmt.Errorf("language version %q in %s is not well formed", vers, filename)
   463  		}
   464  		if semver.Canonical(vers) != vers {
   465  			return nil, fmt.Errorf("language version %v in %s is not canonical", vers, filename)
   466  		}
   467  	}
   468  	mf.versionByModule = make(map[string]module.Version)
   469  	var versions []module.Version
   470  	defaultMajorVersions := make(map[string]string)
   471  	if mainPath != "" {
   472  		// The main module is always the default for its own major version.
   473  		defaultMajorVersions[mainPath] = mainMajor
   474  	}
   475  	// Check that major versions match dependency versions.
   476  	for m, dep := range mf.Deps {
   477  		vers, err := module.NewVersion(m, dep.Version)
   478  		if err != nil {
   479  			return nil, fmt.Errorf("invalid module.cue file %s: cannot make version from module %q, version %q: %v", filename, m, dep.Version, err)
   480  		}
   481  		versions = append(versions, vers)
   482  		if strict && vers.Path() != m {
   483  			return nil, fmt.Errorf("invalid module.cue file %s: no major version in %q", filename, m)
   484  		}
   485  		if dep.Default {
   486  			mp := vers.BasePath()
   487  			if _, ok := defaultMajorVersions[mp]; ok {
   488  				return nil, fmt.Errorf("multiple default major versions found for %v", mp)
   489  			}
   490  			defaultMajorVersions[mp] = semver.Major(vers.Version())
   491  		}
   492  		mf.versionByModule[vers.Path()] = vers
   493  	}
   494  	if mainPath != "" {
   495  		// We don't necessarily have a full version for the main module.
   496  		mainWithMajor := mainPath + "@" + mainMajor
   497  		mainVersion, err := module.NewVersion(mainWithMajor, "")
   498  		if err != nil {
   499  			return nil, err
   500  		}
   501  		mf.versionByModule[mainWithMajor] = mainVersion
   502  	}
   503  	if len(defaultMajorVersions) > 0 {
   504  		mf.defaultMajorVersions = defaultMajorVersions
   505  	}
   506  	mf.versions = versions[:len(versions):len(versions)]
   507  	slices.SortFunc(mf.versions, module.Version.Compare)
   508  	return mf, nil
   509  }
   510  
   511  // ErrNoLanguageVersion is returned by [Parse] and [ParseNonStrict]
   512  // when a cue.mod/module.cue file lacks the `language.version` field.
   513  var ErrNoLanguageVersion = fmt.Errorf("no language version declared in module.cue")
   514  
   515  func parseDataOnlyCUE(ctx *cue.Context, cueData []byte, filename string) (*ast.File, error) {
   516  	dec := encoding.NewDecoder(ctx, &build.File{
   517  		Filename:       filename,
   518  		Encoding:       build.CUE,
   519  		Interpretation: build.Auto,
   520  		Form:           build.Data,
   521  		Source:         cueData,
   522  	}, &encoding.Config{
   523  		Mode:      filetypes.Export,
   524  		AllErrors: true,
   525  	})
   526  	if err := dec.Err(); err != nil {
   527  		return nil, err
   528  	}
   529  	return dec.File(), nil
   530  }
   531  
   532  func newCUEError(err error, filename string) error {
   533  	ps := errors.Positions(err)
   534  	for _, p := range ps {
   535  		if errStr := findErrorComment(p); errStr != "" {
   536  			return fmt.Errorf("invalid module.cue file: %s", errStr)
   537  		}
   538  	}
   539  	// TODO we have more potential to improve error messages here.
   540  	return err
   541  }
   542  
   543  // findErrorComment finds an error comment in the form
   544  //
   545  //	//error: ...
   546  //
   547  // before the given position.
   548  // This works as a kind of poor-man's error primitive
   549  // so we can customize the error strings when verification
   550  // fails.
   551  func findErrorComment(p token.Pos) string {
   552  	if p.Filename() != schemaFile {
   553  		return ""
   554  	}
   555  	off := p.Offset()
   556  	source := moduleSchemaData
   557  	if off > len(source) {
   558  		return ""
   559  	}
   560  	source, _, ok := cutLast(source[:off], "\n")
   561  	if !ok {
   562  		return ""
   563  	}
   564  	_, errorLine, ok := cutLast(source, "\n")
   565  	if !ok {
   566  		return ""
   567  	}
   568  	errStr, ok := strings.CutPrefix(errorLine, "//error: ")
   569  	if !ok {
   570  		return ""
   571  	}
   572  	return errStr
   573  }
   574  
   575  func cutLast(s, sep string) (before, after string, found bool) {
   576  	if i := strings.LastIndex(s, sep); i >= 0 {
   577  		return s[:i], s[i+len(sep):], true
   578  	}
   579  	return "", s, false
   580  }
   581  
   582  // DepVersions returns the versions of all the modules depended on by the
   583  // file. The caller should not modify the returned slice.
   584  //
   585  // This always returns the same value, even if the contents
   586  // of f are changed. If f was not created with [Parse], it returns nil.
   587  func (f *File) DepVersions() []module.Version {
   588  	return slices.Clip(f.versions)
   589  }
   590  
   591  // DefaultMajorVersions returns a map from module base path
   592  // to the major version that's specified as the default for that module.
   593  // The caller should not modify the returned map.
   594  func (f *File) DefaultMajorVersions() map[string]string {
   595  	return f.defaultMajorVersions
   596  }
   597  
   598  // ModuleForImportPath returns the module that should contain the given
   599  // import path and reports whether the module was found.
   600  // It does not check to see if the import path actually exists within the module.
   601  //
   602  // It works entirely from information in f, meaning that it does
   603  // not consult a registry to resolve a package whose module is not
   604  // mentioned in the file, which means it will not work in general unless
   605  // the module is tidy (as with `cue mod tidy`).
   606  func (f *File) ModuleForImportPath(importPath string) (module.Version, bool) {
   607  	ip := ast.ParseImportPath(importPath)
   608  	for prefix := ip.Path; prefix != "."; prefix = path.Dir(prefix) {
   609  		pkgVersion := ip.Version
   610  		if pkgVersion == "" {
   611  			if pkgVersion = f.defaultMajorVersions[prefix]; pkgVersion == "" {
   612  				continue
   613  			}
   614  		}
   615  		if mv, ok := f.versionByModule[prefix+"@"+pkgVersion]; ok {
   616  			return mv, true
   617  		}
   618  	}
   619  	return module.Version{}, false
   620  }