cmd/watchflakes/internal/script: lex !~ as a single token

Even though both ~ and !~ operators are documented as supported at
https://go.dev/wiki/Watchflakes, it seems only the former was ever
supported in practice. As it was uncovered when I tried the latter
in https://go.dev/issue/66337#issuecomment-2569872000, it fails to
parse with an "unexpected !" error.

The problem is that lex doesn't find the !~ token, it reports that
sequence as separate tokens ! and ~. Fix its logic.

Fixes golang/go#71119.

Change-Id: I08dd845a59e976a5eb2687924dce972680c90077
Reviewed-on: https://go-review.googlesource.com/c/build/+/640275
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/watchflakes/internal/script/script.go b/cmd/watchflakes/internal/script/script.go
index 499f2e7..6159251 100644
--- a/cmd/watchflakes/internal/script/script.go
+++ b/cmd/watchflakes/internal/script/script.go
@@ -454,7 +454,7 @@
 		p.i++
 		p.tok = p.s[p.pos:p.i]
 		return
-	case '<': // <-, <=
+	case '<': // < <- <=
 		p.pos = p.i
 		p.i++
 		if p.i < len(p.s) && (p.s[p.i] == '-' || p.s[p.i] == '=') {
@@ -462,7 +462,15 @@
 		}
 		p.tok = p.s[p.pos:p.i]
 		return
-	case '!', '>': // ! != > >=
+	case '!': // ! !~ !=
+		p.pos = p.i
+		p.i++
+		if p.i < len(p.s) && (p.s[p.i] == '~' || p.s[p.i] == '=') {
+			p.i++
+		}
+		p.tok = p.s[p.pos:p.i]
+		return
+	case '>': // > >=
 		p.pos = p.i
 		p.i++
 		if p.i < len(p.s) && p.s[p.i] == '=' {
diff --git a/cmd/watchflakes/internal/script/script_test.go b/cmd/watchflakes/internal/script/script_test.go
index 073de57..71cc09d 100644
--- a/cmd/watchflakes/internal/script/script_test.go
+++ b/cmd/watchflakes/internal/script/script_test.go
@@ -25,7 +25,7 @@
 	{"x ~", "a ~"},
 	{"x &", "a err: :1.3: invalid syntax at &"},
 	{"x &y", "a err: :1.3: invalid syntax at &"},
-	{"output !~ `content`", "a ! ~ `"},
+	{"output !~ `content`", "a !~ `"},
 }
 
 func TestLex(t *testing.T) {
diff --git a/cmd/watchflakes/script_test.go b/cmd/watchflakes/script_test.go
index deb5d8d..79f2709 100644
--- a/cmd/watchflakes/script_test.go
+++ b/cmd/watchflakes/script_test.go
@@ -96,8 +96,17 @@
 	{
 		`default <- pkg == "cmd/go" && test == "TestScript" &&
 		            output !~ ` + "`" + `The process cannot access the file because it is being used by another process.` + "`" + `  # tracked in go.dev/issue/71112`,
-		nil,
-		"script:2.22: unexpected !",
+		[]*script.Rule{{
+			Action: "default",
+			Pattern: &script.AndExpr{
+				X: &script.AndExpr{
+					X: &script.CmpExpr{Field: "pkg", Op: "==", Literal: "cmd/go"},
+					Y: &script.CmpExpr{Field: "test", Op: "==", Literal: "TestScript"},
+				},
+				Y: &script.RegExpr{Field: "output", Not: true, Regexp: regexp.MustCompile(`(?m)The process cannot access the file because it is being used by another process.`)},
+			},
+		}},
+		"",
 	},
 	{
 		`post <- pkg ~ "^cmd/go"`,