Skip to content

Commit

Permalink
Merge pull request #15177 from rwjblue/take-two-dont-mutate-own-prope…
Browse files Browse the repository at this point in the history
…rties

[BUGFIX beta] Avoid creating enumerable properties on all objects created by DI.
  • Loading branch information
rwjblue authored Apr 27, 2017
2 parents c016eca + 652adee commit d222081
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 15 deletions.
31 changes: 27 additions & 4 deletions packages/container/lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ function deprecatedFactoryFor(container, fullName, options = {}) {
let cacheable = !areInjectionsDynamic(injections) && !areInjectionsDynamic(factoryInjections);

factoryInjections[NAME_KEY] = registry.makeToString(factory, fullName);
injections._debugContainerKey = fullName;
setOwner(injections, container.owner);

let injectedFactory = factory.extend(injections);

Expand All @@ -443,9 +445,6 @@ function injectionsFor(container, fullName) {
let type = splitName[0];

let injections = buildInjections(container, registry.getTypeInjections(type), registry.getInjections(fullName));
injections._debugContainerKey = fullName;

setOwner(injections, container.owner);

return injections;
}
Expand Down Expand Up @@ -488,6 +487,7 @@ function instantiate(factory, props, container, fullName) {
// to create time injections
// TODO: support new'ing for instantiation and merge injections for pure JS Functions
let injections = injectionsFor(container, fullName);
injections._debugContainerKey = fullName;

// Ensure that a container is available to an object during instantiation.
// TODO - remove when Ember reaches v3.0.0
Expand Down Expand Up @@ -613,13 +613,22 @@ class DeprecatedFactoryManager {
class FactoryManager {
constructor(container, factory, fullName, normalizedName) {
this.container = container;
this.owner = container.owner;
this.class = factory;
this.fullName = fullName;
this.normalizedName = normalizedName;
this.madeToString = undefined;
this.injections = undefined;
}

toString() {
if (!this.madeToString) {
this.madeToString = this.container.registry.makeToString(this.class, this.fullName);
}

return this.madeToString;
}

create(options = {}) {

let injections = this.injections;
Expand All @@ -631,7 +640,6 @@ class FactoryManager {
}
let props = assign({}, injections, options);

props[NAME_KEY] = this.madeToString || (this.madeToString = this.container.registry.makeToString(this.class, this.fullName));

if (DEBUG) {
let lazyInjections;
Expand All @@ -656,6 +664,21 @@ class FactoryManager {
injectDeprecatedContainer(prototype, this.container);
}

// required to allow access to things like
// the customized toString, _debugContainerKey,
// owner, etc. without a double extend and without
// modifying the objects properties
if (typeof this.class._initFactory === 'function') {
this.class._initFactory(this);
} else {
// in the non-Ember.Object case we need to still setOwner
// this is required for supporting glimmer environment and
// template instantiation which rely heavily on
// `options[OWNER]` being passed into `create`
// TODO: clean this up, and remove in future versions
setOwner(props, this.owner);
}

return this.class.create(props);
}
}
57 changes: 51 additions & 6 deletions packages/container/tests/container_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getOwner, OWNER } from 'ember-utils';
import { getOwner, OWNER, assign } from 'ember-utils';
import { ENV } from 'ember-environment';
import { get } from 'ember-metal';
import { Registry } from '..';
Expand Down Expand Up @@ -71,7 +71,7 @@ QUnit.test('A factory returned from lookupFactory has a debugkey', function() {
equal(PostFactory._debugContainerKey, 'controller:post', 'factory instance receives _debugContainerKey');
});

QUnit.test('fallback for to create time injections if factory has no extend', function() {
QUnit.test('uses create time injections if factory has no extend', function() {
let registry = new Registry();
let container = registry.container();
let AppleController = factory();
Expand All @@ -85,7 +85,6 @@ QUnit.test('fallback for to create time injections if factory has no extend', fu

let postController = container.lookup('controller:post');

equal(postController._debugContainerKey, 'controller:post', 'instance receives _debugContainerKey');
ok(postController.apple instanceof AppleController, 'instance receives an apple of instance AppleController');
});

Expand Down Expand Up @@ -157,9 +156,6 @@ QUnit.test('An individual factory with a registered injection receives the injec
let postController = container.lookup('controller:post');
let store = container.lookup('store:main');

equal(store._debugContainerKey, 'store:main');

equal(postController._debugContainerKey, 'controller:post');
equal(postController.store, store, 'has the correct store injected');
});

Expand Down Expand Up @@ -742,3 +738,52 @@ QUnit.test('#factoryFor options passed to create clobber injections', (assert) =

assert.equal(instrance.ajax, 'fetch');
});

QUnit.test('#factoryFor does not add properties to the object being instantiated when _initFactory is present', function(assert) {
let owner = {};
let registry = new Registry();
let container = registry.container();

let factory;
class Component {
static _initFactory(_factory) { factory = _factory; }
static create(options) {
let instance = new this();
assign(instance, options);
return instance;
}
}
registry.register('component:foo-bar', Component);

let componentFactory = container.factoryFor('component:foo-bar');
let instance = componentFactory.create();

// note: _guid and isDestroyed are being set in the `factory` constructor
// not via registry/container shenanigans
assert.deepEqual(Object.keys(instance), []);
});

// this is skipped until templates and the glimmer environment do not require `OWNER` to be
// passed in as constructor args
QUnit.skip('#factoryFor does not add properties to the object being instantiated', function(assert) {
let owner = {};
let registry = new Registry();
let container = registry.container();

let factory;
class Component {
static create(options) {
let instance = new this();
assign(instance, options);
return instance;
}
}
registry.register('component:foo-bar', Component);

let componentFactory = container.factoryFor('component:foo-bar');
let instance = componentFactory.create();

// note: _guid and isDestroyed are being set in the `factory` constructor
// not via registry/container shenanigans
assert.deepEqual(Object.keys(instance), []);
});
9 changes: 9 additions & 0 deletions packages/ember-metal/lib/meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class Meta {
this._chains = undefined;
this._tag = undefined;
this._tags = undefined;
this._factory = undefined;

// initial value for all flags right now is false
// see FLAGS const for detailed list of flags used
Expand Down Expand Up @@ -345,6 +346,14 @@ export class Meta {
obj[key] = value;
}
}

set factory(factory) {
this._factory = factory;
}

get factory() {
return this._factory;
}
}

const NODE_STACK = [];
Expand Down
11 changes: 9 additions & 2 deletions packages/ember-runtime/lib/system/core_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function makeCtor() {
// possible.

let wasApplied = false;
let initProperties;
let initProperties, initFactory;

class Class {
constructor() {
Expand All @@ -68,6 +68,11 @@ function makeCtor() {
let m = meta(this);
let proto = m.proto;
m.proto = this;

if (initFactory) {
m.factory = initFactory;
initFactory = null;
}
if (initProperties) {
// capture locally so we can clear the closed over variable
let props = initProperties;
Expand Down Expand Up @@ -180,6 +185,7 @@ function makeCtor() {
}

static _initProperties(args) { initProperties = args; }
static _initFactory(factory) { initFactory = factory; }

static proto() {
let superclass = Class.superclass;
Expand Down Expand Up @@ -540,7 +546,8 @@ CoreObject.PrototypeMixin = Mixin.create({
toString() {
let hasToStringExtension = typeof this.toStringExtension === 'function';
let extension = hasToStringExtension ? `:${this.toStringExtension()}` : '';
let ret = `<${this[NAME_KEY] || this.constructor.toString()}:${guidFor(this)}${extension}>`;

let ret = `<${this[NAME_KEY] || meta(this).factory || this.constructor.toString()}:${guidFor(this)}${extension}>`;

return ret;
}
Expand Down
50 changes: 47 additions & 3 deletions packages/ember-runtime/lib/system/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
@submodule ember-runtime
*/

import { symbol } from 'ember-utils';
import { on } from 'ember-metal';
import { symbol, NAME_KEY, OWNER } from 'ember-utils';
import { on, descriptor, meta as metaFor } from 'ember-metal';
import CoreObject from './core_object';
import Observable from '../mixins/observable';
import { assert } from 'ember-debug';
import { DEBUG } from 'ember-env-flags';

let OVERRIDE_CONTAINER_KEY = symbol('OVERRIDE_CONTAINER_KEY');
let OVERRIDE_OWNER = symbol('OVERRIDE_OWNER');

/**
`Ember.Object` is the main base class for all Ember objects. It is a subclass
of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details,
Expand All @@ -21,7 +24,48 @@ import { DEBUG } from 'ember-env-flags';
@uses Ember.Observable
@public
*/
const EmberObject = CoreObject.extend(Observable);
const EmberObject = CoreObject.extend(Observable, {
_debugContainerKey: descriptor({
enumerable: false,
get() {
if (this[OVERRIDE_CONTAINER_KEY]) {
return this[OVERRIDE_CONTAINER_KEY];
}

let meta = metaFor(this);
let { factory } = meta;

return factory && factory.fullName;
},

// we need a setter here largely to support the legacy
// `owner._lookupFactory` and its double extend
set(value) {
this[OVERRIDE_CONTAINER_KEY] = value;
}
}),

[OWNER]: descriptor({
enumerable: false,
get() {
if (this[OVERRIDE_OWNER]) {
return this[OVERRIDE_OWNER];
}

let meta = metaFor(this);
let { factory } = meta;

return factory && factory.owner;
},

// we need a setter here largely to support the legacy
// `owner._lookupFactory` and its double extend
set(value) {
this[OVERRIDE_OWNER] = value;
}
})
});

EmberObject.toString = () => 'Ember.Object';

export let FrameworkObject = EmberObject;
Expand Down

0 comments on commit d222081

Please sign in to comment.