Skip to content

Commit 1115af6

Browse files
adonovangopherbot
authored andcommitted
internal/expect: support named arguments f(a, b, c=d, e="f")
+ test Change-Id: I6c44e32d34bcdf1ca68e8989e99595f691c88329 Reviewed-on: https://go-review.googlesource.com/c/tools/+/626016 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent 0b9e499 commit 1115af6

File tree

4 files changed

+85
-45
lines changed

4 files changed

+85
-45
lines changed

internal/expect/expect.go

+9-11
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ comma-separated list of arguments.
4242
The empty parameter list and the missing parameter list are distinguishable if
4343
needed; they result in a nil or an empty list in the Args parameter respectively.
4444
45-
Arguments are either identifiers or literals.
45+
Arguments may be positional, such as f(value), or named, such as f(name=value).
46+
Positional arguments must appear before named arguments.
47+
Names may not be repeated.
48+
49+
Argument values may be either identifiers or literals.
4650
The literals supported are the basic value literals, of string, float, integer
4751
true, false or nil. All the literals match the standard go conventions, with
4852
all bases of integers, and both quote and backtick strings.
@@ -62,9 +66,10 @@ import (
6266
// It knows the position of the start of the comment, and the name and
6367
// arguments that make up the note.
6468
type Note struct {
65-
Pos token.Pos // The position at which the note identifier appears
66-
Name string // the name associated with the note
67-
Args []interface{} // the arguments for the note
69+
Pos token.Pos // The position at which the note identifier appears
70+
Name string // the name associated with the note
71+
Args []any // positional arguments (non-nil if parens were present)
72+
NamedArgs map[string]any // named arguments (or nil if none)
6873
}
6974

7075
// ReadFile is the type of a function that can provide file contents for a
@@ -116,10 +121,3 @@ func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern
116121
}
117122
return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil
118123
}
119-
120-
func lineEnd(f *token.File, line int) token.Pos {
121-
if line >= f.LineCount() {
122-
return token.Pos(f.Base() + f.Size())
123-
}
124-
return f.LineStart(line + 1)
125-
}

internal/expect/expect_test.go

+27-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"bytes"
99
"go/token"
1010
"os"
11+
"reflect"
12+
"slices"
1113
"testing"
1214

1315
"golang.org/x/tools/internal/expect"
@@ -18,11 +20,13 @@ func TestMarker(t *testing.T) {
1820
filename string
1921
expectNotes int
2022
expectMarkers map[string]string
21-
expectChecks map[string][]interface{}
23+
expectChecks map[string][]any
24+
// expectChecks holds {"id": values} for each call check(id, values...).
25+
// Any named k=v arguments become a final map[string]any argument.
2226
}{
2327
{
2428
filename: "testdata/test.go",
25-
expectNotes: 13,
29+
expectNotes: 14,
2630
expectMarkers: map[string]string{
2731
"αSimpleMarker": "α",
2832
"OffsetMarker": "β",
@@ -36,10 +40,15 @@ func TestMarker(t *testing.T) {
3640
"NonIdentifier": "+",
3741
"StringMarker": "\"hello\"",
3842
},
39-
expectChecks: map[string][]interface{}{
43+
expectChecks: map[string][]any{
4044
"αSimpleMarker": nil,
4145
"StringAndInt": {"Number %d", int64(12)},
4246
"Bool": {true},
47+
"NamedArgs": {int64(1), true, expect.Identifier("a"), map[string]any{
48+
"b": int64(1),
49+
"c": "3",
50+
"d": true,
51+
}},
4352
},
4453
},
4554
{
@@ -79,7 +88,7 @@ func TestMarker(t *testing.T) {
7988
fset := token.NewFileSet()
8089
notes, err := expect.Parse(fset, tt.filename, content)
8190
if err != nil {
82-
t.Fatalf("Failed to extract notes: %v", err)
91+
t.Fatalf("Failed to extract notes:\n%v", err)
8392
}
8493
if len(notes) != tt.expectNotes {
8594
t.Errorf("Expected %v notes, got %v", tt.expectNotes, len(notes))
@@ -99,7 +108,7 @@ func TestMarker(t *testing.T) {
99108
}
100109
ident, ok := n.Args[0].(expect.Identifier)
101110
if !ok {
102-
t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
111+
t.Errorf("%v: got %v (%T), want identifier", fset.Position(n.Pos), n.Args[0], n.Args[0])
103112
continue
104113
}
105114
checkMarker(t, fset, readFile, markers, n.Pos, string(ident), n.Args[1])
@@ -115,21 +124,27 @@ func TestMarker(t *testing.T) {
115124
}
116125
ident, ok := n.Args[0].(expect.Identifier)
117126
if !ok {
118-
t.Errorf("%v: identifier, got %T", fset.Position(n.Pos), n.Args[0])
127+
t.Errorf("%v: got %v (%T), want identifier", fset.Position(n.Pos), n.Args[0], n.Args[0])
119128
continue
120129
}
121-
args, ok := tt.expectChecks[string(ident)]
130+
wantArgs, ok := tt.expectChecks[string(ident)]
122131
if !ok {
123132
t.Errorf("%v: unexpected check %v", fset.Position(n.Pos), ident)
124133
continue
125134
}
126-
if len(n.Args) != len(args)+1 {
127-
t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(args)+1, len(n.Args))
135+
gotArgs := n.Args[1:]
136+
if n.NamedArgs != nil {
137+
// Clip to avoid mutating Args' array.
138+
gotArgs = append(slices.Clip(gotArgs), n.NamedArgs)
139+
}
140+
141+
if len(gotArgs) != len(wantArgs) {
142+
t.Errorf("%v: expected %v args to check, got %v", fset.Position(n.Pos), len(wantArgs), len(gotArgs))
128143
continue
129144
}
130-
for i, got := range n.Args[1:] {
131-
if args[i] != got {
132-
t.Errorf("%v: arg %d expected %v, got %v", fset.Position(n.Pos), i, args[i], got)
145+
for i := range gotArgs {
146+
if !reflect.DeepEqual(wantArgs[i], gotArgs[i]) {
147+
t.Errorf("%v: arg %d: expected %#v, got %#v", fset.Position(n.Pos), i+1, wantArgs[i], gotArgs[i])
133148
}
134149
}
135150
default:

internal/expect/extract.go

+47-22
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func parse(fset *token.FileSet, base token.Pos, text string) ([]*Note, error) {
231231
t := new(tokens).Init(base, text)
232232
notes := parseComment(t)
233233
if t.err != nil {
234-
return nil, fmt.Errorf("%v:%s", fset.Position(t.Pos()), t.err)
234+
return nil, fmt.Errorf("%v: %s", fset.Position(t.Pos()), t.err)
235235
}
236236
return notes, nil
237237
}
@@ -272,20 +272,38 @@ func parseNote(t *tokens) *Note {
272272
// no argument list present
273273
return n
274274
case '(':
275-
n.Args = parseArgumentList(t)
275+
n.Args, n.NamedArgs = parseArgumentList(t)
276276
return n
277277
default:
278278
t.Errorf("unexpected %s parsing note", t.TokenString())
279279
return nil
280280
}
281281
}
282282

283-
func parseArgumentList(t *tokens) []interface{} {
284-
args := []interface{}{} // @name() is represented by a non-nil empty slice.
285-
t.Consume() // '('
283+
func parseArgumentList(t *tokens) (args []any, named map[string]any) {
284+
args = []any{} // @name() is represented by a non-nil empty slice.
285+
t.Consume() // '('
286286
t.Skip('\n')
287287
for t.Token() != ')' {
288-
args = append(args, parseArgument(t))
288+
name, arg := parseArgument(t)
289+
if name != "" {
290+
// f(k=v)
291+
if named == nil {
292+
named = make(map[string]any)
293+
}
294+
if _, dup := named[name]; dup {
295+
t.Errorf("duplicate named argument %q", name)
296+
return nil, nil
297+
}
298+
named[name] = arg
299+
} else {
300+
// f(v)
301+
if named != nil {
302+
t.Errorf("positional argument follows named argument")
303+
return nil, nil
304+
}
305+
args = append(args, arg)
306+
}
289307
if t.Token() != ',' {
290308
break
291309
}
@@ -294,65 +312,72 @@ func parseArgumentList(t *tokens) []interface{} {
294312
}
295313
if t.Token() != ')' {
296314
t.Errorf("unexpected %s parsing argument list", t.TokenString())
297-
return nil
315+
return nil, nil
298316
}
299317
t.Consume() // ')'
300-
return args
318+
return args, named
301319
}
302320

303-
func parseArgument(t *tokens) interface{} {
321+
// parseArgument returns the value of the argument ("f(value)"),
322+
// and its name if named "f(name=value)".
323+
func parseArgument(t *tokens) (name string, value any) {
324+
again:
304325
switch t.Token() {
305326
case scanner.Ident:
306327
v := t.Consume()
307328
switch v {
308329
case "true":
309-
return true
330+
value = true
310331
case "false":
311-
return false
332+
value = false
312333
case "nil":
313-
return nil
334+
value = nil
314335
case "re":
315336
if t.Token() != scanner.String && t.Token() != scanner.RawString {
316337
t.Errorf("re must be followed by string, got %s", t.TokenString())
317-
return nil
338+
return
318339
}
319340
pattern, _ := strconv.Unquote(t.Consume()) // can't fail
320341
re, err := regexp.Compile(pattern)
321342
if err != nil {
322343
t.Errorf("invalid regular expression %s: %v", pattern, err)
323-
return nil
344+
return
324345
}
325-
return re
346+
value = re
326347
default:
327-
return Identifier(v)
348+
// f(name=value)?
349+
if name == "" && t.Token() == '=' {
350+
t.Consume() // '='
351+
name = v
352+
goto again
353+
}
354+
value = Identifier(v)
328355
}
329356

330357
case scanner.String, scanner.RawString:
331-
v, _ := strconv.Unquote(t.Consume()) // can't fail
332-
return v
358+
value, _ = strconv.Unquote(t.Consume()) // can't fail
333359

334360
case scanner.Int:
335361
s := t.Consume()
336362
v, err := strconv.ParseInt(s, 0, 0)
337363
if err != nil {
338364
t.Errorf("cannot convert %v to int: %v", s, err)
339365
}
340-
return v
366+
value = v
341367

342368
case scanner.Float:
343369
s := t.Consume()
344370
v, err := strconv.ParseFloat(s, 64)
345371
if err != nil {
346372
t.Errorf("cannot convert %v to float: %v", s, err)
347373
}
348-
return v
374+
value = v
349375

350376
case scanner.Char:
351377
t.Errorf("unexpected char literal %s", t.Consume())
352-
return nil
353378

354379
default:
355380
t.Errorf("unexpected %s parsing argument", t.TokenString())
356-
return nil
357381
}
382+
return
358383
}

internal/expect/testdata/test.go

+2
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ check(StringAndInt,
3737
)
3838
3939
check(Bool, true)
40+
41+
check(NamedArgs, 1, true, a, b=1, c="3", d=true)
4042
*/

0 commit comments

Comments
 (0)