Repository: DmitrySoshnikov/regexp-tree
Pull request #132 (2017-11-29 20:51:53)

Optimizer: Transform coded chars to simple chars 
The transform in this PR converts coded chars (unicode, hex, oct, dec) to simple chars when they are in the range of ASCII printable chars.

`/\u0041\u0020\u007e/` -> `/A ~/`

It will do the same on class ranges but only when they're included in either `0-9`, `a-z` or `A-Z`.
`/[\x42-\x4c\x6d-\x72]/` -> `/[B-Lm-r]/`

But `/[\u{5a}-\u{61}]/u` is _not_ converted to `/[Z-a]/u`.

It will escape chars when needed (naively, without additional checks):

`/\u005b\u{28}[\u{5c}\x5e]/u` -> `/\[\([\\\^]/u`

This PR relies on (and includes) #130 and #131. Otherwise the tests won't run.
(I'm not sure how annoying it is on your side to handle "PRs with dependencies". If you know of a better way to do this, let me now.)

DmitrySoshnikov: Sounds good; I'll take a deeper look later on at this one (BTW, I also merged the dependency PRs, so you can rebase in case).

Patch:
diff --git a/src/optimizer/transforms/char-code-to-simple-char-transform.js b/src/optimizer/transforms/char-code-to-simple-char-transform.js
new file mode 100644
index 0000000..075d525
--- /dev/null
+++ b/src/optimizer/transforms/char-code-to-simple-char-transform.js
@@ -0,0 +1,87 @@
+/**
+ * The MIT License (MIT)
+ * Copyright (c) 2017-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
+ */
+
+'use strict';
+
+const UPPER_A_CP = 'A'.codePointAt(0);
+const UPPER_Z_CP = 'Z'.codePointAt(0);
+const LOWER_A_CP = 'a'.codePointAt(0);
+const LOWER_Z_CP = 'z'.codePointAt(0);
+const DIGIT_0_CP = '0'.codePointAt(0);
+const DIGIT_9_CP = '9'.codePointAt(0);
+
+/**
+ * A regexp-tree plugin to transform coded chars into simple chars.
+ *
+ * \u0061 -> a
+ */
+module.exports = {
+  Char(path) {
+    const {node, parent} = path;
+    if (isNaN(node.codePoint) || node.kind === 'simple') {
+      return;
+    }
+
+    if (parent.type === 'ClassRange') {
+      if (!isSimpleRange(parent)) {
+        return;
+      }
+    }
+
+    if (!isPrintableASCIIChar(node.codePoint)) {
+      return;
+    }
+
+    const symbol = String.fromCodePoint(node.codePoint);
+    const newChar = {
+      type: 'Char',
+      kind: 'simple',
+      value: symbol,
+      symbol: symbol,
+      codePoint: node.codePoint
+    };
+    if (needsEscape(symbol, parent.type)) {
+      newChar.escaped = true;
+    }
+    path.replace(newChar);
+  }
+};
+
+/**
+ * Checks if a range is included either in 0-9, a-z or A-Z
+ * @param classRange
+ * @returns {boolean}
+ */
+function isSimpleRange(classRange) {
+  const {from, to} = classRange;
+  return (
+    from.codePoint >= DIGIT_0_CP && from.codePoint <= DIGIT_9_CP &&
+    to.codePoint >= DIGIT_0_CP && to.codePoint <= DIGIT_9_CP
+  ) || (
+    from.codePoint >= UPPER_A_CP && from.codePoint <= UPPER_Z_CP &&
+    to.codePoint >= UPPER_A_CP && to.codePoint <= UPPER_Z_CP
+  ) || (
+    from.codePoint >= LOWER_A_CP && from.codePoint <= LOWER_Z_CP &&
+    to.codePoint >= LOWER_A_CP && to.codePoint <= LOWER_Z_CP
+  );
+}
+
+/**
+ * Checks if a code point in the range of printable ASCII chars
+ * (DEL char excluded)
+ * @param codePoint
+ * @returns {boolean}
+ */
+function isPrintableASCIIChar(codePoint) {
+  return codePoint >= 0x20 && codePoint <= 0x7e;
+}
+
+function needsEscape(symbol, parentType) {
+  if (parentType === 'ClassRange' || parentType === 'CharacterClass') {
+    return /[\]\\^-]/.test(symbol);
+  }
+
+  return /[*[()+?^$./\\|{}]/.test(symbol);
+}
\ No newline at end of file
diff --git a/src/optimizer/transforms/index.js b/src/optimizer/transforms/index.js
index 6a2bb44..be1afa7 100644
--- a/src/optimizer/transforms/index.js
+++ b/src/optimizer/transforms/index.js
@@ -9,6 +9,9 @@ module.exports = {
   // \ud83d\ude80 -> \u{1f680}
   'charSurrogatePairToSingleUnicode': require('./char-surrogate-pair-to-single-unicode-transform'),
 
+  // \u0061 -> a
+  'charCodeToSimpleChar': require('./char-code-to-simple-char-transform'),
+
   // /Aa/i -> /aa/i
   'charCaseInsensitiveLowerCaseTransform': require('./char-case-insensitive-lowercase-transform'),
 


Test patch:
diff --git a/src/optimizer/transforms/__tests__/char-code-to-simple-char-transform-test.js b/src/optimizer/transforms/__tests__/char-code-to-simple-char-transform-test.js
new file mode 100644
index 0000000..7ddf8e5
--- /dev/null
+++ b/src/optimizer/transforms/__tests__/char-code-to-simple-char-transform-test.js
@@ -0,0 +1,116 @@
+/**
+ * The MIT License (MIT)
+ * Copyright (c) 2017-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
+ */
+
+'use strict';
+
+const {transform} = require('../../../transform');
+const charCodeToSimpleChar = require('../char-code-to-simple-char-transform');
+
+describe('char-code-to-simple-char', () => {
+
+  it('converts coded chars to simple chars', () => {
+    let re = transform(/\u0041\u0020\u007e[\u0042-\u004c\u006d-\u0072]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/A ~[B-Lm-r]/');
+
+    re = transform(/\u{41}\u{20}\u{7e}[\u{42}-\u{4c}\u{6d}-\u{72}]/u, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/A ~[B-Lm-r]/u');
+
+    re = transform(/\x41\x20\x7e[\x42-\x4c\x6d-\x72]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/A ~[B-Lm-r]/');
+
+    re = transform(/\040\071[\061-\065]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/ 9[1-5]/');
+
+    re = transform(/\65\32\126[\66-\76\109-\114]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/A ~[B-Lm-r]/');
+  });
+
+  it('does not convert coded chars outside of ASCII printable range', () => {
+    let re = transform(/\u0016\u00a9/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\u0016\\u00a9/');
+
+    re = transform(/\u{16}\u{a9}/u, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\u{16}\\u{a9}/u');
+
+    re = transform(/\x16\xa9/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\x16\\xa9/');
+
+    re = transform(/\026/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\026/');
+
+    re = transform(/\22\169/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\22\\169/');
+  });
+
+  it('does not convert class ranges other than included in 0-9, a-z or A-Z', () => {
+    let re = transform(/[\u005a-\u0061]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\u005a-\\u0061]/');
+
+    re = transform(/[\u{5a}-\u{61}]/u, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\u{5a}-\\u{61}]/u');
+
+    re = transform(/[\x5a-\x61]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\x5a-\\x61]/');
+
+    re = transform(/[\054-\061]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\054-\\061]/');
+
+    re = transform(/[\90-\97]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\90-\\97]/');
+  });
+
+  it('escapes converted chars when needed', () => {
+    let re = transform(/[\055]/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\-]/');
+
+    re = transform(/[\u005d\u{5c}\x5e]/u, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/[\\]\\\\\\^]/u');
+
+    re = transform(/\052\43\077\47\125/, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\*\\+\\?\\/\\}/');
+
+    re = transform(/\u005b\u{28}\x29\u005e\u{24}\x2e\u005c\u{7c}\x7b/u, [
+      charCodeToSimpleChar,
+    ]);
+    expect(re.toString()).toBe('/\\[\\(\\)\\^\\$\\.\\\\\\|\\{/u');
+  });
+
+});
\ No newline at end of file