diff --git a/mod/modfile/modfile.go b/mod/modfile/modfile.go index c4e5fb61d..70dfa847c 100644 --- a/mod/modfile/modfile.go +++ b/mod/modfile/modfile.go @@ -22,6 +22,7 @@ package modfile import ( _ "embed" "fmt" + "path" "slices" "strings" "sync" @@ -53,12 +54,13 @@ type File struct { // Use the [File.QualifiedModule] method to obtain a module // path that's always qualified. See also the // [File.ModulePath] and [File.MajorVersion] methods. - Module string `json:"module"` - Language *Language `json:"language,omitempty"` - Source *Source `json:"source,omitempty"` - Deps map[string]*Dep `json:"deps,omitempty"` - Custom map[string]map[string]any `json:"custom,omitempty"` - versions []module.Version + Module string `json:"module"` + Language *Language `json:"language,omitempty"` + Source *Source `json:"source,omitempty"` + Deps map[string]*Dep `json:"deps,omitempty"` + Custom map[string]map[string]any `json:"custom,omitempty"` + versions []module.Version + versionByModule map[string]module.Version // defaultMajorVersions maps from module base path to the // major version default for that path. defaultMajorVersions map[string]string @@ -463,6 +465,7 @@ func parse(modfile []byte, filename string, strict bool) (*File, error) { return nil, fmt.Errorf("language version %v in %s is not canonical", vers, filename) } } + mf.versionByModule = make(map[string]module.Version) var versions []module.Version defaultMajorVersions := make(map[string]string) if mainPath != "" { @@ -486,8 +489,17 @@ func parse(modfile []byte, filename string, strict bool) (*File, error) { } defaultMajorVersions[mp] = semver.Major(vers.Version()) } + mf.versionByModule[vers.Path()] = vers + } + if mainPath != "" { + // We don't necessarily have a full version for the main module. + mainWithMajor := mainPath + "@" + mainMajor + mainVersion, err := module.NewVersion(mainWithMajor, "") + if err != nil { + return nil, err + } + mf.versionByModule[mainWithMajor] = mainVersion } - if len(defaultMajorVersions) > 0 { mf.defaultMajorVersions = defaultMajorVersions } @@ -582,3 +594,27 @@ func (f *File) DepVersions() []module.Version { func (f *File) DefaultMajorVersions() map[string]string { return f.defaultMajorVersions } + +// ModuleForImportPath returns the module that should contain the given +// import path and reports whether the module was found. +// It does not check to see if the import path actually exists within the module. +// +// It works entirely from information in f, meaning that it does +// not consult a registry to resolve a package whose module is not +// mentioned in the file, which means it will not work in general unless +// the module is tidy (as with `cue mod tidy`). +func (f *File) ModuleForImportPath(importPath string) (module.Version, bool) { + ip := module.ParseImportPath(importPath) + for prefix := ip.Path; prefix != "."; prefix = path.Dir(prefix) { + pkgVersion := ip.Version + if pkgVersion == "" { + if pkgVersion = f.defaultMajorVersions[prefix]; pkgVersion == "" { + continue + } + } + if mv, ok := f.versionByModule[prefix+"@"+pkgVersion]; ok { + return mv, true + } + } + return module.Version{}, false +} diff --git a/mod/modfile/modfile_test.go b/mod/modfile/modfile_test.go index b6035cbb1..bddd8ab36 100644 --- a/mod/modfile/modfile_test.go +++ b/mod/modfile/modfile_test.go @@ -27,13 +27,14 @@ import ( ) var parseTests = []struct { - testName string - parse func(modfile []byte, filename string) (*File, error) - data string - wantError string - want *File - wantVersions []module.Version - wantDefaults map[string]string + testName string + parse func(modfile []byte, filename string) (*File, error) + data string + wantError string + want *File + wantVersions []module.Version + wantDefaults map[string]string + wantModVersionForPkg map[string]string }{{ testName: "NoDeps", parse: Parse, @@ -50,6 +51,15 @@ language: version: "v0.8.0-alpha.0" wantDefaults: map[string]string{ "foo.com/bar": "v0", }, + wantModVersionForPkg: map[string]string{ + "foo.com/bar": "foo.com/bar@v0", + "foo.com/bar@v0": "foo.com/bar@v0", + "foo.com/bar/baz@v0": "foo.com/bar@v0", + "foo.com/bar@v1": "", + "foo.com/bar:hello": "foo.com/bar@v0", + "foo.com/bar/baz:hello": "foo.com/bar@v0", + "foo.com/bar/baz@v0:hello": "foo.com/bar@v0", + }, }, { testName: "WithDeps", parse: Parse, @@ -60,6 +70,11 @@ deps: "example.com@v1": { default: true v: "v1.2.3" } +deps: "example.com/other@v1": v: "v1.9.10" +deps: "example.com/other/more/nested@v2": { + v: "v2.9.20" + default: true +} deps: "other.com/something@v0": v: "v0.2.3" `, want: &File{ @@ -75,12 +90,36 @@ deps: "other.com/something@v0": v: "v0.2.3" "other.com/something@v0": { Version: "v0.2.3", }, + "example.com/other@v1": { + Version: "v1.9.10", + }, + "example.com/other/more/nested@v2": { + Version: "v2.9.20", + Default: true, + }, }, }, - wantVersions: parseVersions("example.com@v1.2.3", "other.com/something@v0.2.3"), + wantVersions: parseVersions( + "example.com/other/more/nested@v2.9.20", + "example.com/other@v1.9.10", + "example.com@v1.2.3", + "other.com/something@v0.2.3", + ), wantDefaults: map[string]string{ - "foo.com/bar": "v0", - "example.com": "v1", + "example.com/other/more/nested": "v2", + "foo.com/bar": "v0", + "example.com": "v1", + }, + wantModVersionForPkg: map[string]string{ + "example.com": "example.com@v1.2.3", + "example.com/x/y@v1": "example.com@v1.2.3", + "example.com/x/y@v1:x": "example.com@v1.2.3", + "example.com/other@v1": "example.com/other@v1.9.10", + "example.com/other/p@v1": "example.com/other@v1.9.10", + "example.com/other/more": "example.com@v1.2.3", + "example.com/other/more@v1": "example.com/other@v1.9.10", + "example.com/other/more/nested": "example.com/other/more/nested@v2.9.20", + "example.com/other/more/nested/x:p": "example.com/other/more/nested@v2.9.20", }, }, { testName: "WithSource", @@ -270,6 +309,12 @@ deps: "example.com": v: "v1.2.3" wantDefaults: map[string]string{ "foo.com/bar": "v0", }, + wantModVersionForPkg: map[string]string{ + "example.com": "", // No default major version. + "example.com@v1": "example.com@v1.2.3", + "example.com/x/y@v1": "example.com@v1.2.3", + "example.com/x/y@v1:x": "example.com@v1.2.3", + }, }, { testName: "LegacyWithExtraFields", parse: ParseLegacy, @@ -445,6 +490,17 @@ func TestParse(t *testing.T) { qt.Assert(t, qt.Equals(f.ModulePath(), f.Module)) qt.Assert(t, qt.Equals(f.MajorVersion(), "v0")) } + for p, m := range test.wantModVersionForPkg { + t.Run("package-"+p, func(t *testing.T) { + mv, ok := f.ModuleForImportPath(p) + if m == "" { + qt.Assert(t, qt.IsFalse(ok), qt.Commentf("got version %v", mv)) + return + } + qt.Check(t, qt.IsTrue(ok)) + qt.Check(t, qt.Equals(mv.String(), m)) + }) + } }) } }