Repository: ota-meshi/eslint-plugin-regexp (ts)

Issue:
`regexp/negation` false positives because of `i` flag
See the following example:

```js
var re2 = /[^\P{Ll}]/iu;
```

![image](https://user-images.githubusercontent.com/20878432/124463287-f9bfea80-dd92-11eb-9e42-0c3524d89b30.png)

Ignoring the strange suggested fix (this will be a separate issue), the report is incorrect. 

`/[^\P{Ll}]/iu` != `/\p{Ll}/iu`: 
The Canonicalize operation of the `i` flag is applied _before_ the character class is inverted. This example, `\P{Ll}` means "all character that are not lower-case letters". Since`\p{Ll}` contains all upper-case letters, it will also contain all lower-case letters after the Canonicalize operation (not quite right but almost), so `\P{Ll}` matches (almost) all characters. The character class will then match (almost) no character because it is inverted. This is obviously very different from `/\p{Ll}/iu` which matches (almost) all letter characters.

Pull request:
Fix `regexp/negation` false positives
Fixes #266.

Patch:
diff --git a/lib/rules/negation.ts b/lib/rules/negation.ts
index 4fe008c..d1e616c 100644
--- a/lib/rules/negation.ts
+++ b/lib/rules/negation.ts
@@ -31,23 +31,51 @@ export default createRule("negation", {
             node,
             getRegexpLocation,
             fixReplaceNode,
+            toCharSet,
+            flags,
         }: RegExpContext): RegExpVisitor.Handlers {
             return {
                 onCharacterClassEnter(ccNode) {
                     if (!ccNode.negate || ccNode.elements.length !== 1) {
                         return
                     }
+
                     const element = ccNode.elements[0]
-                    if (element.type === "CharacterSet") {
-                        const negatedCharSet = getNegationText(element)
-                        context.report({
-                            node,
-                            loc: getRegexpLocation(ccNode),
-                            messageId: "unexpected",
-                            data: { negatedCharSet },
-                            fix: fixReplaceNode(ccNode, negatedCharSet),
+                    if (element.type !== "CharacterSet") {
+                        return
+                    }
+
+                    if (flags.ignoreCase && element.kind === "property") {
+                        // The ignore case canonicalization affects negated
+                        // Unicode property escapes in a weird way. In short,
+                        // /\p{Foo}/i is not the same as /[^\P{Foo}]/i if
+                        // \p{Foo} contains case-varying characters.
+                        //
+                        // Note: This only affects Unicode property escapes.
+                        // All other character sets are either case-invariant
+                        // (/./, /\s/, /\d/) or inconsistent (/\w/).
+
+                        const ccSet = toCharSet(ccNode)
+
+                        const negatedElementSet = toCharSet({
+                            ...element,
+                            negate: !element.negate,
                         })
+
+                        if (!ccSet.equals(negatedElementSet)) {
+                            // We cannot remove the negative
+                            return
+                        }
                     }
+
+                    const negatedCharSet = getNegationText(element)
+                    context.report({
+                        node,
+                        loc: getRegexpLocation(ccNode),
+                        messageId: "unexpected",
+                        data: { negatedCharSet },
+                        fix: fixReplaceNode(ccNode, negatedCharSet),
+                    })
                 },
             }
         }


Test patch:
diff --git a/tests/lib/rules/negation.ts b/tests/lib/rules/negation.ts
index cdd1097..f84b386 100644
--- a/tests/lib/rules/negation.ts
+++ b/tests/lib/rules/negation.ts
@@ -9,7 +9,12 @@ const tester = new RuleTester({
 })
 
 tester.run("negation", rule as any, {
-    valid: [String.raw`/[\d]/`, String.raw`/[^\d\s]/`],
+    valid: [
+        String.raw`/[\d]/`,
+        String.raw`/[^\d\s]/`,
+        String.raw`/[^\p{ASCII}]/iu`,
+        String.raw`/[^\P{Ll}]/iu`,
+    ],
     invalid: [
         {
             code: String.raw`/[^\d]/`,
@@ -94,6 +99,13 @@ tester.run("negation", rule as any, {
                 "Unexpected negated character class. Use '\\p{Ll}' instead.",
             ],
         },
+        {
+            code: String.raw`/[^\P{White_Space}]/iu;`,
+            output: String.raw`/\p{White_Space}/iu;`,
+            errors: [
+                "Unexpected negated character class. Use '\\p{White_Space}' instead.",
+            ],
+        },
         {
             code: String.raw`const s ="[^\\w]"
             new RegExp(s)`,