Skip to content

Commit d346382

Browse files
committed
gopls/internal/server: add modify_tags command handler
Previously, the VSCode extension installed and executed gomodifytags directly. To reduce third-party dependencies in gopls, we are submitting a change to gomodifytags to extract the functionality into a library called by gopls (fatih/gomodifytags#117). Now, the extension will call the gopls.modify_tags command and the command handler will invoke the modifytags package's Apply() method. Also adds basic code actions for adding and removing struct tags. This will be extended once we implement a dialogue feature in gopls. Change-Id: Idf130c95eec5d469a454cb6f21897629b3364b06 Reviewed-on: https://go-review.googlesource.com/c/tools/+/652495 Reviewed-by: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 0c2f68a commit d346382

File tree

12 files changed

+607
-2
lines changed

12 files changed

+607
-2
lines changed

gopls/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ require (
2020

2121
require (
2222
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
23+
github.com/fatih/camelcase v1.0.0 // indirect
24+
github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c // indirect
25+
github.com/fatih/structtag v1.2.0 // indirect
2326
github.com/google/safehtml v0.1.0 // indirect
2427
golang.org/x/exp/typeparams v0.0.0-20250218142911-aa4b98e5adaa // indirect
2528
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

gopls/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
22
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
3+
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
4+
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
5+
github.com/fatih/gomodifytags v1.16.0 h1:B65npXIXSk44F6c1hZGE1NazSnt+eXvtdEOG2Uy+QdU=
6+
github.com/fatih/gomodifytags v1.16.0/go.mod h1:TbUyEjH1Zo0GkJd2Q52oVYqYcJ0eGNqG8bsiOb75P9c=
7+
github.com/fatih/gomodifytags v1.17.1-0.20250325171527-8c663b1c0765 h1:T+oCz1SRpqkn4meT0PiAX5vM8HcESrWvsAzyvy0Vdh0=
8+
github.com/fatih/gomodifytags v1.17.1-0.20250325171527-8c663b1c0765/go.mod h1:YVLagR57bBxMai8IAEc7V4E/MWUYi0oUutLrZcTcnI8=
9+
github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c h1:dDSgAjoOMp8da3egfz0t2S+t8RGOpEmEXZubcGuc0Bg=
10+
github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c/go.mod h1:YVLagR57bBxMai8IAEc7V4E/MWUYi0oUutLrZcTcnI8=
11+
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
12+
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
313
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
414
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
515
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

gopls/internal/golang/codeaction.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"golang.org/x/tools/gopls/internal/protocol"
2828
"golang.org/x/tools/gopls/internal/protocol/command"
2929
"golang.org/x/tools/gopls/internal/settings"
30+
"golang.org/x/tools/internal/astutil/cursor"
3031
"golang.org/x/tools/internal/event"
3132
"golang.org/x/tools/internal/imports"
3233
"golang.org/x/tools/internal/typesinternal"
@@ -262,6 +263,8 @@ var codeActionProducers = [...]codeActionProducer{
262263
{kind: settings.RefactorRewriteMoveParamRight, fn: refactorRewriteMoveParamRight, needPkg: true},
263264
{kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true},
264265
{kind: settings.RefactorRewriteEliminateDotImport, fn: refactorRewriteEliminateDotImport, needPkg: true},
266+
{kind: settings.RefactorRewriteAddTags, fn: refactorRewriteAddStructTags, needPkg: true},
267+
{kind: settings.RefactorRewriteRemoveTags, fn: refactorRewriteRemoveStructTags, needPkg: true},
265268
{kind: settings.GoplsDocFeatures, fn: goplsDocFeatures}, // offer this one last (#72742)
266269

267270
// Note: don't forget to update the allow-list in Server.CodeAction
@@ -810,6 +813,82 @@ func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) err
810813
return nil
811814
}
812815

816+
// selectionContainsStructField returns true if the given struct contains a
817+
// field between start and end pos. If needsTag is true, it only returns true if
818+
// the struct field found contains a struct tag.
819+
func selectionContainsStructField(node *ast.StructType, start, end token.Pos, needsTag bool) bool {
820+
for _, field := range node.Fields.List {
821+
if start <= field.End() && end >= field.Pos() {
822+
if !needsTag || field.Tag != nil {
823+
return true
824+
}
825+
}
826+
}
827+
return false
828+
}
829+
830+
// selectionContainsStruct returns true if there exists a struct containing
831+
// fields within start and end positions. If removeTags is true, it means the
832+
// current command is for remove tags rather than add tags, so we only return
833+
// true if the struct field found contains a struct tag to remove.
834+
func selectionContainsStruct(cursor cursor.Cursor, start, end token.Pos, removeTags bool) bool {
835+
cur, ok := cursor.FindByPos(start, end)
836+
if !ok {
837+
return false
838+
}
839+
if _, ok := cur.Node().(*ast.StructType); ok {
840+
return true
841+
}
842+
843+
// Handles case where selection is within struct.
844+
for c := range cur.Enclosing((*ast.StructType)(nil)) {
845+
if selectionContainsStructField(c.Node().(*ast.StructType), start, end, removeTags) {
846+
return true
847+
}
848+
}
849+
850+
// Handles case where selection contains struct but may contain other nodes, including other structs.
851+
for c := range cur.Preorder((*ast.StructType)(nil)) {
852+
node := c.Node().(*ast.StructType)
853+
// Check that at least one field is located within the selection. If we are removing tags, that field
854+
// must also have a struct tag, otherwise we do not provide the code action.
855+
if selectionContainsStructField(node, start, end, removeTags) {
856+
return true
857+
}
858+
}
859+
return false
860+
}
861+
862+
// refactorRewriteAddStructTags produces "Add struct tags" code actions.
863+
// See [server.commandHandler.ModifyTags] for command implementation.
864+
func refactorRewriteAddStructTags(ctx context.Context, req *codeActionsRequest) error {
865+
if selectionContainsStruct(req.pgf.Cursor, req.start, req.end, false) {
866+
// TODO(mkalil): Prompt user for modification args once we have dialogue capabilities.
867+
cmdAdd := command.NewModifyTagsCommand("Add struct tags", command.ModifyTagsArgs{
868+
URI: req.loc.URI,
869+
Range: req.loc.Range,
870+
Add: "json",
871+
})
872+
req.addCommandAction(cmdAdd, false)
873+
}
874+
return nil
875+
}
876+
877+
// refactorRewriteRemoveStructTags produces "Remove struct tags" code actions.
878+
// See [server.commandHandler.ModifyTags] for command implementation.
879+
func refactorRewriteRemoveStructTags(ctx context.Context, req *codeActionsRequest) error {
880+
// TODO(mkalil): Prompt user for modification args once we have dialogue capabilities.
881+
if selectionContainsStruct(req.pgf.Cursor, req.start, req.end, true) {
882+
cmdRemove := command.NewModifyTagsCommand("Remove struct tags", command.ModifyTagsArgs{
883+
URI: req.loc.URI,
884+
Range: req.loc.Range,
885+
Clear: true,
886+
})
887+
req.addCommandAction(cmdRemove, false)
888+
}
889+
return nil
890+
}
891+
813892
// removableParameter returns paramInfo about a removable parameter indicated
814893
// by the given [start, end) range, or nil if no such removal is available.
815894
//

gopls/internal/licenses/gen-licenses.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ END
2525
# are known to have the same license.
2626
mods=$(go list -deps -f '{{with .Module}}{{.Path}}{{end}}' golang.org/x/tools/gopls | sort -u | grep -v golang.org)
2727
for mod in $mods; do
28-
# Find the license file, either LICENSE or COPYING, and add it to the result.
28+
# Find the license file, either LICENSE, COPYING, or LICENSE.md and add it to the result.
2929
dir=$(go list -m -f {{.Dir}} $mod)
30-
license=$(ls -1 $dir | grep -E -i '^(LICENSE|COPYING)$')
30+
license=$(ls -1 $dir | grep -E -i '^(LICENSE|LICENSE.md|COPYING)?$')
3131
echo "-- $mod $license --" >> $tempfile
3232
echo >> $tempfile
3333
sed 's/^-- / &/' $dir/$license >> $tempfile

gopls/internal/licenses/licenses.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,122 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3030
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
3131
THE SOFTWARE.
3232
33+
-- github.com/fatih/camelcase LICENSE.md --
34+
35+
The MIT License (MIT)
36+
37+
Copyright (c) 2015 Fatih Arslan
38+
39+
Permission is hereby granted, free of charge, to any person obtaining a copy of
40+
this software and associated documentation files (the "Software"), to deal in
41+
the Software without restriction, including without limitation the rights to
42+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
43+
the Software, and to permit persons to whom the Software is furnished to do so,
44+
subject to the following conditions:
45+
46+
The above copyright notice and this permission notice shall be included in all
47+
copies or substantial portions of the Software.
48+
49+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
51+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
52+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
53+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
54+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
55+
56+
-- github.com/fatih/gomodifytags LICENSE --
57+
58+
Copyright (c) 2017, Fatih Arslan
59+
All rights reserved.
60+
61+
Redistribution and use in source and binary forms, with or without
62+
modification, are permitted provided that the following conditions are met:
63+
64+
* Redistributions of source code must retain the above copyright notice, this
65+
list of conditions and the following disclaimer.
66+
67+
* Redistributions in binary form must reproduce the above copyright notice,
68+
this list of conditions and the following disclaimer in the documentation
69+
and/or other materials provided with the distribution.
70+
71+
* Neither the name of gomodifytags nor the names of its
72+
contributors may be used to endorse or promote products derived from
73+
this software without specific prior written permission.
74+
75+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
76+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
77+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
78+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
79+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
80+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
81+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
82+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
83+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
84+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
85+
86+
-- github.com/fatih/structtag LICENSE --
87+
88+
Copyright (c) 2017, Fatih Arslan
89+
All rights reserved.
90+
91+
Redistribution and use in source and binary forms, with or without
92+
modification, are permitted provided that the following conditions are met:
93+
94+
* Redistributions of source code must retain the above copyright notice, this
95+
list of conditions and the following disclaimer.
96+
97+
* Redistributions in binary form must reproduce the above copyright notice,
98+
this list of conditions and the following disclaimer in the documentation
99+
and/or other materials provided with the distribution.
100+
101+
* Neither the name of structtag nor the names of its
102+
contributors may be used to endorse or promote products derived from
103+
this software without specific prior written permission.
104+
105+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
106+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
107+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
108+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
109+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
110+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
111+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
112+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
113+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
114+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
115+
116+
This software includes some portions from Go. Go is used under the terms of the
117+
BSD like license.
118+
119+
Copyright (c) 2012 The Go Authors. All rights reserved.
120+
121+
Redistribution and use in source and binary forms, with or without
122+
modification, are permitted provided that the following conditions are
123+
met:
124+
125+
* Redistributions of source code must retain the above copyright
126+
notice, this list of conditions and the following disclaimer.
127+
* Redistributions in binary form must reproduce the above
128+
copyright notice, this list of conditions and the following disclaimer
129+
in the documentation and/or other materials provided with the
130+
distribution.
131+
* Neither the name of Google Inc. nor the names of its
132+
contributors may be used to endorse or promote products derived from
133+
this software without specific prior written permission.
134+
135+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
136+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
137+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
138+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
139+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
140+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
141+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
142+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
143+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
144+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
145+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
146+
147+
The Go gopher was designed by Renee French. http://reneefrench.blogspot.com/ The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: https://blog.golang.org/gopher
148+
33149
-- github.com/google/go-cmp LICENSE --
34150
35151
Copyright (c) 2017 The Go Authors. All rights reserved.

gopls/internal/protocol/command/command_gen.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/protocol/command/interface.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ type Interface interface {
297297

298298
// PackageSymbols: Return information about symbols in the given file's package.
299299
PackageSymbols(context.Context, PackageSymbolsArgs) (PackageSymbolsResult, error)
300+
301+
// ModifyTags: Add or remove struct tags on a given node.
302+
ModifyTags(context.Context, ModifyTagsArgs) error
300303
}
301304

302305
type RunTestsArgs struct {
@@ -830,3 +833,19 @@ type PackageSymbol struct {
830833
// Index of this symbol's file in PackageSymbolsResult.Files
831834
File int `json:"file,omitempty"`
832835
}
836+
837+
// ModifyTagsArgs holds variables that determine how struct tags are modified.
838+
type ModifyTagsArgs struct {
839+
URI protocol.DocumentURI // uri of the file to be modified
840+
Range protocol.Range // range in the file for where to modify struct tags
841+
Add string // comma-separated list of tags to add; i.e. "json,xml"
842+
AddOptions string // comma-separated list of options to add, per tag; i.e. "json=omitempty"
843+
Remove string // comma-separated list of tags to remove
844+
RemoveOptions string // comma-separated list of options to remove
845+
Clear bool // if set, clear all tags. tags are cleared before any new tags are added
846+
ClearOptions bool // if set, clear all tag options; options are cleared before any new options are added
847+
Overwrite bool // if set, replace existing tags when adding
848+
SkipUnexportedFields bool // if set, do not modify tags on unexported struct fields
849+
Transform string // transform rule for adding tags; i.e. "snakecase"
850+
ValueFormat string // format for the tag's value, after transformation; for example "column:{field}"
851+
}

0 commit comments

Comments
 (0)