github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/expr/expr.go (about)

     1  // Copyright 2017 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  // Package expr provides support for evaluating boolean expressions.
     6  package expr
     7  
     8  import (
     9  	"fmt"
    10  	"go/ast"
    11  	"go/parser"
    12  	"go/token"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  )
    17  
    18  // Expr holds a parsed boolean expression that matches some combination of attributes.
    19  //
    20  // Expressions are supplied as strings consisting of the following tokens:
    21  //
    22  //   - Attributes, either as bare identifiers (only if compliant with
    23  //     https://golang.org/ref/spec#Identifiers) or as double-quoted strings
    24  //     (in which '*' characters are interpreted as wildcards)
    25  //   - Binary operators: && (and), || (or)
    26  //   - Unary operator: ! (not)
    27  //   - Grouping: (, )
    28  //
    29  // The expression syntax is conveniently a subset of Go's syntax, so Go's parser and ast
    30  // packages are used to convert the initial expression into a binary expression tree.
    31  //
    32  // After an Expr object is created from a string expression, it can be asked if
    33  // it is satisfied by a supplied set of attributes.
    34  type Expr struct {
    35  	root ast.Expr
    36  }
    37  
    38  // exprValidator is used to validate that a parsed Go expression is a
    39  // valid boolean expression. It implements the ast.Visitor interface.
    40  type exprValidator struct {
    41  	err error
    42  }
    43  
    44  // setErr stores a formatted error in ev's err field (if not already set) and
    45  // returns nil to instruct ast.Walk to stop walking the expression.
    46  func (ev *exprValidator) setErr(format string, args ...interface{}) ast.Visitor {
    47  	if ev.err == nil {
    48  		ev.err = fmt.Errorf(format, args...)
    49  	}
    50  	return nil
    51  }
    52  
    53  // Visit returns itself if n is a valid node in a boolean expression or sets
    54  // ev's err field and returns nil if it isn't.
    55  func (ev *exprValidator) Visit(n ast.Node) ast.Visitor {
    56  	// ast.Walk calls Visit(nil) after visiting non-nil children.
    57  	if n == nil {
    58  		return nil
    59  	}
    60  
    61  	switch v := n.(type) {
    62  	case *ast.BinaryExpr:
    63  		if v.Op != token.LAND && v.Op != token.LOR {
    64  			return ev.setErr("invalid binary operator %q", v.Op)
    65  		}
    66  	case *ast.ParenExpr:
    67  	case *ast.UnaryExpr:
    68  		if v.Op != token.NOT {
    69  			return ev.setErr("invalid unary operator %q", v.Op)
    70  		}
    71  	case *ast.Ident:
    72  	case *ast.BasicLit:
    73  		if v.Kind != token.STRING {
    74  			return ev.setErr("non-string literal %q", v.Value)
    75  		}
    76  	default:
    77  		return ev.setErr("invalid node of type %T", v)
    78  	}
    79  	return ev
    80  }
    81  
    82  // New parses and validates boolean expression s, returning an Expr object
    83  // that can be used to test whether the expression is satisfied by different
    84  // sets of attributes.
    85  func New(s string) (*Expr, error) {
    86  	root, err := parser.ParseExpr(s)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	v := exprValidator{}
    92  	ast.Walk(&v, root)
    93  	return &Expr{root}, v.err
    94  }
    95  
    96  // Matches returns true if the expression is satisfied by attributes attrs.
    97  func (e *Expr) Matches(attrs []string) bool {
    98  	am := make(map[string]struct{}, len(attrs))
    99  	for _, a := range attrs {
   100  		am[a] = struct{}{}
   101  	}
   102  	return exprTrue(e.root, am)
   103  }
   104  
   105  // exprTrue returns true if e is satisfied by attributes attrs.
   106  func exprTrue(e ast.Expr, attrs map[string]struct{}) bool {
   107  	switch v := e.(type) {
   108  	case *ast.BinaryExpr:
   109  		switch v.Op {
   110  		case token.LAND:
   111  			return exprTrue(v.X, attrs) && exprTrue(v.Y, attrs)
   112  		case token.LOR:
   113  			return exprTrue(v.X, attrs) || exprTrue(v.Y, attrs)
   114  		}
   115  	case *ast.ParenExpr:
   116  		return exprTrue(v.X, attrs)
   117  	case *ast.UnaryExpr:
   118  		switch v.Op {
   119  		case token.NOT:
   120  			return !exprTrue(v.X, attrs)
   121  		}
   122  	case *ast.Ident:
   123  		return hasAttr(attrs, v.Name)
   124  	case *ast.BasicLit:
   125  		switch v.Kind {
   126  		case token.STRING:
   127  			// Strip doublequotes.
   128  			str, err := strconv.Unquote(v.Value)
   129  			if err != nil {
   130  				return false
   131  			}
   132  			return hasAttr(attrs, str)
   133  		}
   134  	}
   135  	return false
   136  }
   137  
   138  func hasAttr(attrs map[string]struct{}, want string) bool {
   139  	if !strings.Contains(want, "*") {
   140  		_, ok := attrs[want]
   141  		return ok
   142  	}
   143  
   144  	// The pattern looks like a glob. Temporarily replace asterisks with zero bytes
   145  	// so we can escape other chars that have special meanings in regular expressions.
   146  	want = strings.Replace(want, "*", "\000", -1)
   147  	want = regexp.QuoteMeta(want)
   148  	want = strings.Replace(want, "\000", ".*", -1)
   149  	want = "^" + want + "$"
   150  
   151  	re, err := regexp.Compile(want)
   152  	if err != nil {
   153  		return false
   154  	}
   155  	for attr := range attrs {
   156  		if re.MatchString(attr) {
   157  			return true
   158  		}
   159  	}
   160  	return false
   161  }