Repository: VKCOM/noverify
Pull request #990 (2021-05-23 09:15:05)

phpgrep: implement partially normalized (fuzzy) matching
Dynamic rules will use this feature by default.
There is `@strict-syntax` attribute to disable this behavior explicitly.


Patch:
diff --git a/src/cmd/embeddedrules/rules.php b/src/cmd/embeddedrules/rules.php
index 81e2913..894072b 100644
--- a/src/cmd/embeddedrules/rules.php
+++ b/src/cmd/embeddedrules/rules.php
@@ -238,12 +238,14 @@ function offBy1() {
   /**
    * @warning probably intended to use count-1 as an index
    * @fix     $a[count($a) - 1]
+   * @strict-syntax
    */
   $a[count($a)];
 
   /**
    * @warning probably intended to use sizeof-1 as an index
    * @fix     $a[sizeof($a) - 1]
+   * @strict-syntax
    */
   $a[sizeof($a)];
 }
diff --git a/src/ir/normalize/normalize.go b/src/ir/normalize/normalize.go
index ac63950..108a813 100644
--- a/src/ir/normalize/normalize.go
+++ b/src/ir/normalize/normalize.go
@@ -10,6 +10,7 @@ import (
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/ir/irfmt"
 	"github.com/VKCOM/noverify/src/ir/irutil"
+	"github.com/VKCOM/noverify/src/ir/phpcore"
 	"github.com/VKCOM/noverify/src/meta"
 )
 
@@ -354,10 +355,7 @@ func (norm *normalizer) normalizedExpr(e ir.Node) ir.Node {
 		}
 
 		// Replace aliased functions.
-		alias, ok := funcAliases[funcName.Value]
-		if ok {
-			e.Function = alias
-		}
+		e.Function = phpcore.ResolveAlias(funcName)
 
 	case *ir.Assign:
 		if !sideEffectFree(e.Variable) {
@@ -446,31 +444,6 @@ var (
 	emptyStringNode = &ir.String{}
 )
 
-var funcAliases = map[string]*ir.Name{
-	// See https://www.php.net/manual/ru/aliases.php
-
-	`doubleval`: {Value: `floatval`},
-
-	`ini_alter`:    {Value: `ini_set`},
-	`is_integer`:   {Value: `is_int`},
-	`is_long`:      {Value: `is_int`},
-	`is_real`:      {Value: `is_float`},
-	`is_double`:    {Value: `is_float`},
-	`is_writeable`: {Value: `is_writable`},
-
-	`join`:       {Value: `implode`},
-	`chop`:       {Value: `rtrim`},
-	`strchr`:     {Value: `strstr`},
-	`pos`:        {Value: `current`},
-	`key_exists`: {Value: `array_key_exists`},
-	`sizeof`:     {Value: `count`},
-
-	`close`:                {Value: `closedir`},
-	`fputs`:                {Value: `fwrite`},
-	`magic_quotes_runtime`: {Value: `set_magic_quotes_runtime`},
-	`show_source`:          {Value: `highlight_file`},
-}
-
 func literalValue(e ir.Node) string {
 	switch e := e.(type) {
 	case *ir.Lnumber:
diff --git a/src/ir/phpcore/phpcore.go b/src/ir/phpcore/phpcore.go
new file mode 100644
index 0000000..202c0cc
--- /dev/null
+++ b/src/ir/phpcore/phpcore.go
@@ -0,0 +1,42 @@
+package phpcore
+
+import (
+	"github.com/VKCOM/noverify/src/ir"
+)
+
+func ResolveAlias(function ir.Node) ir.Node {
+	nm, ok := function.(*ir.Name)
+	if !ok {
+		return function
+	}
+	alias, ok := funcAliases[nm.Value]
+	if ok {
+		return alias
+	}
+	return function
+}
+
+var funcAliases = map[string]*ir.Name{
+	// See https://www.php.net/manual/ru/aliases.php
+
+	`doubleval`: {Value: `floatval`},
+
+	`ini_alter`:    {Value: `ini_set`},
+	`is_integer`:   {Value: `is_int`},
+	`is_long`:      {Value: `is_int`},
+	`is_real`:      {Value: `is_float`},
+	`is_double`:    {Value: `is_float`},
+	`is_writeable`: {Value: `is_writable`},
+
+	`join`:       {Value: `implode`},
+	`chop`:       {Value: `rtrim`},
+	`strchr`:     {Value: `strstr`},
+	`pos`:        {Value: `current`},
+	`key_exists`: {Value: `array_key_exists`},
+	`sizeof`:     {Value: `count`},
+
+	`close`:                {Value: `closedir`},
+	`fputs`:                {Value: `fwrite`},
+	`magic_quotes_runtime`: {Value: `set_magic_quotes_runtime`},
+	`show_source`:          {Value: `highlight_file`},
+}
diff --git a/src/phpgrep/compile.go b/src/phpgrep/compile.go
index a43903f..1da5a7e 100644
--- a/src/phpgrep/compile.go
+++ b/src/phpgrep/compile.go
@@ -13,6 +13,8 @@ type compiler struct {
 	src  []byte
 	vars map[string]struct{}
 
+	fuzzyMatching bool
+
 	err error
 }
 
@@ -28,8 +30,9 @@ func compile(opts *Compiler, pattern []byte) (*Matcher, error) {
 	}
 
 	c := compiler{
-		src:  src,
-		vars: make(map[string]struct{}),
+		src:           src,
+		vars:          make(map[string]struct{}),
+		fuzzyMatching: opts.FuzzyMatching,
 	}
 	rootIR.Walk(&c)
 
@@ -42,6 +45,7 @@ func compile(opts *Compiler, pattern []byte) (*Matcher, error) {
 			root:          rootIR,
 			numVars:       len(c.vars),
 			caseSensitive: opts.CaseSensitive,
+			fuzzyMatching: opts.FuzzyMatching,
 		},
 	}
 
@@ -54,6 +58,15 @@ func (c *compiler) EnterNode(n ir.Node) bool {
 		return true
 	}
 
+	if c.fuzzyMatching {
+		switch v := n.(type) {
+		case *ir.Lnumber:
+			v.Value = normalizedIntValue(v.Value)
+		case *ir.Dnumber:
+			v.Value = normalizedFloatValue(v.Value)
+		}
+	}
+
 	v, ok := n.(*ir.Var)
 	if !ok {
 		return true
diff --git a/src/phpgrep/doc.go b/src/phpgrep/doc.go
index d82b78d..19af889 100644
--- a/src/phpgrep/doc.go
+++ b/src/phpgrep/doc.go
@@ -17,8 +17,6 @@ package phpgrep
 //
 // - Replace functionality.
 //
-// - Handle case sensitivity carefully (provide an option?).
-//
 // - stmt.Expression vs normal expressions named captures (should they match?).
 //
 // - Multi-statements matching.
diff --git a/src/phpgrep/matcher.go b/src/phpgrep/matcher.go
index cf6d52a..d0bc492 100644
--- a/src/phpgrep/matcher.go
+++ b/src/phpgrep/matcher.go
@@ -6,6 +6,8 @@ import (
 	"strings"
 
 	"github.com/VKCOM/noverify/src/ir"
+	"github.com/VKCOM/noverify/src/ir/irutil"
+	"github.com/VKCOM/noverify/src/ir/phpcore"
 )
 
 type matcherState struct {
@@ -22,6 +24,7 @@ type matcher struct {
 	numVars int
 
 	caseSensitive bool
+	fuzzyMatching bool
 
 	// Used only when -tracing build tag is specified.
 	tracingBuf   *bytes.Buffer
@@ -227,6 +230,12 @@ func (m *matcher) eqNodeWithCase(state *matcherState, x, y ir.Node) bool {
 	}
 }
 
+func (m *matcher) eqBinaryExpr(state *matcherState, xlhs, xrhs, ylhs, yrhs ir.Node) bool {
+	// TODO: if it's safe, ignore parens here too.
+	// Hint: it's not always safe to do so.
+	return m.eqNode(state, xlhs, ylhs) && m.eqNode(state, xrhs, yrhs)
+}
+
 func (m *matcher) eqNode(state *matcherState, x, y ir.Node) bool {
 	if tracingEnabled && m.tracingBuf != nil {
 		pad := strings.Repeat(" • ", m.tracingDepth)
@@ -371,19 +380,32 @@ func (m *matcher) eqNode(state *matcherState, x, y ir.Node) bool {
 
 	case *ir.ListExpr:
 		y, ok := y.(*ir.ListExpr)
-		return ok && x.ShortSyntax == y.ShortSyntax &&
-			m.eqArrayItemSlice(state, x.Items, y.Items)
+		if !ok {
+			return false
+		}
+		if !m.fuzzyMatching {
+			if x.ShortSyntax != y.ShortSyntax {
+				return false
+			}
+		}
+		return m.eqArrayItemSlice(state, x.Items, y.Items)
 
 	case *ir.NewExpr:
 		y, ok := y.(*ir.NewExpr)
 		if !ok || !m.eqNodeWithCase(state, x.Class, y.Class) {
 			return false
 		}
-		if x.Args == nil {
-			return y.Args == nil
-		}
-		if y.Args == nil {
-			return x.Args == nil
+		if m.fuzzyMatching {
+			if len(x.Args) != len(y.Args) {
+				return false
+			}
+		} else {
+			if x.Args == nil {
+				return y.Args == nil
+			}
+			if y.Args == nil {
+				return x.Args == nil
+			}
 		}
 		return m.eqNodeSlice(state, x.Args, y.Args)
 
@@ -457,23 +479,58 @@ func (m *matcher) eqNode(state *matcherState, x, y ir.Node) bool {
 		if !ok {
 			return false
 		}
+		xVal := x.Val
+		yVal := y.Val
+		if m.fuzzyMatching {
+			xVal = irutil.Unparen(xVal)
+			yVal = irutil.Unparen(yVal)
+		}
 		if x.Key == nil || y.Key == nil {
-			return x.Key == y.Key && m.eqNode(state, x.Val, y.Val)
+			return x.Key == y.Key && m.eqNode(state, xVal, yVal)
+		}
+		xKey := x.Key
+		yKey := y.Key
+		if m.fuzzyMatching {
+			xKey = irutil.Unparen(xKey)
+			yKey = irutil.Unparen(yKey)
 		}
-		return m.eqNode(state, x.Key, y.Key) && m.eqNode(state, x.Val, y.Val)
+		return m.eqNode(state, xKey, yKey) && m.eqNode(state, xVal, yVal)
 	case *ir.ArrayExpr:
 		y, ok := y.(*ir.ArrayExpr)
-		return ok && x.ShortSyntax == y.ShortSyntax &&
-			m.eqArrayItemSlice(state, x.Items, y.Items)
+		if !ok {
+			return false
+		}
+		if !m.fuzzyMatching {
+			if x.ShortSyntax != y.ShortSyntax {
+				return false
+			}
+		}
+		return m.eqArrayItemSlice(state, x.Items, y.Items)
 
 	case *ir.Argument:
 		y, ok := y.(*ir.Argument)
-		return ok && x.IsReference == y.IsReference &&
-			x.Variadic == y.Variadic &&
-			m.eqNode(state, x.Expr, y.Expr)
+		if !ok || x.IsReference != y.IsReference || x.Variadic != y.Variadic {
+			return false
+		}
+		xExpr := x.Expr
+		yExpr := y.Expr
+		if m.fuzzyMatching {
+			xExpr = irutil.Unparen(xExpr)
+			yExpr = irutil.Unparen(yExpr)
+		}
+		return m.eqNode(state, xExpr, yExpr)
 	case *ir.FunctionCallExpr:
 		y, ok := y.(*ir.FunctionCallExpr)
-		if !ok || !m.eqNodeWithCase(state, x.Function, y.Function) {
+		if !ok {
+			return false
+		}
+		xFunc := x.Function
+		yFunc := y.Function
+		if m.fuzzyMatching {
+			xFunc = phpcore.ResolveAlias(xFunc)
+			yFunc = phpcore.ResolveAlias(yFunc)
+		}
+		if !m.eqNodeWithCase(state, xFunc, yFunc) {
 			return false
 		}
 		return m.eqNodeSlice(state, x.Args, y.Args)
@@ -561,95 +618,115 @@ func (m *matcher) eqNode(state *matcherState, x, y ir.Node) bool {
 		return ok && y.Value == x.Value
 	case *ir.Lnumber:
 		y, ok := y.(*ir.Lnumber)
-		return ok && y.Value == x.Value
+		if !ok {
+			return false
+		}
+		if m.fuzzyMatching {
+			return normalizedIntValue(y.Value) == x.Value
+		}
+		return y.Value == x.Value
 	case *ir.Dnumber:
 		y, ok := y.(*ir.Dnumber)
-		return ok && y.Value == x.Value
+		if !ok {
+			return false
+		}
+		if m.fuzzyMatching {
+			return normalizedFloatValue(y.Value) == x.Value
+		}
+		return y.Value == x.Value
 	case *ir.String:
 		y, ok := y.(*ir.String)
-		return ok && y.Value == x.Value
+		if !ok {
+			return false
+		}
+		if !m.fuzzyMatching {
+			if y.DoubleQuotes != x.DoubleQuotes {
+				return false
+			}
+		}
+		return y.Value == x.Value
 
 	case *ir.CoalesceExpr:
 		y, ok := y.(*ir.CoalesceExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.ConcatExpr:
 		y, ok := y.(*ir.ConcatExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.DivExpr:
 		y, ok := y.(*ir.DivExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.ModExpr:
 		y, ok := y.(*ir.ModExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.MulExpr:
 		y, ok := y.(*ir.MulExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.PowExpr:
 		y, ok := y.(*ir.PowExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.BitwiseAndExpr:
 		y, ok := y.(*ir.BitwiseAndExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.BitwiseOrExpr:
 		y, ok := y.(*ir.BitwiseOrExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.BitwiseXorExpr:
 		y, ok := y.(*ir.BitwiseXorExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.ShiftLeftExpr:
 		y, ok := y.(*ir.ShiftLeftExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.ShiftRightExpr:
 		y, ok := y.(*ir.ShiftRightExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.LogicalAndExpr:
 		y, ok := y.(*ir.LogicalAndExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.LogicalOrExpr:
 		y, ok := y.(*ir.LogicalOrExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.LogicalXorExpr:
 		y, ok := y.(*ir.LogicalXorExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.BooleanAndExpr:
 		y, ok := y.(*ir.BooleanAndExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.BooleanOrExpr:
 		y, ok := y.(*ir.BooleanOrExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.NotEqualExpr:
 		y, ok := y.(*ir.NotEqualExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.NotIdenticalExpr:
 		y, ok := y.(*ir.NotIdenticalExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.EqualExpr:
 		y, ok := y.(*ir.EqualExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.IdenticalExpr:
 		y, ok := y.(*ir.IdenticalExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.GreaterExpr:
 		y, ok := y.(*ir.GreaterExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.GreaterOrEqualExpr:
 		y, ok := y.(*ir.GreaterOrEqualExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.SmallerExpr:
 		y, ok := y.(*ir.SmallerExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.SmallerOrEqualExpr:
 		y, ok := y.(*ir.SmallerOrEqualExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.SpaceshipExpr:
 		y, ok := y.(*ir.SpaceshipExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.PlusExpr:
 		y, ok := y.(*ir.PlusExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 	case *ir.MinusExpr:
 		y, ok := y.(*ir.MinusExpr)
-		return ok && m.eqNode(state, x.Left, y.Left) && m.eqNode(state, x.Right, y.Right)
+		return ok && m.eqBinaryExpr(state, x.Left, x.Right, y.Left, y.Right)
 
 	case *ir.ConstFetchExpr:
 		y, ok := y.(*ir.ConstFetchExpr)
diff --git a/src/phpgrep/phpgrep.go b/src/phpgrep/phpgrep.go
index 7b3f085..c23481f 100644
--- a/src/phpgrep/phpgrep.go
+++ b/src/phpgrep/phpgrep.go
@@ -13,6 +13,10 @@ type Compiler struct {
 	// case sensitivity is set to true, compiled matcher will reject
 	// any spelling mismatches.
 	CaseSensitive bool
+
+	// FuzzyMatching enables some forms of normalization to the
+	// pattern so `[$x]` matches `array($x)` too.
+	FuzzyMatching bool
 }
 
 // Compile compiler a given pattern into a matcher.
diff --git a/src/phpgrep/utils.go b/src/phpgrep/utils.go
index 75b1418..3775c10 100644
--- a/src/phpgrep/utils.go
+++ b/src/phpgrep/utils.go
@@ -1,11 +1,29 @@
 package phpgrep
 
 import (
+	"strconv"
+
 	"github.com/z7zmey/php-parser/pkg/position"
 
 	"github.com/VKCOM/noverify/src/ir"
 )
 
+func normalizedIntValue(s string) string {
+	v, err := strconv.ParseInt(s, 0, 64)
+	if err != nil {
+		return s
+	}
+	return strconv.FormatInt(v, 10)
+}
+
+func normalizedFloatValue(s string) string {
+	v, err := strconv.ParseFloat(s, 64)
+	if err != nil {
+		return s
+	}
+	return strconv.FormatFloat(v, 'f', -1, 64)
+}
+
 func findNamed(capture []CapturedNode, name string) (ir.Node, bool) {
 	for _, c := range capture {
 		if c.Name == name {
diff --git a/src/rules/parser.go b/src/rules/parser.go
index 036c5b2..8a5fda4 100644
--- a/src/rules/parser.go
+++ b/src/rules/parser.go
@@ -205,6 +205,9 @@ func (p *parser) parseRuleInfo(st ir.Node, proto *Rule) (Rule, error) {
 			rule.Level = lintapi.LevelNotice
 			rule.Message = part.ParamsText
 
+		case "strict-syntax":
+			rule.StrictSyntax = true
+
 		case "fix":
 			if rule.Fix != "" {
 				return rule, p.errorf(st, "duplicated @fix")
@@ -352,6 +355,7 @@ func (p *parser) parseRule(st ir.Node, proto *Rule) error {
 	}
 
 	pos := ir.GetPosition(st)
+	p.compiler.FuzzyMatching = !rule.StrictSyntax
 	m, err := p.compiler.Compile(p.sources[pos.StartPos-1 : pos.EndPos])
 	if err != nil {
 		return p.errorf(st, "pattern compilation error: %v", err)
diff --git a/src/rules/rules.go b/src/rules/rules.go
index b2af4ed..d8e24d7 100644
--- a/src/rules/rules.go
+++ b/src/rules/rules.go
@@ -81,6 +81,9 @@ type Rule struct {
 	// Level is a severity level that is used during report generation.
 	Level int
 
+	// StrictSyntax determines whether phpgrep fuzzy search should not be used.
+	StrictSyntax bool
+
 	// Message is a report text that is printed when this rule matches.
 	Message string
 


Test patch:
diff --git a/src/phpgrep/matcher_test.go b/src/phpgrep/matcher_test.go
index 2ad8d49..1283f22 100644
--- a/src/phpgrep/matcher_test.go
+++ b/src/phpgrep/matcher_test.go
@@ -117,6 +117,111 @@ func TestMatchConcurrent(t *testing.T) {
 	}
 }
 
+func TestMatchFuzzy(t *testing.T) {
+	var c Compiler
+	c.FuzzyMatching = true
+
+	runMatchTest(t, &c, true, []*matcherTest{
+		// NewExpr: nil args list -vs- empty argument list.
+		{`new C`, `new C()`},
+		{`new C`, `new C`},
+		{`new C()`, `new C`},
+		{`new C()`, `new C()`},
+
+		// ArrayExpr: [] -vs- array() syntax.
+		{`[]`, `[]`},
+		{`[]`, `array()`},
+		{`[$x]`, `array(10)`},
+		{`array()`, `[]`},
+		{`array($x)`, `[10]`},
+
+		// ListExpr: [] -vs- list() syntax.
+		{`list($_) = $_`, `list($x) = $a`},
+		{`[$_] = $_`, `list($x) = $a`},
+		{`list($_) = $_`, `[$x] = $a`},
+
+		// Numerical literals are normalized.
+		{`1`, `1`},
+		{`0x1`, `1`},
+		{`0b1`, `1`},
+		{`0.01`, `.01`},
+		{`0.10`, `0.1`},
+		{`0.00`, `0.0`},
+
+		// Builtin function aliases are replaced.
+		{`doubleval($_)`, `floatval($_)`},
+		{`floatval($_)`, `doubleval($_)`},
+		{`is_long($x)`, `is_long($v)`},
+		{`is_long($x)`, `is_int($v)`},
+		{`is_long($x)`, `is_integer($v)`},
+		{`is_integer($x)`, `is_long($v)`},
+		{`is_integer($x)`, `is_int($v)`},
+		{`is_integer($x)`, `is_integer($v)`},
+
+		// Double quotes are as good as single quotes
+		{`"a"`, `'a'`},
+
+		// String literals are interpreted by the IR generator.
+		{`"\n"`, `"\x0a"`},
+
+		// Parens are ignored, when sensible.
+		{`f($x, $x)`, `f((1), 1)`},
+		{`f($x, $x)`, `f(1, (1))`},
+		{`[$x, $x]`, `[(1), 1]`},
+		{`[$x, $x]`, `[1, (1)]`},
+		{`[$_ => $x, $_ => $x]`, `['a' => (1), 'b' => 1]`},
+		{`[$_ => $x, $_ => $x]`, `['a' => 1, 'b' => (1)]`},
+		{`[$x => $_, $x => $_]`, `[(1) => "a", 1 => "b"]`},
+		{`[$x => $_, $x => $_]`, `[1 => "a", (1) => "b"]`},
+
+		// TODO: some binary expr could be normalized, but
+		// it could lead to different parsing results when
+		// LHS and RHS operands have different precedence.
+		//
+		// {`$x == $x`, `$v == $v`},
+		// {`$x == $x`, `$v == ($v)`},
+		// {`$x == $x`, `($v) == $v`},
+		// {`$x == $x`, `($v) == ($v)`},
+		// {`($x) == $x`, `(1) == 1`},
+		// {`($x) == $x`, `(1) == (1)`},
+		// {`($x) == $x`, `((1)) == (1)`},
+	})
+}
+
+func TestMatchFuzzyNegative(t *testing.T) {
+	var c Compiler
+	c.FuzzyMatching = true
+
+	runMatchTest(t, &c, false, []*matcherTest{
+		{`new C`, `new C(1)`},
+
+		{`[]`, `[1]`},
+		{`[1]`, `array()`},
+
+		{`list($_) = $_`, `list($x, $y) = $a`},
+
+		{`1`, `2`},
+		{`0x2`, `1`},
+		{`0b10`, `1`},
+		{`0.011`, `.01`},
+
+		{`f($_)`, `floatval($_)`},
+		{`floatval($_)`, `f($_)`},
+
+		{`"a"`, `'b'`},
+
+		{`"\n"`, `"\x0f"`},
+
+		{`$x == $x`, `$v == $v2`},
+		{`$x == $x`, `$v == ($v2)`},
+		{`$x == $x`, `($v) == $v2`},
+		{`$x == $x`, `($v) == ($v2)`},
+		{`($x) == $x`, `(1) == 2`},
+		{`($x) == $x`, `(1) == (2)`},
+		{`($x) == $x`, `((1)) == (2)`},
+	})
+}
+
 func TestMatchCaseSensitive(t *testing.T) {
 	var c Compiler
 	c.CaseSensitive = true
@@ -129,6 +234,8 @@ func TestMatchCaseSensitive(t *testing.T) {
 		{`$x instanceof c`, `$x instanceof c`},
 		{`c::value`, `c::value`},
 		{`c::$value`, `c::$value`},
+		{`TRUE`, `TRUE`},
+		{`true`, `true`},
 	})
 }
 
@@ -153,6 +260,8 @@ func TestMatchCaseSensitiveNegative(t *testing.T) {
 		{`C::value`, `c::value`},
 		{`c::$value`, `C::$value`},
 		{`C::$value`, `c::$value`},
+		{`TRUE`, `true`},
+		{`true`, `TRUE`},
 	})
 }
 
@@ -478,6 +587,11 @@ func TestMatch(t *testing.T) {
 		{`f((1))`, `f((1))`},
 		{`($foo)()`, `($foo)()`},
 		{`($foo)->x`, `($foo)->x`},
+		{`($x) == $x`, `(1) == 1`},
+		{`($x) == $x`, `((1)) == (1)`},
+		{`f(($x), ($x))`, `f((1), (1))`},
+		{`[$_ => ($x), $_ => ($x)]`, `['a' => (1), 'b' => (1)]`},
+		{`[($x) => $_, ($x) => $_]`, `[(1) => "a", (1) => "b"]`},
 
 		{`try { ${"*"}; } catch (Exception $_) { ${"*"}; }`, `try {} catch (Exception $e) {}`},
 		{`try { ${"*"}; } catch (Exception $_) { ${"*"}; }`, `try {} catch (Exception $ex) { var_dump($ex); }`},
@@ -599,6 +713,8 @@ func TestMatchNegative(t *testing.T) {
 
 		{`"$x$x"`, `"11"`},
 		{`"$x$x"`, `'$x$x'`},
+		{`"a"`, `'a'`},
+		{`'a'`, `"a"`},
 
 		{`int($x)`, `$v`},
 		{`array($x)`, `$v`},
@@ -651,6 +767,15 @@ func TestMatchNegative(t *testing.T) {
 		{`($foo)->x`, `$foo->x`},
 		{`(($foo))()`, `($foo)()`},
 		{`(($foo))->x`, `($foo)->x`},
+		{`$x == $x`, `$v == ($v)`},
+		{`$x == $x`, `($v) == $v`},
+		{`($x) == $x`, `(1) == (1)`},
+		{`f($x, $x)`, `f((1), 1)`},
+		{`f($x, $x)`, `f(1, (1))`},
+		{`[$_ => $x, $_ => $x]`, `['a' => (1), 'b' => 1]`},
+		{`[$_ => $x, $_ => $x]`, `['a' => 1, 'b' => (1)]`},
+		{`[$x => $_, $x => $_]`, `[(1) => "a", 1 => "b"]`},
+		{`[$x => $_, $x => $_]`, `[1 => "a", (1) => "b"]`},
 
 		{`try { ${"*"}; } catch (Exception $_) { ${"*"}; }`, `try {} catch (MyException $e) {}`},
 		{`try { ${"*"}; } catch (Exception $_) { ${"*"}; }`, `try {} catch (Exception $_) {} catch (Exception $_) {}`},