Add support for extending builtins (#7020)

This commit is contained in:
Nicolò Ribaudo
2017-12-20 20:46:00 +01:00
committed by Henry Zhu
parent 148fe7d3ff
commit 0c885b3200
22 changed files with 244 additions and 7 deletions

View File

@@ -4,9 +4,15 @@
## Caveats
Built-in classes such as `Date`, `Array`, `DOM` etc cannot be properly subclassed
due to limitations in ES5 (for the [classes](http://babeljs.io/docs/plugins/transform-classes) plugin).
You can try to use [@babel/plugin-transform-builtin-extend](https://github.com/loganfsmyth/babel-plugin-transform-builtin-extend) based on `Object.setPrototypeOf` and `Reflect.construct`, but it also has some limitations.
When extending a native class (e.g., `class extends Array {}`), the super class
needs to be wrapped. This is needed to workaround two problems:
- Babel transpiles classes using `SuperClass.apply(/* ... */)`, but native
classes aren't callable and thus throw in this case.
- Some built-in functions (like `Array`) always return a new object. Instead of
returning it, Babel should treat it as the new `this`.
The wrapper works on IE11 and every other browser with `Object.setPrototypeOf` or `__proto__` as fallback.
There is **NO IE <= 10 support**. If you need IE <= 10 it's recommended that you don't extend natives.
## Examples

View File

@@ -10,7 +10,8 @@
"@babel/helper-define-map": "7.0.0-beta.35",
"@babel/helper-function-name": "7.0.0-beta.35",
"@babel/helper-optimise-call-expression": "7.0.0-beta.35",
"@babel/helper-replace-supers": "7.0.0-beta.35"
"@babel/helper-replace-supers": "7.0.0-beta.35",
"globals": "^11.1.0"
},
"keywords": [
"babel-plugin"

View File

@@ -3,6 +3,15 @@ import VanillaTransformer from "./vanilla";
import annotateAsPure from "@babel/helper-annotate-as-pure";
import nameFunction from "@babel/helper-function-name";
import { types as t } from "@babel/core";
import globals from "globals";
const getBuiltinClasses = category =>
Object.keys(globals[category]).filter(name => /^[A-Z]/.test(name));
const builtinClasses = new Set([
...getBuiltinClasses("builtin"),
...getBuiltinClasses("browser"),
]);
export default function(api, options) {
const { loose } = options;
@@ -54,7 +63,9 @@ export default function(api, options) {
node[VISITED] = true;
path.replaceWith(new Constructor(path, state.file).run());
path.replaceWith(
new Constructor(path, state.file, builtinClasses).run(),
);
if (path.isCallExpression()) {
annotateAsPure(path);

View File

@@ -4,6 +4,8 @@ import optimiseCall from "@babel/helper-optimise-call-expression";
import * as defineMap from "@babel/helper-define-map";
import { traverse, template, types as t } from "@babel/core";
type ReadonlySet<T> = Set<T> | { has(val: T): boolean };
const noMethodVisitor = {
"FunctionExpression|FunctionDeclaration"(path) {
path.skip();
@@ -61,7 +63,7 @@ const findThisesVisitor = traverse.visitors.merge([
]);
export default class ClassTransformer {
constructor(path: NodePath, file) {
constructor(path: NodePath, file, builtinClasses: ReadonlySet<string>) {
this.parent = path.parent;
this.scope = path.scope;
this.node = path.node;
@@ -93,6 +95,12 @@ export default class ClassTransformer {
this.superName = this.node.superClass || t.identifier("Function");
this.isDerived = !!this.node.superClass;
const { name } = this.superName;
this.extendsNative =
this.isDerived &&
builtinClasses.has(name) &&
!this.scope.hasBinding(name, /* noGlobals */ true);
}
run() {
@@ -112,7 +120,13 @@ export default class ClassTransformer {
//
if (this.isDerived) {
closureArgs.push(superName);
if (this.extendsNative) {
closureArgs.push(
t.callExpression(this.file.addHelper("wrapNativeSuper"), [superName]),
);
} else {
closureArgs.push(superName);
}
superName = this.scope.generateUidIdentifierBasedOnNode(superName);
closureParams.push(superName);

View File

@@ -0,0 +1,23 @@
function B() {}
B.b = function() {
return 'B.b';
};
class C extends B {}
assert.equal(Object.getPrototypeOf(C), B);
assert.equal(Object.getPrototypeOf(C.prototype), B.prototype);
assert.equal(C.b(), 'B.b');
class D extends Object {}
assert.ok(D instanceof Object)
assert.ok(D.prototype instanceof Object);
assert.equal(D.keys, Object.keys);
class E {}
assert.equal(Object.getPrototypeOf(E), Function.prototype);
assert.equal(Object.getPrototypeOf(E.prototype), Object.prototype);
assert.isFalse('keys' in E);

View File

@@ -0,0 +1,4 @@
// JSON is wrapped because it starts with an uppercase letter, but it
// should not be possible to extend it anyway.
assert.throws(() => class BetterJSON extends JSON {});

View File

@@ -0,0 +1,3 @@
{
"plugins": ["transform-classes", "transform-block-scoping"]
}

View File

@@ -0,0 +1,34 @@
// Imported from
// https://github.com/WebReflection/babel-plugin-transform-builtin-classes/blob/85efe1374e1c59a8323c7eddd4326f6c93d9f64f/test/test.js
class List extends Array {
constructor(value) {
super().push(value);
}
push(value) {
super.push(value);
return this;
}
}
assert.ok(new List(1) instanceof List, 'new List is an instanceof List');
assert.ok(new List(2) instanceof Array, 'new List is an instanceof Array');
var l = new List(3);
assert.ok(l.length === 1 && l[0] === 3, 'constructor pushes an entry');
assert.ok(l.push(4) === l && l.length === 2 && l.join() === '3,4', 'method override works');
class SecondLevel extends List {
method() {
return this;
}
}
assert.ok(new SecondLevel(1) instanceof SecondLevel, 'new SecondLevel is an instanceof SecondLevel');
assert.ok(new SecondLevel(2) instanceof List, 'new SecondLevel is an instanceof List');
assert.ok(new SecondLevel(3) instanceof Array, 'new SecondLevel is an instanceof Array');
var s = new SecondLevel(4);
assert.ok(s.length === 1 && s[0] === 4, 'constructor pushes an entry');
assert.ok(s.push(5) === s && s.length === 2 && s.join() === '4,5', 'inherited override works');
assert.ok(s.method() === s, 'new method works');

View File

@@ -0,0 +1,3 @@
{
"plugins": ["transform-classes","transform-block-scoping"]
}

View File

@@ -0,0 +1 @@
class List extends Array {}

View File

@@ -0,0 +1,4 @@
class List extends Array {}
assert.ok(new List instanceof List);
assert.ok(new List instanceof Array);

View File

@@ -0,0 +1,23 @@
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
var _gPO = Object.getPrototypeOf || function _gPO(o) { return o.__proto__; };
var _sPO = Object.setPrototypeOf || function _sPO(o, p) { o.__proto__ = p; };
var _construct = typeof Reflect === "object" && Reflect.construct || function _construct(Parent, args, Class) { var Constructor, a = [null]; a.push.apply(a, args); Constructor = Parent.bind.apply(Parent, a); return _sPO(new Constructor(), Class.prototype); };
var _cache = typeof Map === "function" && new Map();
function _wrapNativeSuper(Class) { if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() {} Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writeable: true, configurable: true } }); return _sPO(Wrapper, _sPO(function Super() { return _construct(Class, arguments, _gPO(this).constructor); }, Class)); }
var List =
/*#__PURE__*/
function (_Array) {
_inheritsLoose(List, _Array);
function List() {
return _Array.apply(this, arguments) || this;
}
return List;
}(_wrapNativeSuper(Array));

View File

@@ -0,0 +1,6 @@
{
"plugins": [
["transform-classes", { "loose": true }],
"transform-block-scoping"
]
}

View File

@@ -0,0 +1,3 @@
class Array {}
class List extends Array {}

View File

@@ -0,0 +1,16 @@
let Array = function Array() {
babelHelpers.classCallCheck(this, Array);
};
let List =
/*#__PURE__*/
function (_Array) {
babelHelpers.inherits(List, _Array);
function List() {
babelHelpers.classCallCheck(this, List);
return babelHelpers.possibleConstructorReturn(this, (List.__proto__ || Object.getPrototypeOf(List)).apply(this, arguments));
}
return List;
}(Array);

View File

@@ -0,0 +1,3 @@
{
"plugins": ["transform-classes", "external-helpers"]
}

View File

@@ -0,0 +1 @@
class List extends Array {}

View File

@@ -0,0 +1,4 @@
class List extends Array {}
assert.ok(new List instanceof List);
assert.ok(new List instanceof Array);

View File

@@ -0,0 +1,29 @@
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _gPO = Object.getPrototypeOf || function _gPO(o) { return o.__proto__; };
var _sPO = Object.setPrototypeOf || function _sPO(o, p) { o.__proto__ = p; };
var _construct = typeof Reflect === "object" && Reflect.construct || function _construct(Parent, args, Class) { var Constructor, a = [null]; a.push.apply(a, args); Constructor = Parent.bind.apply(Parent, a); return _sPO(new Constructor(), Class.prototype); };
var _cache = typeof Map === "function" && new Map();
function _wrapNativeSuper(Class) { if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() {} Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writeable: true, configurable: true } }); return _sPO(Wrapper, _sPO(function Super() { return _construct(Class, arguments, _gPO(this).constructor); }, Class)); }
var List =
/*#__PURE__*/
function (_Array) {
_inherits(List, _Array);
function List() {
_classCallCheck(this, List);
return _possibleConstructorReturn(this, (List.__proto__ || Object.getPrototypeOf(List)).apply(this, arguments));
}
return List;
}(_wrapNativeSuper(Array));

View File

@@ -0,0 +1,3 @@
{
"plugins": ["transform-classes", "transform-block-scoping"]
}