From 0d3153546b17aebca4ce89e06ae4a05d682dea68 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 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 --- 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 7ad17b14c..dd4a94eae 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 @@ -461,6 +463,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 != "" { @@ -484,8 +487,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 } @@ -580,3 +592,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 a5e8eb699..56b897631 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 + wantPackageVersions 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", }, + wantPackageVersions: 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", + }, + wantPackageVersions: 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", }, + wantPackageVersions: 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, @@ -447,6 +492,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.wantPackageVersions { + 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)) + }) + } }) } }