Skip to content

Commit 7479e1b

Browse files
committed
internal/telemetry/cmd/stacks: test predicates
Factor out parsing and evaluation of #stacks predicates. Add a test. For golang/go#71045. Change-Id: I677e34e555a1f1ebb0722088d55e0c9edd3b3f40 Reviewed-on: https://go-review.googlesource.com/c/tools/+/643776 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 726ba32 commit 7479e1b

File tree

2 files changed

+125
-59
lines changed

2 files changed

+125
-59
lines changed

gopls/internal/telemetry/cmd/stacks/stacks.go

Lines changed: 75 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -404,81 +404,97 @@ func readIssues(pcfg ProgramConfig) ([]*Issue, error) {
404404
for _, issue := range issues {
405405
block := findPredicateBlock(issue.Body)
406406
if block != "" {
407-
expr, err := parser.ParseExpr(block)
407+
pred, err := parsePredicate(block)
408408
if err != nil {
409409
log.Printf("invalid predicate in issue #%d: %v\n<<%s>>",
410410
issue.Number, err, block)
411411
continue
412412
}
413-
var validate func(ast.Expr) error
414-
validate = func(e ast.Expr) error {
415-
switch e := e.(type) {
416-
case *ast.UnaryExpr:
417-
if e.Op != token.NOT {
418-
return fmt.Errorf("invalid op: %s", e.Op)
419-
}
420-
return validate(e.X)
421-
422-
case *ast.BinaryExpr:
423-
if e.Op != token.LAND && e.Op != token.LOR {
424-
return fmt.Errorf("invalid op: %s", e.Op)
425-
}
426-
if err := validate(e.X); err != nil {
427-
return err
428-
}
429-
return validate(e.Y)
413+
issue.predicateText = block
414+
issue.predicate = pred
415+
}
416+
}
430417

431-
case *ast.ParenExpr:
432-
return validate(e.X)
418+
return issues, nil
419+
}
433420

434-
case *ast.BasicLit:
435-
if e.Kind != token.STRING {
436-
return fmt.Errorf("invalid literal (%s)", e.Kind)
437-
}
438-
if _, err := strconv.Unquote(e.Value); err != nil {
439-
return err
440-
}
421+
// parsePredicate parses a predicate expression, returning a function that evaluates
422+
// the predicate on a stack.
423+
// The expression must match this grammar:
424+
//
425+
// expr = "string literal"
426+
// | ( expr )
427+
// | ! expr
428+
// | expr && expr
429+
// | expr || expr
430+
func parsePredicate(s string) (func(string) bool, error) {
431+
expr, err := parser.ParseExpr(s)
432+
if err != nil {
433+
return nil, fmt.Errorf("parse error: %w", err)
434+
}
435+
var validate func(ast.Expr) error
436+
validate = func(e ast.Expr) error {
437+
switch e := e.(type) {
438+
case *ast.UnaryExpr:
439+
if e.Op != token.NOT {
440+
return fmt.Errorf("invalid op: %s", e.Op)
441+
}
442+
return validate(e.X)
441443

442-
default:
443-
return fmt.Errorf("syntax error (%T)", e)
444-
}
445-
return nil
444+
case *ast.BinaryExpr:
445+
if e.Op != token.LAND && e.Op != token.LOR {
446+
return fmt.Errorf("invalid op: %s", e.Op)
446447
}
447-
if err := validate(expr); err != nil {
448-
log.Printf("invalid predicate in issue #%d: %v\n<<%s>>",
449-
issue.Number, err, block)
450-
continue
448+
if err := validate(e.X); err != nil {
449+
return err
451450
}
452-
issue.predicateText = block
453-
issue.predicate = func(stack string) bool {
454-
var eval func(ast.Expr) bool
455-
eval = func(e ast.Expr) bool {
456-
switch e := e.(type) {
457-
case *ast.UnaryExpr:
458-
return !eval(e.X)
459-
460-
case *ast.BinaryExpr:
461-
if e.Op == token.LAND {
462-
return eval(e.X) && eval(e.Y)
463-
} else {
464-
return eval(e.X) || eval(e.Y)
465-
}
451+
return validate(e.Y)
466452

467-
case *ast.ParenExpr:
468-
return eval(e.X)
453+
case *ast.ParenExpr:
454+
return validate(e.X)
469455

470-
case *ast.BasicLit:
471-
substr, _ := strconv.Unquote(e.Value)
472-
return strings.Contains(stack, substr)
473-
}
474-
panic("unreachable")
475-
}
476-
return eval(expr)
456+
case *ast.BasicLit:
457+
if e.Kind != token.STRING {
458+
return fmt.Errorf("invalid literal (%s)", e.Kind)
459+
}
460+
if _, err := strconv.Unquote(e.Value); err != nil {
461+
return err
477462
}
463+
464+
default:
465+
return fmt.Errorf("syntax error (%T)", e)
478466
}
467+
return nil
468+
}
469+
if err := validate(expr); err != nil {
470+
return nil, err
479471
}
480472

481-
return issues, nil
473+
return func(stack string) bool {
474+
var eval func(ast.Expr) bool
475+
eval = func(e ast.Expr) bool {
476+
switch e := e.(type) {
477+
case *ast.UnaryExpr:
478+
return !eval(e.X)
479+
480+
case *ast.BinaryExpr:
481+
if e.Op == token.LAND {
482+
return eval(e.X) && eval(e.Y)
483+
} else {
484+
return eval(e.X) || eval(e.Y)
485+
}
486+
487+
case *ast.ParenExpr:
488+
return eval(e.X)
489+
490+
case *ast.BasicLit:
491+
substr, _ := strconv.Unquote(e.Value)
492+
return strings.Contains(stack, substr)
493+
}
494+
panic("unreachable")
495+
}
496+
return eval(expr)
497+
}, nil
482498
}
483499

484500
// claimStack maps each stack ID to its issue (if any).

gopls/internal/telemetry/cmd/stacks/stacks_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,53 @@ func TestReadPCLineTable(t *testing.T) {
7575
})
7676
}
7777
}
78+
79+
func TestParsePredicate(t *testing.T) {
80+
for _, tc := range []struct {
81+
expr string
82+
arg string
83+
want bool
84+
}{
85+
{`"x"`, `"x"`, true},
86+
{`"x"`, `"axe"`, true}, // literals match by strings.Contains
87+
{`"x"`, `"y"`, false},
88+
{`!"x"`, "x", false},
89+
{`!"x"`, "y", true},
90+
{`"x" && "y"`, "xy", true},
91+
{`"x" && "y"`, "x", false},
92+
{`"x" && "y"`, "y", false},
93+
{`"xz" && "zy"`, "xzy", true}, // matches need not be disjoint
94+
{`"x" || "y"`, "xy", true},
95+
{`"x" || "y"`, "x", true},
96+
{`"x" || "y"`, "y", true},
97+
{`"x" || "y"`, "z", false},
98+
} {
99+
eval, err := parsePredicate(tc.expr)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
got := eval(tc.arg)
104+
if got != tc.want {
105+
t.Errorf("%s applied to %q: got %t, want %t", tc.expr, tc.arg, got, tc.want)
106+
}
107+
}
108+
}
109+
110+
func TestParsePredicateError(t *testing.T) {
111+
// Validate that bad predicates return errors.
112+
for _, expr := range []string{
113+
``,
114+
`1`,
115+
`foo`, // an identifier, not a literal
116+
`"x" + "y"`,
117+
`"x" &&`,
118+
`~"x"`,
119+
`f(1)`,
120+
} {
121+
if _, err := parsePredicate(expr); err == nil {
122+
t.Errorf("%s: got nil, want error", expr)
123+
} else {
124+
t.Logf("%s: %v", expr, err)
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)