Skip to content

Commit abf6a1d

Browse files
go/analysis/passes/structtag: recognize multiple keys per tag
As of Go 1.16 the reflect package now supports multiple keys per tag. For golang/go#40281 Fixes golang/go#43083 Change-Id: I55cdc35c857a5e73dc009c2842d7bd83c63d7712 Reviewed-on: https://go-review.googlesource.com/c/tools/+/277092 Trust: Ian Lance Taylor <[email protected]> Run-TryBot: Ian Lance Taylor <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Tim King <[email protected]>
1 parent 6d345e8 commit abf6a1d

File tree

3 files changed

+104
-38
lines changed

3 files changed

+104
-38
lines changed

go/analysis/passes/structtag/structtag.go

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,12 @@ var (
207207
)
208208

209209
// validateStructTag parses the struct tag and returns an error if it is not
210-
// in the canonical format, which is a space-separated list of key:"value"
211-
// settings. The value may contain spaces.
210+
// in the canonical format, as defined by reflect.StructTag.
212211
func validateStructTag(tag string) error {
213212
// This code is based on the StructTag.Get code in package reflect.
214213

215214
n := 0
215+
var keys []string
216216
for ; tag != ""; n++ {
217217
if n > 0 && tag != "" && tag[0] != ' ' {
218218
// More restrictive than reflect, but catches likely mistakes
@@ -240,14 +240,27 @@ func validateStructTag(tag string) error {
240240
if i == 0 {
241241
return errTagKeySyntax
242242
}
243-
if i+1 >= len(tag) || tag[i] != ':' {
243+
if i+1 >= len(tag) || tag[i] < ' ' || tag[i] == 0x7f {
244244
return errTagSyntax
245245
}
246-
if tag[i+1] != '"' {
246+
key := tag[:i]
247+
keys = append(keys, key)
248+
tag = tag[i:]
249+
250+
// If we found a space char here - assume that we have a tag with
251+
// multiple keys.
252+
if tag[0] == ' ' {
253+
continue
254+
}
255+
256+
// Spaces were filtered above so we assume that here we have
257+
// only valid tag value started with `:"`.
258+
if tag[0] != ':' || tag[1] != '"' {
247259
return errTagValueSyntax
248260
}
249-
key := tag[:i]
250-
tag = tag[i+1:]
261+
262+
// Remove the colon leaving tag at the start of the quoted string.
263+
tag = tag[1:]
251264

252265
// Scan quoted string to find value.
253266
i = 1
@@ -263,51 +276,56 @@ func validateStructTag(tag string) error {
263276
qvalue := tag[:i+1]
264277
tag = tag[i+1:]
265278

266-
value, err := strconv.Unquote(qvalue)
279+
wholeValue, err := strconv.Unquote(qvalue)
267280
if err != nil {
268281
return errTagValueSyntax
269282
}
270283

271-
if !checkTagSpaces[key] {
272-
continue
273-
}
274-
275-
switch key {
276-
case "xml":
277-
// If the first or last character in the XML tag is a space, it is
278-
// suspicious.
279-
if strings.Trim(value, " ") != value {
280-
return errTagValueSpace
284+
for _, key := range keys {
285+
if !checkTagSpaces[key] {
286+
continue
281287
}
282288

283-
// If there are multiple spaces, they are suspicious.
284-
if strings.Count(value, " ") > 1 {
285-
return errTagValueSpace
286-
}
289+
value := wholeValue
290+
switch key {
291+
case "xml":
292+
// If the first or last character in the XML tag is a space, it is
293+
// suspicious.
294+
if strings.Trim(value, " ") != value {
295+
return errTagValueSpace
296+
}
287297

288-
// If there is no comma, skip the rest of the checks.
289-
comma := strings.IndexRune(value, ',')
290-
if comma < 0 {
291-
continue
298+
// If there are multiple spaces, they are suspicious.
299+
if strings.Count(value, " ") > 1 {
300+
return errTagValueSpace
301+
}
302+
303+
// If there is no comma, skip the rest of the checks.
304+
comma := strings.IndexRune(value, ',')
305+
if comma < 0 {
306+
continue
307+
}
308+
309+
// If the character before a comma is a space, this is suspicious.
310+
if comma > 0 && value[comma-1] == ' ' {
311+
return errTagValueSpace
312+
}
313+
value = value[comma+1:]
314+
case "json":
315+
// JSON allows using spaces in the name, so skip it.
316+
comma := strings.IndexRune(value, ',')
317+
if comma < 0 {
318+
continue
319+
}
320+
value = value[comma+1:]
292321
}
293322

294-
// If the character before a comma is a space, this is suspicious.
295-
if comma > 0 && value[comma-1] == ' ' {
323+
if strings.IndexByte(value, ' ') >= 0 {
296324
return errTagValueSpace
297325
}
298-
value = value[comma+1:]
299-
case "json":
300-
// JSON allows using spaces in the name, so skip it.
301-
comma := strings.IndexRune(value, ',')
302-
if comma < 0 {
303-
continue
304-
}
305-
value = value[comma+1:]
306326
}
307327

308-
if strings.IndexByte(value, ' ') >= 0 {
309-
return errTagValueSpace
310-
}
328+
keys = keys[:0]
311329
}
312330
return nil
313331
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build go1.16
6+
7+
package structtag_test
8+
9+
import (
10+
"testing"
11+
12+
"golang.org/x/tools/go/analysis/analysistest"
13+
"golang.org/x/tools/go/analysis/passes/structtag"
14+
)
15+
16+
// Test the multiple key format added in Go 1.16.
17+
18+
func TestGo16(t *testing.T) {
19+
testdata := analysistest.TestData()
20+
analysistest.Run(t, testdata, structtag.Analyzer, "go16")
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Tests for the new multiple-key struct tag format supported in 1.16.
6+
7+
package go16
8+
9+
type Go16StructTagTest struct {
10+
OK int `multiple keys can:"share a value"`
11+
OK2 int `json bson xml form:"field_1,omitempty" other:"value"`
12+
}
13+
14+
type Go16UnexportedEncodingTagTest struct {
15+
F int `json xml:"ff"`
16+
17+
// We currently always check json first, and return after an error.
18+
f1 int `json xml:"f1"` // want "struct field f1 has json tag but is not exported"
19+
f2 int `xml json:"f2"` // want "struct field f2 has json tag but is not exported"
20+
f3 int `xml bson:"f3"` // want "struct field f3 has xml tag but is not exported"
21+
f4 int `bson xml:"f4"` // want "struct field f4 has xml tag but is not exported"
22+
}
23+
24+
type Go16DuplicateFields struct {
25+
JSONXML int `json xml:"c"`
26+
DuplicateJSONXML int `json xml:"c"` // want "struct field DuplicateJSONXML repeats json tag .c. also at go16.go:25" "struct field DuplicateJSONXML repeats xml tag .c. also at go16.go:25"
27+
}

0 commit comments

Comments
 (0)