Repository: DmitrySoshnikov/regexp-tree
Pull request #115 (2017-11-19 14:33:41)

Optimizer: convert char class to \w differently depending on i and u flags
The meaning of `\w` is extended when regexp has `i` and `u` flags so we should not do the optimization in this case.

mathiasbynens: With `i` and `u` flags enabled, you could do the same optimization for `[0-9a-zA-Z_\u017F\u212A]` specifically.
Golmote: @mathiasbynens Really? Wow I didn't think there were only two exceptions!
I've updated the PR to handle those.

I've also added the case where, the `i` flag being set, there is no need for both `a-z` and `A-Z`.

So:
`/[\da-zA-Z_$]/`-> `/[\w$]/`
`/[\da-z_$]/i`-> `/[\w$]/i`
`/[\dA-Z_$]/i`-> `/[\w$]/i`
`/[\da-z_\u017F\u212A$]/iu`-> `/[\w$]/iu`
`/[\dA-Z_\u{017F}\u{212A}$]/iu`-> `/[\w$]/iu`
mathiasbynens: More could potentially be added as the Unicode Standard gets updates, but yeah, for now it’s only those two. https://mathiasbynens.be/notes/es6-unicode-regex#impact-i has some more info.

Patch:
diff --git a/index.js b/index.js
index 65ebbfd..0ebef0c 100644
--- a/index.js
+++ b/index.js
@@ -120,8 +120,8 @@ const regexpTree = {
    *
    * @return TransformResult object
    */
-  optimize(regexp, whitelist) {
-    return optimizer.optimize(regexp, whitelist);
+  optimize(regexp) {
+    return optimizer.optimize(regexp);
   },
 
   /**
diff --git a/src/optimizer/index.js b/src/optimizer/index.js
index 86a78c3..249e80f 100644
--- a/src/optimizer/index.js
+++ b/src/optimizer/index.js
@@ -25,11 +25,7 @@ module.exports = {
    *
    *   /\w+e+/
    */
-  optimize(regexp, transformsWhitelist = []) {
-    const transformToApply = transformsWhitelist.length > 0
-      ? transformsWhitelist
-      : Object.keys(optimizationTransforms);
-
+  optimize(regexp) {
     let prevResult;
     let result;
     do {
@@ -37,18 +33,7 @@ module.exports = {
         prevResult = result.toRegExp().toString();
         regexp = result.toRegExp();
       }
-      transformToApply.forEach(transformName => {
-
-        if (!optimizationTransforms.hasOwnProperty(transformName)) {
-          throw new Error(
-            `Unknown optimization-transform: ${transformName}. ` +
-            `Available transforms are: ` +
-            Object.keys(optimizationTransforms).join(', ')
-          );
-        }
-
-        const transformer = optimizationTransforms[transformName];
-
+      optimizationTransforms.forEach(transformer => {
         result = transform.transform(regexp, transformer);
         regexp = result.getAST();
       });
diff --git a/src/optimizer/transforms/char-class-to-meta-transform.js b/src/optimizer/transforms/char-class-to-meta-transform.js
index 5ce61b3..1e51802 100644
--- a/src/optimizer/transforms/char-class-to-meta-transform.js
+++ b/src/optimizer/transforms/char-class-to-meta-transform.js
@@ -10,13 +10,19 @@
  * their meta symbols equivalents.
  */
 module.exports = {
+  _hasIFlag: false,
+  _hasUFlag: false,
+  init(ast) {
+    this._hasIFlag = ast.flags.includes('i');
+    this._hasUFlag = ast.flags.includes('u');
+  },
   CharacterClass(path) {
 
     // [0-9] -> \d
     rewriteNumberRanges(path);
 
     // [a-zA-Z_0-9] -> \w
-    rewriteWordRanges(path);
+    rewriteWordRanges(path, this._hasIFlag, this._hasUFlag);
 
     // [ \t\r\n\f] -> \s
     rewriteWhitespaceRanges(path);
@@ -50,13 +56,15 @@ function rewriteNumberRanges(path) {
  * Thus, the ranges may go in any order, and other symbols/ranges
  * are kept untouched, e.g. [a-z_\dA-Z$] -> [\w$]
  */
-function rewriteWordRanges(path) {
+function rewriteWordRanges(path, hasIFlag, hasUFlag) {
   const {node} = path;
 
   let numberPath = null;
   let lowerCasePath = null;
   let upperCasePath = null;
   let underscorePath = null;
+  let u017fPath = null;
+  let u212aPath = null;
 
   node.expressions.forEach((expression, i) => {
 
@@ -79,14 +87,25 @@ function rewriteWordRanges(path) {
     else if (isUnderscore(expression)) {
       underscorePath = path.getChild(i);
     }
+
+    else if (hasIFlag && hasUFlag && isU017fPath(expression)) {
+      u017fPath = path.getChild(i);
+    }
+
+    else if (hasIFlag && hasUFlag && isU212aPath(expression)) {
+      u212aPath = path.getChild(i);
+    }
   });
 
   // If we found the whole pattern, replace it.
   if (
     numberPath &&
-    lowerCasePath &&
-    upperCasePath &&
-    underscorePath
+    (
+      (lowerCasePath && upperCasePath) ||
+      (hasIFlag && (lowerCasePath || upperCasePath))
+    ) &&
+    underscorePath &&
+    (!hasUFlag || !hasIFlag || (u017fPath && u212aPath))
   ) {
 
     // Put \w in place of \d.
@@ -102,9 +121,19 @@ function rewriteWordRanges(path) {
     });
 
     // Other paths are removed.
-    lowerCasePath.remove();
-    upperCasePath.remove();
+    if (lowerCasePath) {
+      lowerCasePath.remove();
+    }
+    if (upperCasePath) {
+      upperCasePath.remove();
+    }
     underscorePath.remove();
+    if (u017fPath) {
+      u017fPath.remove();
+    }
+    if (u212aPath) {
+      u212aPath.remove();
+    }
   }
 }
 
@@ -216,3 +245,17 @@ function isUnderscore(node) {
   );
 }
 
+function isU017fPath(node) {
+  return (
+    node.type === 'Char' &&
+    node.kind === 'unicode' &&
+    node.codePoint === 0x017f
+  );
+}
+function isU212aPath(node) {
+  return (
+    node.type === 'Char' &&
+    node.kind === 'unicode' &&
+    node.codePoint === 0x212a
+  );
+}
diff --git a/src/optimizer/transforms/char-class-to-single-char-transform.js b/src/optimizer/transforms/char-class-to-single-char-transform.js
index c056f9c..2be33f0 100644
--- a/src/optimizer/transforms/char-class-to-single-char-transform.js
+++ b/src/optimizer/transforms/char-class-to-single-char-transform.js
@@ -58,11 +58,11 @@ function isAppropriateChar(node) {
 }
 
 function isMeta(value) {
-  return /^\\[dwsDWS]$/.test(value);
+  return /^\\[dwsDWS$]/.test(value);
 }
 
 function getInverseMeta(value) {
-  return /[dws]/.test(value)
+  return /[dws$]/.test(value)
     ? value.toUpperCase()
     : value.toLowerCase();
 }
diff --git a/src/optimizer/transforms/index.js b/src/optimizer/transforms/index.js
index 05bf8b9..3b37113 100644
--- a/src/optimizer/transforms/index.js
+++ b/src/optimizer/transforms/index.js
@@ -5,37 +5,37 @@
 
 'use strict';
 
-module.exports = {
+module.exports = [
   // [\d\d] -> [\d]
-  'charClassRemoveDuplicates': require('./char-class-remove-duplicates-transform'),
+  require('./char-class-remove-duplicates-transform'),
 
   // a{1,2}a{2,3} -> a{3,5}
-  'quantifiersMerge': require('./quantifiers-merge-transform'),
+  require('./quantifiers-merge-transform'),
 
   // a{1,} -> a+, a{3,3} -> a{3}, a{1} -> a
-  'quantifierRangeToSymbol': require('./quantifier-range-to-symbol-transform'),
+  require('./quantifier-range-to-symbol-transform'),
 
   // [0-9] -> [\d]
-  'charClassToMeta': require('./char-class-to-meta-transform'),
+  require('./char-class-to-meta-transform'),
 
   // [\d] -> \d, [^\w] -> \W
-  'charClassToSingleChar': require('./char-class-to-single-char-transform'),
+  require('./char-class-to-single-char-transform'),
 
   // \e -> e
-  'charEscapeUnescape': require('./char-escape-unescape-transform'),
+  require('./char-escape-unescape-transform'),
 
   // (ab|ab) -> (ab)
-  'disjunctionRemoveDuplicates': require('./disjunction-remove-duplicates-transform'),
+  require('./disjunction-remove-duplicates-transform'),
 
   // (a|b|c) -> [abc]
-  'groupSingleCharsToCharClass': require('./group-single-chars-to-char-class'),
+  require('./group-single-chars-to-char-class'),
 
   // (?:)a -> a
-  'removeEmptyGroup': require('./remove-empty-group-transform'),
+  require('./remove-empty-group-transform'),
 
   // (?:a) -> a
-  'ungroup': require('./ungroup-transform'),
+  require('./ungroup-transform'),
 
   // abcabcabc -> (?:abc){3}
-  'combineRepeatingPatterns': require('./combine-repeating-patterns-transform')
-};
\ No newline at end of file
+  require('./combine-repeating-patterns-transform')
+];
\ No newline at end of file
diff --git a/src/traverse/index.js b/src/traverse/index.js
index ff029ca..95be86a 100644
--- a/src/traverse/index.js
+++ b/src/traverse/index.js
@@ -150,7 +150,7 @@ module.exports = {
     // Allow handlers to initializer themselves.
     handlers.forEach(handler => {
       if (typeof handler.init === 'function') {
-        handler.init();
+        handler.init(ast);
       }
     });
 


Test patch:
diff --git a/src/optimizer/__tests__/optimizer-integration-test.js b/src/optimizer/__tests__/optimizer-integration-test.js
index 7825f4a..3a97e29 100644
--- a/src/optimizer/__tests__/optimizer-integration-test.js
+++ b/src/optimizer/__tests__/optimizer-integration-test.js
@@ -81,12 +81,4 @@ describe('optimizer-integration-test', () => {
       .toBe(optimized.toString());
   });
 
-  it('applies whitelist only', () => {
-    const original = '/(?:[a])/';
-    const optimized = '/[a]/';
-
-    expect(optimizer.optimize(original, ['ungroup']).toString())
-      .toBe(optimized.toString());
-  });
-
 });
\ No newline at end of file
diff --git a/src/optimizer/transforms/__tests__/char-class-to-meta-transform-test.js b/src/optimizer/transforms/__tests__/char-class-to-meta-transform-test.js
index b4e6529..268eb8c 100644
--- a/src/optimizer/transforms/__tests__/char-class-to-meta-transform-test.js
+++ b/src/optimizer/transforms/__tests__/char-class-to-meta-transform-test.js
@@ -24,6 +24,20 @@ describe('char-class-to-meta', () => {
     expect(re.toString()).toBe('/[\\w$]/');
   });
 
+  it('replaces word ranges when regexp has i flag', () => {
+    const re = transform(/[0-9a-z_$]/i, [
+      charClassToMeta,
+    ]);
+    expect(re.toString()).toBe('/[\\w$]/i');
+  });
+
+  it('replaces word ranges when regexp has i and u flags', () => {
+    const re = transform(/[\da-zA-Z_\u017F\u212A$]/iu, [
+      charClassToMeta,
+    ]);
+    expect(re.toString()).toBe('/[\\w$]/iu');
+  });
+
   it('whitespace ranges', () => {
     const re = transform(/[ \t\r\n\f]/, [
       charClassToMeta,