Skip to content

Commit

Permalink
internal/telemetry/cmd/stacks: anchored literals
Browse files Browse the repository at this point in the history
Literals in predicates are re-interpreted as matching at word boundaries.
A literal like "fu+12" will no longer match "fu+123" or "snafu+12".

For golang/go#71045.

Change-Id: Id5b6c8ad536dadebdb9593cbfa13ff8dd81b6645
Reviewed-on: https://go-review.googlesource.com/c/tools/+/643835
Reviewed-by: Alan Donovan <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
jba committed Jan 22, 2025
1 parent 684910f commit fcc9d81
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 9 deletions.
3 changes: 2 additions & 1 deletion gopls/internal/doc/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
"EnumValues": null,
"Default": "[]",
"Status": "",
"Hierarchy": "build"
"Hierarchy": "build",
"DeprecationMessage": ""
},
{
"Name": "hoverKind",
Expand Down
26 changes: 22 additions & 4 deletions gopls/internal/telemetry/cmd/stacks/stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
// > | expr && expr
// > | expr || expr
//
// Each string literal implies a substring match on the stack;
// Each string literal must match complete words on the stack;
// the other productions are boolean operations.
// As an example of literal matching, "fu+12" matches "x:fu+12 "
// but not "fu:123" or "snafu+12".
//
// The stacks command gathers all such predicates out of the
// labelled issues and evaluates each one against each new stack.
Expand Down Expand Up @@ -76,6 +78,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
Expand Down Expand Up @@ -424,11 +427,21 @@ func readIssues(pcfg ProgramConfig) ([]*Issue, error) {
// | ! expr
// | expr && expr
// | expr || expr
//
// The value of a string literal is whether it is a substring of the stack, respecting word boundaries.
// That is, a literal L behaves like the regular expression \bL'\b, where L' is L with
// regexp metacharacters quoted.
func parsePredicate(s string) (func(string) bool, error) {
expr, err := parser.ParseExpr(s)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}

// Cache compiled regexps since we need them more than once.
literalRegexps := make(map[*ast.BasicLit]*regexp.Regexp)

// Check for errors in the predicate so we can report them now,
// ensuring that evaluation is error-free.
var validate func(ast.Expr) error
validate = func(e ast.Expr) error {
switch e := e.(type) {
Expand All @@ -454,9 +467,15 @@ func parsePredicate(s string) (func(string) bool, error) {
if e.Kind != token.STRING {
return fmt.Errorf("invalid literal (%s)", e.Kind)
}
if _, err := strconv.Unquote(e.Value); err != nil {
lit, err := strconv.Unquote(e.Value)
if err != nil {
return err
}
// The literal should match complete words. It may match multiple words,
// if it contains non-word runes like whitespace; but it must match word
// boundaries at each end.
// The constructed regular expression is always valid.
literalRegexps[e] = regexp.MustCompile(`\b` + regexp.QuoteMeta(lit) + `\b`)

default:
return fmt.Errorf("syntax error (%T)", e)
Expand Down Expand Up @@ -485,8 +504,7 @@ func parsePredicate(s string) (func(string) bool, error) {
return eval(e.X)

case *ast.BasicLit:
substr, _ := strconv.Unquote(e.Value)
return strings.Contains(stack, substr)
return literalRegexps[e].MatchString(stack)
}
panic("unreachable")
}
Expand Down
16 changes: 12 additions & 4 deletions gopls/internal/telemetry/cmd/stacks/stacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,23 @@ func TestParsePredicate(t *testing.T) {
want bool
}{
{`"x"`, `"x"`, true},
{`"x"`, `"axe"`, true}, // literals match by strings.Contains
{`"x"`, `"axe"`, false}, // literals match whole words
{`"x"`, "val:x+5", true},
{`"fu+12"`, "x:fu+12,", true},
{`"fu+12"`, "snafu+12,", false},
{`"fu+12"`, "x:fu+123,", false},
{`"a.*b"`, "a.*b", true}, // regexp metachars are escaped
{`"a.*b"`, "axxb", false}, // ditto
{`"x"`, `"y"`, false},
{`!"x"`, "x", false},
{`!"x"`, "y", true},
{`"x" && "y"`, "xy", true},
{`"x" && "y"`, "xy", false},
{`"x" && "y"`, "x y", true},
{`"x" && "y"`, "x", false},
{`"x" && "y"`, "y", false},
{`"xz" && "zy"`, "xzy", true}, // matches need not be disjoint
{`"x" || "y"`, "xy", true},
{`"xz" && "zy"`, "xzy", false},
{`"xz" && "zy"`, "zy,xz", true},
{`"x" || "y"`, "x\ny", true},
{`"x" || "y"`, "x", true},
{`"x" || "y"`, "y", true},
{`"x" || "y"`, "z", false},
Expand Down

0 comments on commit fcc9d81

Please sign in to comment.