From b684a52eba474a16fe730f3ae5d68b9ac3991d8e Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Wed, 29 Jan 2025 11:12:16 +0000 Subject: [PATCH] mod/modfile: add ModuleForImportPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes it straightforward to resolve modules and default major versions for packages without consulting a registry or invoking any of the code inside internal/mod. This is valid to do for packages that are part of the current dependency graph as long as the module is tidy, because the tidy algorithm guarantees that all dependency modules are present in the module.cue file. Signed-off-by: Roger Peppe Change-Id: Ia0d86b335e2c1f2963e75f7901322fb55a842643 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1207985 Reviewed-by: Daniel Martí TryBot-Result: CUEcueckoo Unity-Result: CUE porcuepine --- mod/modfile/modfile.go | 50 ++++++++++++++++++++---- mod/modfile/modfile_test.go | 76 ++++++++++++++++++++++++++++++++----- 2 files changed, 109 insertions(+), 17 deletions(-) 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)) + }) + } }) } }