Skip to content

Commit 416adfc

Browse files
committed
cmd/cue: new command: cue mod rename
This command changes the module path of the current main module, adjusting all import paths accordingly. It still isn't perfect (it doesn't check for references to "submodules"), but it's probably good enough for now. Fixes #3626. Signed-off-by: Roger Peppe <[email protected]> Change-Id: I9afc1a30a0b96955a061dbb51a18d84b07082254 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1207142 TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Daniel Martí <[email protected]>
1 parent fdf92d1 commit 416adfc

File tree

6 files changed

+428
-7
lines changed

6 files changed

+428
-7
lines changed

cmd/cue/cmd/fmt.go

+3-7
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,9 @@ func formatFile(file *build.File, opts []format.Option, doDiff, check bool, cmd
179179
// We buffer the input and output bytes to compare them.
180180
// This allows us to determine whether a file is already
181181
// formatted, without modifying the file.
182-
src, ok := file.Source.([]byte)
183-
if !ok {
184-
var err error
185-
src, err = source.ReadAll(file.Filename, file.Source)
186-
if err != nil {
187-
return false, err
188-
}
182+
src, err := source.ReadAll(file.Filename, file.Source)
183+
if err != nil {
184+
return false, err
189185
}
190186

191187
syntax, err := parser.ParseFile(file.Filename, src, parser.ParseComments)

cmd/cue/cmd/mod.go

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ See also:
4949
cmd.AddCommand(newModGetCmd(c))
5050
cmd.AddCommand(newModInitCmd(c))
5151
cmd.AddCommand(newModRegistryCmd(c))
52+
cmd.AddCommand(newModRenameCmd(c))
5253
cmd.AddCommand(newModResolveCmd(c))
5354
cmd.AddCommand(newModTidyCmd(c))
5455
cmd.AddCommand(newModUploadCmd(c))

cmd/cue/cmd/modrename.go

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright 2025 The 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 cmd
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"cuelang.org/go/cue/ast"
24+
"cuelang.org/go/cue/build"
25+
"cuelang.org/go/cue/format"
26+
"cuelang.org/go/cue/literal"
27+
"cuelang.org/go/cue/load"
28+
"cuelang.org/go/cue/parser"
29+
"cuelang.org/go/internal/mod/semver"
30+
"cuelang.org/go/mod/module"
31+
"github.com/spf13/cobra"
32+
)
33+
34+
func newModRenameCmd(c *Command) *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "rename <newModulePath>",
37+
Short: "rename the current module",
38+
Long: `Rename changes the name of the current module,
39+
updating import statements in source files as required.
40+
41+
Note that this command is not yet stable and may be changed.
42+
`,
43+
RunE: mkRunE(c, runModRename),
44+
Args: cobra.ExactArgs(1),
45+
}
46+
47+
return cmd
48+
}
49+
50+
type modRenamer struct {
51+
oldModulePath string
52+
oldModuleMajor string
53+
oldModuleQualifier string
54+
newModulePath string
55+
newModuleMajor string
56+
newModuleQualifier string
57+
}
58+
59+
func runModRename(cmd *Command, args []string) error {
60+
modFilePath, mf, _, err := readModuleFile()
61+
if err != nil {
62+
return nil
63+
}
64+
if mf.Module == args[0] {
65+
// Nothing to do
66+
return nil
67+
}
68+
var mr modRenamer
69+
mr.oldModulePath, mr.oldModuleMajor, err = splitModulePath(mf.Module)
70+
if err != nil {
71+
return err
72+
}
73+
mr.oldModuleQualifier = module.ParseImportPath(mr.oldModulePath).Qualifier
74+
mf.Module = args[0]
75+
mr.newModulePath, mr.newModuleMajor, err = splitModulePath(mf.Module)
76+
if err != nil {
77+
return err
78+
}
79+
mr.newModuleQualifier = module.ParseImportPath(mr.newModulePath).Qualifier
80+
81+
// TODO if we're renaming to a module that we currently depend on,
82+
// perhaps we should detect that and give an error.
83+
newModFileData, err := mf.Format()
84+
if err != nil {
85+
return fmt.Errorf("invalid resulting module.cue file after edits: %v", err)
86+
}
87+
if err := os.WriteFile(modFilePath, newModFileData, 0o666); err != nil {
88+
return err
89+
}
90+
91+
modRoot, err := findModuleRoot()
92+
if err != nil {
93+
return err
94+
}
95+
binst := load.Instances([]string{"./..."}, &load.Config{
96+
Dir: modRoot,
97+
ModuleRoot: modRoot,
98+
Tests: true,
99+
Tools: true,
100+
AllCUEFiles: true,
101+
Package: "*",
102+
// Note: the mod renaming can work even when
103+
// some external imports don't.
104+
SkipImports: true,
105+
})
106+
if len(binst) == 0 {
107+
// No packages to rename.
108+
return nil
109+
}
110+
if binst[0].Module == "" {
111+
return fmt.Errorf("no current module to rename")
112+
}
113+
for _, inst := range binst {
114+
if err := inst.Err; err != nil {
115+
return err
116+
}
117+
for _, file := range inst.BuildFiles {
118+
if filepath.Dir(file.Filename) != inst.Dir {
119+
// Avoid processing files which are inherited from parent directories.
120+
continue
121+
}
122+
if err := mr.renameFile(file); err != nil {
123+
return err
124+
}
125+
}
126+
}
127+
return nil
128+
}
129+
130+
func (mr *modRenamer) renameFile(file *build.File) error {
131+
syntax, err := parser.ParseFile(file.Filename, file.Source, parser.ParseComments)
132+
if err != nil {
133+
return err
134+
}
135+
136+
changed := false
137+
for _, decl := range syntax.Preamble() {
138+
if decl, ok := decl.(*ast.ImportDecl); ok {
139+
for _, spec := range decl.Specs {
140+
ch, err := mr.rewriteImport(spec)
141+
if err != nil {
142+
return err
143+
}
144+
changed = changed || ch
145+
}
146+
}
147+
}
148+
if !changed {
149+
return nil
150+
}
151+
data, err := format.Node(syntax)
152+
if err != nil {
153+
return err
154+
}
155+
if err := os.WriteFile(file.Filename, data, 0o666); err != nil {
156+
return err
157+
}
158+
return nil
159+
}
160+
161+
func (mr *modRenamer) rewriteImport(spec *ast.ImportSpec) (changed bool, err error) {
162+
importPath, err := literal.Unquote(spec.Path.Value)
163+
if err != nil {
164+
return false, fmt.Errorf("malformed import path in AST: %v", err)
165+
}
166+
ip := module.ParseImportPath(importPath)
167+
if !pkgIsUnderneath(ip.Path, mr.oldModulePath) {
168+
return false, nil
169+
}
170+
171+
// TODO it's possible that we've got a import of a package in a nested module
172+
// rather than a reference to a package in this module. We can only
173+
// tell that by actually importing the dependencies and looking up the
174+
// package in those dependencies, which seems like overkill for now at least.
175+
if ip.Qualifier == "" {
176+
return false, fmt.Errorf("import path %q has no implied package qualifier", importPath)
177+
}
178+
if ip.Version != "" && ip.Version != mr.oldModuleMajor {
179+
// Same module, different major version. Don't change it.
180+
return false, nil
181+
}
182+
ip.Path = mr.newModulePath + strings.TrimPrefix(ip.Path, mr.oldModulePath)
183+
if ip.Version != "" {
184+
// Keep the major version if it was there already; the main
185+
// module is always the default.
186+
ip.Version = mr.newModuleMajor
187+
}
188+
// Note: ip.Qualifier remains the same as before, which means
189+
// that regardless of the new import path, the implied identifier
190+
// will remain the same, so no change is needed to spec.Ident.
191+
ip.ExplicitQualifier = false // Only include if needed.
192+
spec.Path.Value = literal.String.Quote(ip.String())
193+
return true, nil
194+
}
195+
196+
// pkgIsUnderneath reports whether pkg2 is a package
197+
// underneath (or the same as) pkg1.
198+
func pkgIsUnderneath(pkg1, pkg2 string) bool {
199+
if len(pkg1) < len(pkg2) {
200+
return false
201+
}
202+
if !strings.HasPrefix(pkg1, pkg2) {
203+
return false
204+
}
205+
return len(pkg1) == len(pkg2) || pkg1[len(pkg2)] == '/'
206+
}
207+
208+
func splitModulePath(path string) (mpath string, mvers string, err error) {
209+
mpath, mvers, ok := module.SplitPathVersion(path)
210+
if ok {
211+
if semver.Major(mvers) != mvers {
212+
return "", "", fmt.Errorf("module path %q should contain the major version only", path)
213+
}
214+
if err := module.CheckPath(mpath); err != nil {
215+
return "", "", fmt.Errorf("invalid module path %q: %v", path, err)
216+
}
217+
return mpath, mvers, nil
218+
}
219+
mpath = path
220+
if err := module.CheckPathWithoutVersion(mpath); err != nil {
221+
return "", "", fmt.Errorf("invalid module path %q: %v", path, err)
222+
}
223+
return mpath, "v0", nil
224+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Make sure that things work OK when cue mod rename renames
2+
# a module where the last component differs.
3+
4+
# Make a copy of the original files.
5+
cp cue.mod/module.cue cue.mod/module.cue-0
6+
cp x.cue x.cue-0
7+
cp y/y.cue y/y.cue-0
8+
9+
exec cue mod rename main.org/y
10+
cmp cue.mod/module.cue cue.mod/module.cue-1
11+
cmp x.cue x.cue-1
12+
cmp y/y.cue y/y.cue-1
13+
14+
# Check that renaming back to the original name
15+
# gets us back to the original content.
16+
exec cue mod rename main.org/x
17+
cmp cue.mod/module.cue cue.mod/module.cue-0
18+
cmp x.cue x.cue-0
19+
cmp y/y.cue y/y.cue-0
20+
21+
-- cue.mod/module.cue --
22+
module: "main.org/x"
23+
language: {
24+
version: "v0.9.0"
25+
}
26+
-- x.cue --
27+
package x
28+
29+
x: 1
30+
-- y/y.cue --
31+
package y
32+
33+
import "main.org/x"
34+
35+
y: x.x
36+
-- cue.mod/module.cue-1 --
37+
module: "main.org/y"
38+
language: {
39+
version: "v0.9.0"
40+
}
41+
-- x.cue-1 --
42+
package x
43+
44+
x: 1
45+
-- y/y.cue-1 --
46+
package y
47+
48+
import "main.org/y:x"
49+
50+
y: x.x
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Test that cue mod rename does not rewrite an import
2+
# of a package with a matching path but in a different major
3+
# version of the module.
4+
5+
# First make sure everything is in canonical form.
6+
exec cue fmt ./...
7+
8+
# Make a copy of the original files.
9+
cp cue.mod/module.cue cue.mod/module.cue-0
10+
cp foo.cue foo.cue-0
11+
cp bar/bar.cue bar/bar.cue-0
12+
13+
exec cue mod rename other.org/bar
14+
cmp cue.mod/module.cue cue.mod/module.cue-1
15+
cmp foo.cue foo.cue-1
16+
cmp bar/bar.cue bar/bar.cue-1
17+
18+
# Renaming back to the original name should
19+
# result in no changes from the original files.
20+
exec cue mod rename main.org/foo@v0
21+
cmp cue.mod/module.cue cue.mod/module.cue-0
22+
cmp foo.cue foo.cue-0
23+
cmp bar/bar.cue bar/bar.cue-0
24+
25+
-- cue.mod/module.cue --
26+
module: "main.org/foo@v0"
27+
language: {
28+
version: "v0.9.0"
29+
}
30+
deps: {
31+
"main.org/foo@v1": {
32+
v: "v1.2.3"
33+
}
34+
}
35+
-- foo.cue --
36+
package foo
37+
38+
import "main.org/foo@v1"
39+
40+
x: foo.x
41+
-- bar/bar.cue --
42+
package bar
43+
44+
import "main.org/foo@v0"
45+
46+
foo.x
47+
48+
-- cue.mod/module.cue-1 --
49+
module: "other.org/bar"
50+
language: {
51+
version: "v0.9.0"
52+
}
53+
deps: {
54+
"main.org/foo@v1": {
55+
v: "v1.2.3"
56+
}
57+
}
58+
-- foo.cue-1 --
59+
package foo
60+
61+
import "main.org/foo@v1"
62+
63+
x: foo.x
64+
-- bar/bar.cue-1 --
65+
package bar
66+
67+
import "other.org/bar@v0:foo"
68+
69+
foo.x

0 commit comments

Comments
 (0)