From 92a6118747593d3782a4f08d01e5c43aacf7322c Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 27 May 2018 22:36:01 -0700 Subject: [PATCH] [Breaking] update to match latest spec Per https://github.com/tc39/proposal-string-matchall/pull/33 / https://github.com/tc39/proposal-string-matchall/pull/35 --- .eslintrc | 8 +++- helpers/MatchAllIterator.js | 41 ++++++++++--------- helpers/RegExpStringIterator.js | 72 +++++++++++++++++++++++---------- implementation.js | 25 +++++------- package.json | 4 +- regexp-matchall.js | 18 ++++++--- shim.js | 15 +++++++ test/shimmed.js | 2 +- test/tests.js | 34 ++++++++-------- 9 files changed, 137 insertions(+), 82 deletions(-) diff --git a/.eslintrc b/.eslintrc index e01b277..381d7ed 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,11 +4,12 @@ "extends": "@ljharb", "rules": { + "complexity": [2, 12], "func-name-matching": 0, "max-nested-callbacks": [2, 3], - "max-params": [2, 3], + "max-params": [2, 4], "max-statements-per-line": [2, { "max": 2 }], - "max-statements": [2, 22], + "max-statements": [2, 24], "new-cap": [2, { "capIsNewExceptions": [ "IsRegExp", @@ -27,8 +28,11 @@ "RegExpExec", "CreateIterResultObject", "AdvanceStringIndex", + "GetIntrinsic", + "ObjectCreate", ], }], "no-restricted-syntax": [2, "BreakStatement", "ContinueStatement", "DebuggerStatement", "LabeledStatement", "WithStatement"], + "operator-linebreak": [2, "before"], } } diff --git a/helpers/MatchAllIterator.js b/helpers/MatchAllIterator.js index 2e02e76..b2c4024 100644 --- a/helpers/MatchAllIterator.js +++ b/helpers/MatchAllIterator.js @@ -5,29 +5,34 @@ var flagsGetter = require('regexp.prototype.flags'); var RegExpStringIterator = require('./RegExpStringIterator'); var OrigRegExp = RegExp; -var hasFlags = typeof (/a/).flags === 'string'; module.exports = function MatchAllIterator(R, O) { - if (!ES.IsRegExp(R)) { - throw new TypeError('MatchAllIterator requires a regex'); - } var S = ES.ToString(O); - var C = ES.SpeciesConstructor(R, OrigRegExp); - var flags = ES.Get(R, 'flags'); - var matcher; - var actualFlags = typeof flags === 'string' ? flags : flagsGetter(R); - if (hasFlags) { - matcher = new C(R, actualFlags); // ES.Construct(C, [R, actualFlags]); - } else if (C === OrigRegExp) { - // workaround for older engines that lack RegExp.prototype.flags - matcher = new C(R.source, actualFlags); // ES.Construct(C, [R.source, actualFlags]); + var matcher, global, fullUnicode, flags; + if (ES.IsRegExp(R)) { + var C = ES.SpeciesConstructor(R, OrigRegExp); + flags = ES.Get(R, 'flags'); + if (typeof flags === 'string') { + matcher = new C(R, flags); // ES.Construct(C, [R, flags]); + } else if (C === OrigRegExp) { + // workaround for older engines that lack RegExp.prototype.flags + matcher = new C(R.source, flagsGetter(R)); // ES.Construct(C, [R.source, flagsGetter(R)]); + } else { + matcher = new C(R, flagsGetter(R)); // ES.Construct(C, [R, flagsGetter(R)]); + } + global = ES.ToBoolean(ES.Get(matcher, 'global')); + fullUnicode = ES.ToBoolean(ES.Get(matcher, 'unicode')); + var lastIndex = ES.ToLength(ES.Get(R, 'lastIndex')); + ES.Set(matcher, 'lastIndex', lastIndex, true); } else { - matcher = new C(R, actualFlags); // ES.Construct(C, [R, actualFlags]); + flags = 'g'; + matcher = new OrigRegExp(R, flags); + global = true; + fullUnicode = false; + if (ES.Get(matcher, 'lastIndex') !== 0) { + throw new TypeError('Assertion failed: newly constructed RegExp had a lastIndex !== 0. Please report this!'); + } } - var global = ES.ToBoolean(ES.Get(R, 'global')); - var fullUnicode = ES.ToBoolean(ES.Get(R, 'unicode')); - var lastIndex = ES.ToLength(ES.Get(R, 'lastIndex')); - ES.Set(matcher, 'lastIndex', lastIndex, true); return new RegExpStringIterator(matcher, S, global, fullUnicode); }; diff --git a/helpers/RegExpStringIterator.js b/helpers/RegExpStringIterator.js index 06ab54e..01f06f6 100644 --- a/helpers/RegExpStringIterator.js +++ b/helpers/RegExpStringIterator.js @@ -2,18 +2,16 @@ var define = require('define-properties'); var ES = require('es-abstract'); +var GetIntrinsic = require('es-abstract/GetIntrinsic'); var hasSymbols = require('has-symbols')(); var hidden = require('./hidden')(); +var undefined; // eslint-disable-line no-shadow-restricted-names -/* eslint max-params: 1 */ var RegExpStringIterator = function RegExpStringIterator(R, S, global, fullUnicode) { if (ES.Type(S) !== 'String') { throw new TypeError('S must be a string'); } - if (!ES.IsRegExp(R)) { - throw new TypeError('R must be a RegExp'); - } if (ES.Type(global) !== 'Boolean') { throw new TypeError('global must be a boolean'); } @@ -23,34 +21,43 @@ var RegExpStringIterator = function RegExpStringIterator(R, S, global, fullUnico hidden.set(this, '[[IteratingRegExp]]', R); hidden.set(this, '[[IteratedString]]', S); hidden.set(this, '[[Global]]', global); - hidden.set(this, '[[FullUnicode]]', fullUnicode); + hidden.set(this, '[[Unicode]]', fullUnicode); hidden.set(this, '[[Done]]', false); }; +var IteratorPrototype = GetIntrinsic('%IteratorPrototype%', true); +if (IteratorPrototype) { + RegExpStringIterator.prototype = ES.ObjectCreate(IteratorPrototype); +} + define(RegExpStringIterator.prototype, { - /* eslint complexity: 1, max-statements: 1 */ next: function next() { var O = this; if (ES.Type(O) !== 'Object') { throw new TypeError('receiver must be an object'); } - if (!(this instanceof RegExpStringIterator) || !hidden.has(O, '[[IteratingRegExp]]') || !hidden.has(O, '[[IteratedString]]')) { + if ( + !(O instanceof RegExpStringIterator) + || !hidden.has(O, '[[IteratingRegExp]]') + || !hidden.has(O, '[[IteratedString]]') + || !hidden.has(O, '[[Global]]') + || !hidden.has(O, '[[Unicode]]') + || !hidden.has(O, '[[Done]]') + ) { throw new TypeError('"this" value must be a RegExpStringIterator instance'); } - if (hidden.get(this, '[[Done]]')) { - return ES.CreateIterResultObject(null, true); + if (hidden.get(O, '[[Done]]')) { + return ES.CreateIterResultObject(undefined, true); } - var R = hidden.get(this, '[[IteratingRegExp]]'); - var S = hidden.get(this, '[[IteratedString]]'); - var global = hidden.get(this, '[[Global]]'); - var fullUnicode = hidden.get(this, '[[FullUnicode]]'); - + var R = hidden.get(O, '[[IteratingRegExp]]'); + var S = hidden.get(O, '[[IteratedString]]'); + var global = hidden.get(O, '[[Global]]'); + var fullUnicode = hidden.get(O, '[[Unicode]]'); var match = ES.RegExpExec(R, S); if (match === null) { - hidden.set(this, '[[Done]]', true); - return ES.CreateIterResultObject(null, true); + hidden.set(O, '[[Done]]', true); + return ES.CreateIterResultObject(undefined, true); } - if (global) { var matchStr = ES.ToString(ES.Get(match, '0')); if (matchStr === '') { @@ -60,13 +67,36 @@ define(RegExpStringIterator.prototype, { } return ES.CreateIterResultObject(match, false); } - hidden.set(this, '[[Done]]', true); + hidden.set(O, '[[Done]]', true); return ES.CreateIterResultObject(match, false); } }); -if (hasSymbols && Symbol.toStringTag) { - RegExpStringIterator.prototype[Symbol.toStringTag] = 'RegExp String Iterator'; - RegExpStringIterator.prototype[Symbol.iterator] = function () { return this; }; +if (hasSymbols) { + var defineP = Object.defineProperty; + if (Symbol.toStringTag) { + if (defineP) { + defineP(RegExpStringIterator.prototype, Symbol.toStringTag, { + configurable: true, + enumerable: false, + value: 'RegExp String Iterator', + writable: false + }); + } else { + RegExpStringIterator.prototype[Symbol.toStringTag] = 'RegExp String Iterator'; + } + } + + if (!IteratorPrototype && Symbol.iterator) { + var func = {}; + func[Symbol.iterator] = RegExpStringIterator.prototype[Symbol.iterator] || function SymbolIterator() { + return this; + }; + var predicate = {}; + predicate[Symbol.iterator] = function () { + return RegExpStringIterator.prototype[Symbol.iterator] !== func[Symbol.iterator]; + }; + define(RegExpStringIterator.prototype, func, predicate); + } } module.exports = RegExpStringIterator; diff --git a/implementation.js b/implementation.js index 085ec2f..3b0f2a9 100644 --- a/implementation.js +++ b/implementation.js @@ -3,25 +3,20 @@ var ES = require('es-abstract'); var hasSymbols = require('has-symbols')(); -var OrigRegExp = RegExp; - var MatchAllIterator = require('./helpers/MatchAllIterator'); module.exports = function matchAll(regexp) { var O = ES.RequireObjectCoercible(this); - var R; - if (ES.IsRegExp(regexp)) { - R = regexp; - } else { - R = new OrigRegExp(regexp, 'g'); - } - var matcher; - if (hasSymbols && typeof Symbol.matchAll === 'symbol') { - matcher = ES.GetMethod(R, Symbol.matchAll); - } - if (typeof matcher !== 'undefined') { - return ES.Call(matcher, R, [O]); + + if (typeof regexp !== 'undefined' && regexp !== null) { + var matcher; + if (hasSymbols && typeof Symbol.matchAll === 'symbol') { + matcher = ES.GetMethod(regexp, Symbol.matchAll); + } + if (typeof matcher !== 'undefined') { + return ES.Call(matcher, regexp, [O]); + } } - return MatchAllIterator(R, O); + return MatchAllIterator(regexp, O); }; diff --git a/package.json b/package.json index 7b3ba20..afc8a17 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "tests-only": "npm run test:module && npm run test:shim", "test:module": "node test", "test:shim": "node test/shimmed", - "prelint": "evalmd *.md", + "prelint": "evalmd *.md", "lint": "eslint ." }, "repository": { @@ -36,7 +36,7 @@ "homepage": "https://github.com/ljharb/String.prototype.matchAll#readme", "dependencies": { "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", + "es-abstract": "^1.12.0", "function-bind": "^1.1.1", "has-symbols": "^1.0.0", "regexp.prototype.flags": "^1.2.0" diff --git a/regexp-matchall.js b/regexp-matchall.js index eac4e9f..efa3e57 100644 --- a/regexp-matchall.js +++ b/regexp-matchall.js @@ -3,16 +3,22 @@ var ES = require('es-abstract'); var MatchAllIterator = require('./helpers/MatchAllIterator'); -var regexMatchAll = function symbolMatchAll(string) { - var R = this; // eslint-disable-line no-invalid-this - if (!ES.IsRegExp(R)) { - throw new TypeError('"this" value must be a RegExp'); +var regexMatchAll = function SymbolMatchAll(string) { + var R = this; + if (ES.Type(R) !== 'Object') { + throw new TypeError('"this" value must be an Object'); } return MatchAllIterator(R, string); }; -if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(regexMatchAll, 'name').configurable) { - Object.defineProperty(regexMatchAll, 'name', { value: '[Symbol.matchAll]' }); +var defineP = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +if (defineP && gOPD) { + var desc = gOPD(regexMatchAll, 'name'); + if (desc && desc.configurable) { + defineP(regexMatchAll, 'name', { value: '[Symbol.matchAll]' }); + } } module.exports = regexMatchAll; diff --git a/shim.js b/shim.js index 44e558a..8a76232 100644 --- a/shim.js +++ b/shim.js @@ -5,6 +5,9 @@ var hasSymbols = require('has-symbols')(); var getPolyfill = require('./polyfill'); var regexMatchAll = require('./regexp-matchall'); +var defineP = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + module.exports = function shimMatchAll() { var polyfill = getPolyfill(); define( @@ -21,6 +24,18 @@ module.exports = function shimMatchAll() { { matchAll: function () { return Symbol.matchAll !== symbol; } } ); + if (defineP && gOPD) { + var desc = gOPD(Symbol, symbol); + if (!desc || desc.configurable) { + defineP(Symbol, symbol, { + configurable: false, + enumerable: false, + value: symbol, + writable: false + }); + } + } + var func = {}; func[symbol] = RegExp.prototype[symbol] || regexMatchAll; var predicate = {}; diff --git a/test/shimmed.js b/test/shimmed.js index 36af351..7caec19 100644 --- a/test/shimmed.js +++ b/test/shimmed.js @@ -38,7 +38,7 @@ test('shimmed', function (t) { if (functionNamesConfigurable) { s2t.equal(RegExp.prototype[Symbol.matchAll].name, '[Symbol.matchAll]', 'RegExp.prototype[Symbol.matchAll] has name "[Symbol.matchAll]"'); } else { - s2t.equal(RegExp.prototype[Symbol.matchAll].name, 'symbolMatchAll', 'RegExp.prototype[Symbol.matchAll] has best guess name "symbolMatchAll"'); + s2t.equal(RegExp.prototype[Symbol.matchAll].name, 'SymbolMatchAll', 'RegExp.prototype[Symbol.matchAll] has best guess name "SymbolMatchAll"'); } s2t.end(); }); diff --git a/test/tests.js b/test/tests.js index 9c00cfa..ef4a383 100644 --- a/test/tests.js +++ b/test/tests.js @@ -39,7 +39,7 @@ var testResults = function (t, iterator, expectedResults, item) { st.equal(result.done, expected.done, 'result ' + (index + 1) + ' is ' + (expected.done ? '' : 'not ') + 'done'); st.test('result ' + (index + 1), { skip: result.done !== expected.done }, function (s2t) { if (expected.done) { - s2t.equal(result.value, null, 'result ' + (index + 1) + ' value is null'); + s2t.equal(result.value, undefined, 'result ' + (index + 1) + ' value is undefined'); } else { s2t.equal(Array.isArray(result.value), true, 'result ' + (index + 1) + ' value is an array'); s2t.deepEqual(entries(result.value || {}), entries(expected.value || {}), 'result ' + (index + 1) + ' has the same entries'); @@ -54,28 +54,28 @@ var testResults = function (t, iterator, expectedResults, item) { module.exports = function (matchAll, regexMatchAll, t) { t.test('non-regexes', function (st) { var notRegexes = [ - [null, [{ value: null, done: true }]], + [null, [{ value: undefined, done: true }]], [undefined, [ { value: assign([''], groups({ index: 0, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 1, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 2, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 3, input: 'abc' })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]], - [NaN, [{ value: null, done: true }]], - [42, [{ value: null, done: true }]], - [new Date(), [{ value: null, done: true }]], + [NaN, [{ value: undefined, done: true }]], + [42, [{ value: undefined, done: true }]], + [new Date(), [{ value: undefined, done: true }]], [{}, [ { value: assign(['b'], groups({ index: 1, input: 'abc' })), done: false }, { value: assign(['c'], groups({ index: 2, input: 'abc' })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]], [[], [ { value: assign([''], groups({ index: 0, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 1, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 2, input: 'abc' })), done: false }, { value: assign([''], groups({ index: 3, input: 'abc' })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]] ]; var str = 'abc'; @@ -99,7 +99,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign(['a'], groups({ index: 0, input: str })), done: false }, { value: assign(['a'], groups({ index: 1, input: str })), done: false }, { value: assign(['c'], groups({ index: 3, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(st, matchAll(strObj, regex), expectedResults); st.end(); @@ -117,7 +117,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign(['a'], groups({ index: 0, input: str })), done: false }, { value: assign(['a'], groups({ index: 1, input: str })), done: false }, { value: assign(['c'], groups({ index: 3, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, regex), expectedResults); s2t.end(); @@ -139,7 +139,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign(['A'], groups({ index: 0, input: str })), done: false }, { value: assign(['a'], groups({ index: 1, input: str })), done: false }, { value: assign(['C'], groups({ index: 3, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, regex), expectedResults); return s2t.end(); @@ -152,7 +152,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign(['A'], groups({ index: 0, input: str })), done: false }, { value: assign(['a'], groups({ index: 2, input: str })), done: false }, { value: assign(['C'], groups({ index: 6, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, regex), expectedResults); s2t.end(); @@ -163,7 +163,7 @@ module.exports = function (matchAll, regexMatchAll, t) { var regex = /[bc]/i; var expectedResults = [ { value: assign(['B'], groups({ index: 2, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, regex), expectedResults); s2t.end(); @@ -182,7 +182,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign(['a'], groups({ index: 0, input: str })), done: false }, { value: assign(['a'], groups({ index: 1, input: str })), done: false }, { value: assign(['c'], groups({ index: 3, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(st, iterator, expectedResults); st.end(); @@ -197,7 +197,7 @@ module.exports = function (matchAll, regexMatchAll, t) { { value: assign([''], groups({ index: 2, input: str })), done: false }, { value: assign([''], groups({ index: 3, input: str })), done: false }, { value: assign([''], groups({ index: 4, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, /\B/g), expectedResults); s2t.end(); @@ -205,7 +205,7 @@ module.exports = function (matchAll, regexMatchAll, t) { st.test('sticky', { skip: !hasSticky }, function (s2t) { var expectedResults = [ - { value: null, done: true } + { value: undefined, done: true } ]; /* eslint no-invalid-regexp: [2, { "allowConstructorFlags": ["y"] }] */ testResults(s2t, matchAll(str, new RegExp('\\B', 'y')), expectedResults); @@ -215,7 +215,7 @@ module.exports = function (matchAll, regexMatchAll, t) { st.test('unflagged', function (s2t) { var expectedResults = [ { value: assign([''], groups({ index: 1, input: str })), done: false }, - { value: null, done: true } + { value: undefined, done: true } ]; testResults(s2t, matchAll(str, /\B/), expectedResults); s2t.end();