From 4361efb03b79e71bf0cea92b94ff377ed718bad4 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 16 May 2012 22:21:08 -0700 Subject: [PATCH] feat($injector): provide API for retrieving function annotations --- src/auto/injector.js | 142 ++++++++++++++++++++++++++++++-------- test/auto/injectorSpec.js | 27 +++++--- 2 files changed, 131 insertions(+), 38 deletions(-) diff --git a/src/auto/injector.js b/src/auto/injector.js index 250f05c5b826..49247ff4ee2a 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -42,19 +42,32 @@ var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(.+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; -function inferInjectionArgs(fn) { - assertArgFn(fn); - if (!fn.$inject) { - var args = fn.$inject = []; - var fnText = fn.toString().replace(STRIP_COMMENTS, ''); - var argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ - args.push(name); +function annotate(fn) { + var $inject, + fnText, + argDecl, + last; + + if (typeof fn == 'function') { + if (!($inject = fn.$inject)) { + $inject = []; + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ + arg.replace(FN_ARG, function(all, underscore, name){ + $inject.push(name); + }); }); - }); + fn.$inject = $inject; + } + } else if (isArray(fn)) { + last = fn.length - 1; + assertArgFn(fn[last], 'fn') + $inject = fn.slice(0, last); + } else { + assertArgFn(fn, 'fn', true); } - return fn.$inject; + return $inject; } /////////////////////////////////////// @@ -152,6 +165,87 @@ function inferInjectionArgs(fn) { * @returns {Object} new instance of `Type`. */ +/** + * @ngdoc method + * @name angular.module.AUTO.$injector#annotate + * @methodOf angular.module.AUTO.$injector + * + * @description + * Returns an array of service names which the function is requesting for injection. This API is used by the injector + * to determine which services need to be injected into the function when the function is invoked. There are three + * ways in which the function can be annotated with the needed dependencies. + * + * # Argument names + * + * The simplest form is to extract the dependencies from the arguments of the function. This is done by converting + * the function into a string using `toString()` method and extracting the argument names. + *
+ *   // Given
+ *   function MyController($scope, $route) {
+ *     // ...
+ *   }
+ *
+ *   // Then
+ *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
+ * 
+ * + * This method does not work with code minfication / obfuscation. For this reason the following annotation strategies + * are supported. + * + * # The `$injector` property + * + * If a function has an `$inject` property and its value is an array of strings, then the strings represent names of + * services to be injected into the function. + *
+ *   // Given
+ *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
+ *     // ...
+ *   }
+ *   // Define function dependencies
+ *   MyController.$inject = ['$scope', '$route'];
+ *
+ *   // Then
+ *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
+ * 
+ * + * # The array notation + * + * It is often desirable to inline Injected functions and that's when setting the `$inject` property is very + * inconvenient. In these situations using the array notation to specify the dependencies in a way that survives + * minification is a better choice: + * + *
+ *   // We wish to write this (not minification / obfuscation safe)
+ *   injector.invoke(function($compile, $rootScope) {
+ *     // ...
+ *   });
+ *
+ *   // We are forced to write break inlining
+ *   var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) {
+ *     // ...
+ *   };
+ *   tmpFn.$inject = ['$compile', '$rootScope'];
+ *   injector.invoke(tempFn);
+ *
+ *   // To better support inline function the inline annotation is supported
+ *   injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) {
+ *     // ...
+ *   }]);
+ *
+ *   // Therefore
+ *   expect(injector.annotate(
+ *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
+ *    ).toEqual(['$compile', '$rootScope']);
+ * 
+ * + * @param {function|Array.} fn Function for which dependent service names need to be retrieved as described + * above. + * + * @returns {Array.} The names of the services which the function requires. + */ + + + /** * @ngdoc object @@ -454,23 +548,11 @@ function createInjector(modulesToLoad) { function invoke(fn, self, locals){ var args = [], - $inject, - length, + $inject = annotate(fn), + length, i, key; - if (typeof fn == 'function') { - $inject = inferInjectionArgs(fn); - length = $inject.length; - } else { - if (isArray(fn)) { - $inject = fn; - length = $inject.length - 1; - fn = $inject[length]; - } - assertArgFn(fn, 'fn'); - } - - for(var i = 0; i < length; i++) { + for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; args.push( locals && locals.hasOwnProperty(key) @@ -478,6 +560,11 @@ function createInjector(modulesToLoad) { : getService(key, path) ); } + if (!fn.$inject) { + // this means that we must be an array. + fn = fn[length]; + } + // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke switch (self ? -1 : args.length) { @@ -510,7 +597,8 @@ function createInjector(modulesToLoad) { return { invoke: invoke, instantiate: instantiate, - get: getService + get: getService, + annotate: annotate }; } } diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 83bd3d1f81e2..33fecac616de 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -123,11 +123,11 @@ describe('injector', function() { it('should return $inject', function() { function fn() {} fn.$inject = ['a']; - expect(inferInjectionArgs(fn)).toBe(fn.$inject); - expect(inferInjectionArgs(function() {})).toEqual([]); - expect(inferInjectionArgs(function () {})).toEqual([]); - expect(inferInjectionArgs(function () {})).toEqual([]); - expect(inferInjectionArgs(function /* */ () {})).toEqual([]); + expect(annotate(fn)).toBe(fn.$inject); + expect(annotate(function() {})).toEqual([]); + expect(annotate(function () {})).toEqual([]); + expect(annotate(function () {})).toEqual([]); + expect(annotate(function /* */ () {})).toEqual([]); }); @@ -142,43 +142,48 @@ describe('injector', function() { */ _c, /* {some type} */ d) { extraParans();} - expect(inferInjectionArgs($f_n0)).toEqual(['$a', 'b_', '_c', 'd']); + expect(annotate($f_n0)).toEqual(['$a', 'b_', '_c', 'd']); expect($f_n0.$inject).toEqual(['$a', 'b_', '_c', 'd']); }); it('should strip leading and trailing underscores from arg name during inference', function() { function beforeEachFn(_foo_) { /* foo = _foo_ */ }; - expect(inferInjectionArgs(beforeEachFn)).toEqual(['foo']); + expect(annotate(beforeEachFn)).toEqual(['foo']); }); it('should handle no arg functions', function() { function $f_n0() {} - expect(inferInjectionArgs($f_n0)).toEqual([]); + expect(annotate($f_n0)).toEqual([]); expect($f_n0.$inject).toEqual([]); }); it('should handle no arg functions with spaces in the arguments list', function() { function fn( ) {} - expect(inferInjectionArgs(fn)).toEqual([]); + expect(annotate(fn)).toEqual([]); expect(fn.$inject).toEqual([]); }); it('should handle args with both $ and _', function() { function $f_n0($a_) {} - expect(inferInjectionArgs($f_n0)).toEqual(['$a_']); + expect(annotate($f_n0)).toEqual(['$a_']); expect($f_n0.$inject).toEqual(['$a_']); }); it('should throw on non function arg', function() { expect(function() { - inferInjectionArgs({}); + annotate({}); }).toThrow(); }); + + + it('should publish annotate API', function() { + expect(injector.annotate).toBe(annotate); + }); });