Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

WIP - feat($injector): add support for non-string IDs (and other minor stuff) #14998

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions docs/content/error/$injector/itkn.ngdoc

This file was deleted.

17 changes: 15 additions & 2 deletions src/apis.js
Original file line number Diff line number Diff line change
@@ -36,14 +36,19 @@ function hashKey(obj, nextUidFn) {
/**
* HashMap which can use objects as keys
*/
function HashMap(array, isolatedUid) {
function HashMap(seedData, isolatedUid) {
if (isolatedUid) {
var uid = 0;
this.nextUid = function() {
return ++uid;
};
}
forEach(array, this.put, this);

if (seedData) {
var putFn = isArray(seedData) ?
this.put : reverseParams(this.put.bind(this));
forEach(seedData, putFn, this);
}
}
HashMap.prototype = {
/**
@@ -63,6 +68,14 @@ HashMap.prototype = {
return this[hashKey(key, this.nextUid)];
},

/**
* @param key
* @returns {boolean} whether a value is stored under the specified key
*/
has: function(key) {
return this.hasOwnProperty(hashKey(key, this.nextUid));
},

/**
* Remove the key/value pair
* @param key
139 changes: 80 additions & 59 deletions src/auto/injector.js
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/

var PROVIDER_ID_SUFFIX = 'Provider';
var ARROW_ARG = /^([^\(]+?)=>/;
var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
@@ -74,7 +75,7 @@ function stringifyFn(fn) {
// Support: Chrome 50-51 only
// Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51
// (See https://github.com/angular/angular.js/issues/14487.)
// TODO (gkalpak): Remove workaround when Chrome v52 is released
// TODO(gkalpak): Remove workaround when Chrome v52 is released
return Function.prototype.toString.call(fn) + ' ';
}

@@ -129,6 +130,14 @@ function annotate(fn, strictDi, name) {
return $inject;
}

function getProviderId(id) {
return !isString(id) ? id : id + PROVIDER_ID_SUFFIX;
}

function stringifyIdForError(id, suffix) {
return isString(id) ? id : id + suffix;
}

///////////////////////////////////////

/**
@@ -647,36 +656,33 @@ function annotate(fn, strictDi, name) {
function createInjector(modulesToLoad, strictDi) {
strictDi = (strictDi === true);
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
loadedModules = new HashMap([], true),
providerCache = {
loadedModules = new HashMap(null, true),
providerCache = new HashMap({
$provide: {
provider: supportObject(provider),
factory: supportObject(factory),
service: supportObject(service),
value: supportObject(value),
constant: supportObject(constant),
decorator: decorator
}
},
providerInjector = (providerCache.$injector =
createInternalInjector(providerCache, function(serviceName, caller) {
if (angular.isString(caller)) {
path.push(caller);
}
provider: supportObject(provider),
factory: supportObject(factory),
service: supportObject(service),
value: supportObject(value),
constant: supportObject(constant),
decorator: decorator
}
}),
instanceCache = new HashMap(),
providerInjector =
createInternalInjector(providerCache, function(serviceName) {
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),
instanceCache = {},
}, ' (provider)'),
protoInstanceInjector =
createInternalInjector(instanceCache, function(serviceName, caller) {
var provider = providerInjector.get(serviceName + providerSuffix, caller);
return instanceInjector.invoke(
provider.$get, provider, undefined, serviceName);
createInternalInjector(instanceCache, function(serviceName) {
var provider = providerInjector.get(getProviderId(serviceName));
return instanceInjector.invoke(provider.$get, provider);
}),
instanceInjector = protoInstanceInjector;

providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
providerCache.put('$injector', providerInjector);
providerCache.put(getProviderId('$injector'), {$get: valueFn(protoInstanceInjector)});

var runBlocks = loadModules(modulesToLoad);
instanceInjector = protoInstanceInjector.get('$injector');
instanceInjector.strictDi = strictDi;
@@ -690,7 +696,7 @@ function createInjector(modulesToLoad, strictDi) {

function supportObject(delegate) {
return function(key, value) {
if (isObject(key)) {
if ((arguments.length === 1) && isObject(key)) {
forEach(key, reverseParams(delegate));
} else {
return delegate(key, value);
@@ -706,7 +712,10 @@ function createInjector(modulesToLoad, strictDi) {
if (!provider_.$get) {
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
}
return (providerCache[name + providerSuffix] = provider_);

providerCache.put(getProviderId(name), provider_);

return provider_;
}

function enforceReturnValue(name, factory) {
@@ -731,16 +740,18 @@ function createInjector(modulesToLoad, strictDi) {
}]);
}

function value(name, val) { return factory(name, valueFn(val), false); }
function value(name, val) {
return factory(name, valueFn(val), false);
}

function constant(name, value) {
assertNotHasOwnProperty(name, 'constant');
providerCache[name] = value;
instanceCache[name] = value;
providerCache.put(name, value);
instanceCache.put(name, value);
}

function decorator(serviceName, decorFn) {
var origProvider = providerInjector.get(serviceName + providerSuffix),
var origProvider = providerInjector.get(getProviderId(serviceName)),
orig$get = origProvider.$get;

origProvider.$get = function() {
@@ -775,9 +786,7 @@ function createInjector(modulesToLoad, strictDi) {
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
runInvokeQueue(moduleFn._invokeQueue);
runInvokeQueue(moduleFn._configBlocks);
} else if (isFunction(module)) {
runBlocks.push(providerInjector.invoke(module));
} else if (isArray(module)) {
} else if (isFunction(module) || isArray(module)) {
runBlocks.push(providerInjector.invoke(module));
} else {
assertArgFn(module, 'module');
@@ -805,29 +814,45 @@ function createInjector(modulesToLoad, strictDi) {
// internal Injector
////////////////////////////////////

function createInternalInjector(cache, factory) {
function createInternalInjector(cache, factory, displayNameSuffix) {
if (!isString(displayNameSuffix)) {
displayNameSuffix = '';
}

function getService(serviceName, caller) {
if (cache.hasOwnProperty(serviceName)) {
if (cache[serviceName] === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
serviceName + ' <- ' + path.join(' <- '));
}
return cache[serviceName];
} else {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
cache[serviceName] = factory(serviceName, caller);
return cache[serviceName];
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
var hasCaller = isDefined(caller);
var hadInstance = cache.has(serviceName);
var instance;

if (hasCaller) {
path.unshift(stringifyIdForError(caller, displayNameSuffix));
}
path.unshift(stringifyIdForError(serviceName, displayNameSuffix));

try {
if (hadInstance) {
instance = cache.get(serviceName);

if (instance === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
}
throw err;
} finally {
path.shift();

return instance;
} else {
cache.put(serviceName, INSTANTIATING);

instance = factory(serviceName);
cache.put(serviceName, instance);

return instance;
}
} finally {
if (!hadInstance && (cache.get(serviceName) === INSTANTIATING)) {
cache.remove(serviceName);
}

path.shift();
if (hasCaller) path.shift();
}
}

@@ -838,12 +863,8 @@ function createInjector(modulesToLoad, strictDi) {

for (var i = 0, length = $inject.length; i < length; i++) {
var key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
getService(key, serviceName));
var localsHasKey = locals && isString(key) && locals.hasOwnProperty(key);
args.push(localsHasKey ? locals[key] : getService(key, serviceName));
}
return args;
}
@@ -901,7 +922,7 @@ function createInjector(modulesToLoad, strictDi) {
get: getService,
annotate: createInjector.$$annotate,
has: function(name) {
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
return cache.has(name) || providerCache.has(getProviderId(name));
}
};
}
5 changes: 4 additions & 1 deletion src/ng/directive/select.js
Original file line number Diff line number Diff line change
@@ -563,8 +563,11 @@ var selectDirective = function() {
// Write value now needs to set the selected property of each matching option
selectCtrl.writeValue = function writeMultipleValue(value) {
var items = new HashMap(value);
var selectValueMap = selectCtrl.selectValueMap;

forEach(element.find('option'), function(option) {
option.selected = isDefined(items.get(option.value)) || isDefined(items.get(selectCtrl.selectValueMap[option.value]));
var value = option.value;
option.selected = items.has(value) || items.has(selectValueMap[value]);
});
};

12 changes: 12 additions & 0 deletions test/ApiSpecs.js
Original file line number Diff line number Diff line change
@@ -8,11 +8,16 @@ describe('api', function() {
var key = {};
var value1 = {};
var value2 = {};

map.put(key, value1);
map.put(key, value2);

expect(map.has(key)).toBe(true);
expect(map.has({})).toBe(false);
expect(map.get(key)).toBe(value2);
expect(map.get({})).toBeUndefined();
expect(map.remove(key)).toBe(value2);
expect(map.has(key)).toBe(false);
expect(map.get(key)).toBeUndefined();
});

@@ -23,6 +28,13 @@ describe('api', function() {
expect(map.get('c')).toBeUndefined();
});

it('should init from an object', function() {
var map = new HashMap({a: 'foo', b: 'bar'});
expect(map.get('a')).toBe('foo');
expect(map.get('b')).toBe('bar');
expect(map.get('c')).toBeUndefined();
});

it('should maintain hashKey for object keys', function() {
var map = new HashMap();
var key = {};
245 changes: 219 additions & 26 deletions test/auto/injectorSpec.js
Original file line number Diff line number Diff line change
@@ -46,13 +46,13 @@ describe('injector', function() {
it('should resolve dependency graph and instantiate all services just once', function() {
var log = [];

// s1
// / | \
// / s2 \
// / / | \ \
// /s3 < s4 > s5
// //
// s6
// ____ s1 _
// / | \
// / __ s2 __ \
// / / | \ \
// | s3 <- s4 --> s5
// | /
// s6


providers('s1', function() { log.push('s1'); return {}; }, {$inject: ['s2', 's5', 's6']});
@@ -80,14 +80,14 @@ describe('injector', function() {
it('should provide useful message if no provider', function() {
expect(function() {
injector.get('idontexist');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist\n');
});


it('should provide the caller name if given', function() {
expect(function() {
injector.get('idontexist', 'callerName');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist <- callerName");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- callerName\n');
});


@@ -96,18 +96,18 @@ describe('injector', function() {
var $controller = injector.get('$controller');
expect(function() {
$controller('myCtrl', {$scope: {}});
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist <- myCtrl");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- myCtrl\n');
});


it('should not corrupt the cache when an object fails to get instantiated', function() {
expect(function() {
injector.get('idontexist');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist\n');

expect(function() {
injector.get('idontexist');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist\n');
});


@@ -116,7 +116,7 @@ describe('injector', function() {
providers('b', function(a) {return 2;});
expect(function() {
injector.get('b');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: idontexistProvider <- idontexist <- a <- b");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- a <- b\n');
});


@@ -287,7 +287,7 @@ describe('injector', function() {
});

// Support: Chrome 50-51 only
// TODO (gkalpak): Remove when Chrome v52 is released.
// TODO(gkalpak): Remove when Chrome v52 is released.
// it('should be able to inject fat-arrow function', function() {
// inject(($injector) => {
// expect($injector).toBeDefined();
@@ -327,7 +327,7 @@ describe('injector', function() {
}

// Support: Chrome 50-51 only
// TODO (gkalpak): Remove when Chrome v52 is released.
// TODO(gkalpak): Remove when Chrome v52 is released.
// it('should be able to invoke classes', function() {
// class Test {
// constructor($injector) {
@@ -800,7 +800,7 @@ describe('injector', function() {
expect(function() {
createInjector(['TestModule']);
}).toThrowMinErr(
'$injector', 'modulerr', /Failed to instantiate module TestModule due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
'$injector', 'modulerr', /Failed to instantiate module TestModule due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy\n/
);
});

@@ -810,7 +810,7 @@ describe('injector', function() {
expect(function() {
createInjector([myModule]);
}).toThrowMinErr(
'$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
'$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy\n/
);
});

@@ -820,7 +820,7 @@ describe('injector', function() {
expect(function() {
createInjector([['xyzzy', myModule]]);
}).toThrowMinErr(
'$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
'$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy\n/
);
});

@@ -831,7 +831,7 @@ describe('injector', function() {
$provide.factory('service', function(service) {});
return function(service) {};
}]);
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: service <- service');
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: service <- service\n');
});


@@ -842,7 +842,7 @@ describe('injector', function() {
$provide.factory('b', function(a) {});
return function(a) {};
}]);
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: a <- b <- a');
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: a <- b <- a\n');
});

});
@@ -884,7 +884,6 @@ describe('injector', function() {
var $injector = createInjectorWithValue('instance', instance);
expect($injector.invoke(function(instance) { return instance; })).toBe(instance);
});

});


@@ -1039,20 +1038,20 @@ describe('injector', function() {

describe('protection modes', function() {
it('should prevent provider lookup in app', function() {
var $injector = createInjector([function($provide) {
var $injector = createInjector([function($provide) {
$provide.value('name', 'angular');
}]);
expect(function() {
$injector.get('nameProvider');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: nameProviderProvider <- nameProvider");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nameProviderProvider <- nameProvider\n');
});


it('should prevent provider configuration in app', function() {
var $injector = createInjector([]);
var $injector = createInjector([]);
expect(function() {
$injector.get('$provide').value('a', 'b');
}).toThrowMinErr("$injector", "unpr", "Unknown provider: $provideProvider <- $provide");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: $provideProvider <- $provide\n');
});


@@ -1062,7 +1061,7 @@ describe('injector', function() {
createInjector([function($provide) {
$provide.value('name', 'angular');
}, instanceLookupInModule]);
}).toThrowError(/\[\$injector:unpr] Unknown provider: name/);
}).toThrowError(/\[\$injector:unpr] Unknown provider: name\n/);
});
});
});
@@ -1072,11 +1071,18 @@ describe('strict-di injector', function() {

describe('with ngMock', function() {
it('should not throw when calling mock.module() with "magic" annotations', function() {
var executed = false;

expect(function() {
module(function($provide, $httpProvider, $compileProvider) {
// Don't throw!
executed = true;
});
}).not.toThrow();

inject();

expect(executed).toBe(true);
});


@@ -1162,3 +1168,190 @@ describe('strict-di injector', function() {
expect($injector.strictDi).toBe(true);
}));
});

describe('injector with non-string IDs', function() {
it('should support non-string service identifiers', function() {
var ids = [
null,
true,
1,
{},
[],
noop,
/./
];

module(function($provide) {
ids.forEach(function(id, idx) { $provide.value(id, idx); });
$provide.factory('allDeps', ids.concat(function() { return sliceArgs(arguments); }));
});

inject(function(allDeps) {
expect(allDeps.length).toBe(ids.length);
expect(allDeps.every(function(dep, idx) { return dep === idx; })).toBe(true);
});
});


it('should support configuring services with non-string identifiers', function() {
/* eslint-disable no-new-wrappers */
var id1 = new String('same string, no problem');
var id2 = new String('same string, no problem');
/* eslint-enable */

angular.
module('test', []).
factory(id2, [id1, identity]).
provider(id1, function Id1Provider() {
var value = 'foo';
this.setValue = function setValue(newValue) { value = newValue; };
this.$get = function $get() { return value; };
}).
config([id1, function config(id1Provider) {
id1Provider.setValue('bar');
}]);

module('test');
inject([id2, function(dep2) {
expect(dep2).toBe('bar');
}]);
});


it('should support decorating services with non-string identifiers', function() {
/* eslint-disable no-new-wrappers */
var id1 = new String('same string, no problem');
var id2 = new String('same string, no problem');
/* eslint-enable */

angular.
module('test', []).
factory(id2, [id1, identity]).
value(id1, 'foo').
decorator(id1, function decorator($delegate) {
expect($delegate).toBe('foo');
return 'bar';
});

module('test');
inject([id2, function(dep2) {
expect(dep2).toBe('bar');
}]);
});


it('should still allow passing multiple providers as object', function() {
var obj = {
foo: 'foo',
bar: 'bar'
};

module(function($provide) {
$provide.value(obj);
$provide.value(obj, 'foo&bar');
});

inject(['foo', 'bar', obj, function(foo, bar, fooBar) {
expect(foo).toBe('foo');
expect(bar).toBe('bar');
expect(fooBar).toBe('foo&bar');
}]);
});


describe('should stringify non-string identifiers for error messages', function() {
var foo, bar, baz;

beforeEach(function() {
foo = {toString: valueFn('fooThingy')};
bar = {toString: valueFn('barThingy')};
baz = {toString: valueFn('bazThingy')};
});


it('(Unknown provider)', function() {
var executed = false;

module(function($provide) {
expect(function() {
$provide.provider('foo', ['barProvider', noop]);
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: barProvider\n');

expect(function() {
$provide.provider('foo', [{}, noop]);
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: [object Object] (provider)\n');

expect(function() {
$provide.provider('foo', [bar, noop]);
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: barThingy (provider)\n');

executed = true;
});

inject();

expect(executed).toBe(true);
});


it('(Unknown service)', function() {
var executed = false;

module(function($provide) {
$provide.provider(foo, valueFn({$get: ['bar', noop]}));
$provide.provider('bar', valueFn({$get: [baz, noop]}));

$provide.provider('foo', valueFn({$get: [bar, noop]}));
$provide.provider(bar, valueFn({$get: ['baz', noop]}));
});

inject(function($injector) {
var specs = [
['bar', 'bazThingy (provider) <- bazThingy <- bar'],
[foo, 'bazThingy (provider) <- bazThingy <- bar <- fooThingy'],
[bar, 'bazProvider <- baz <- barThingy'],
['foo', 'bazProvider <- baz <- barThingy <- foo']
];

forEach(specs, function(spec) {
var serviceId = spec[0];
var errorPath = spec[1];

expect(function() {
$injector.get(serviceId);
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: ' + errorPath + '\n');
});

executed = true;
});

expect(executed).toBe(true);
});


it('(Circular dependency)', function() {
var executed = false;

module(function($provide) {
$provide.provider(foo, valueFn({$get: ['bar', noop]}));
$provide.provider('bar', valueFn({$get: [baz, noop]}));
$provide.provider(baz, valueFn({$get: ['foo', noop]}));
$provide.provider('foo', valueFn({$get: [bar, noop]}));
$provide.provider(bar, valueFn({$get: ['baz', noop]}));
$provide.provider('baz', valueFn({$get: [foo, noop]}));
});

inject(function($injector) {
var errorPath = 'fooThingy <- baz <- barThingy <- foo <- bazThingy <- bar <- fooThingy';

expect(function() {
$injector.get(foo);
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: ' + errorPath + '\n');

executed = true;
});

expect(executed).toBe(true);
});
});
});
2 changes: 1 addition & 1 deletion test/ng/parseSpec.js
Original file line number Diff line number Diff line change
@@ -1994,7 +1994,7 @@ describe('parser', function() {

expect(function() {
scope.$eval("1|nonexistent");
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter');
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter\n');

scope.offset = 3;
expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");