Skip to content

Commit

Permalink
feat(injector): "strict-DI" mode which disables "automatic" function …
Browse files Browse the repository at this point in the history
…annotation

This modifies the injector to prevent automatic annotation from occurring for a given injector.

This behaviour can be enabled when bootstrapping the application by using the attribute
"ng-strict-di" on the root element (the element containing "ng-app"), or alternatively by passing
an object with the property "strictDi" set to "true" in angular.bootstrap, when bootstrapping
manually.

JS example:

    angular.module("name", ["dependencies", "otherdeps"])
      .provider("$willBreak", function() {
        this.$get = function($rootScope) {
        };
      })
      .run(["$willBreak", function($willBreak) {
        // This block will never run because the noMagic flag was set to true,
        // and the $willBreak '$get' function does not have an explicit
        // annotation.
      }]);

    angular.bootstrap(document, ["name"], {
      strictDi: true
    });

HTML:

    <html ng-app="name" ng-strict-di>
      <!-- ... -->
    </html>

This will only affect functions with an arity greater than 0, and without an $inject property.

Closes angular#6719
Closes angular#6717
Closes angular#4504
Closes angular#6069
Closes angular#3611
  • Loading branch information
caitp committed Apr 11, 2014
1 parent ab92da4 commit f66dfde
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 33 deletions.
54 changes: 54 additions & 0 deletions docs/content/error/$injector/strictdi.ngdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@ngdoc error
@name $injector:strictdi
@fullName Explicit annotation required
@description

This error occurs when attempting to invoke a function or provider which
has not been explicitly annotated, while the application is running with
strict-di mode enabled.

For example:

```
angular.module("myApp", [])
// BadController cannot be invoked, because
// the dependencies to be injected are not
// explicitly listed.
.controller("BadController", function($scope, $http, $filter) {
// ...
});
```

To fix the error, explicitly annotate the function using either the inline
bracket notation, or with the $inject property:

```
function GoodController1($scope, $http, $filter) {
// ...
}
GoodController1.$inject = ["$scope", "$http", "$filter"];

angular.module("myApp", [])
// GoodController1 can be invoked because it
// had an $inject property, which is an array
// containing the dependency names to be
// injected.
.controller("GoodController1", GoodController1)

// GoodController2 can also be invoked, because
// the dependencies to inject are listed, in
// order, in the array, with the function to be
// invoked trailing on the end.
.controller("GoodController2", [
"$scope",
"$http",
"$filter",
function($scope, $http, $filter) {
// ...
}
]);

```

For more information about strict-di mode, see {@link ng.directive:ngApp ngApp}
and {@link api/angular.bootstrap angular.bootstrap}.
116 changes: 113 additions & 3 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,19 @@ function encodeUriQuery(val, pctEncodeSpaces) {
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}

var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];

function getNgAttribute(element, ngAttr) {
var attr, i, ii = ngAttrPrefixes.length, j, jj;
element = jqLite(element);
for (i=0; i<ii; ++i) {
attr = ngAttrPrefixes[i] + ngAttr;
if (isString(attr = element.attr(attr))) {
return attr;
}
}
return null;
}

/**
* @ngdoc directive
Expand All @@ -1147,6 +1160,11 @@ function encodeUriQuery(val, pctEncodeSpaces) {
* @element ANY
* @param {angular.Module} ngApp an optional application
* {@link angular.module module} name to load.
* @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be
* created in "strict-di" mode. This means that the application will fail to invoke functions which
* do not use explicit function annotation (and are thus unsuitable for minification), as described
* in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in
* tracking down the root of these bugs.
*
* @description
*
Expand Down Expand Up @@ -1184,12 +1202,92 @@ function encodeUriQuery(val, pctEncodeSpaces) {
</file>
</example>
*
* Using `ngStrictDi`, you would see something like this:
*
<example ng-app-included="true">
<file name="index.html">
<div ng-app="ngAppStrictDemo" ng-strict-di>
<div ng-controller="GoodController1">
I can add: {{a}} + {{b}} = {{ a+b }}
<p>This renders because the controller does not fail to
instantiate, by using explicit annotation style (see
script.js for details)
</p>
</div>
<div ng-controller="GoodController2">
Name: <input ng-model="name"><br />
Hello, {{name}}!
<p>This renders because the controller does not fail to
instantiate, by using explicit annotation style
(see script.js for details)
</p>
</div>
<div ng-controller="BadController">
I can add: {{a}} + {{b}} = {{ a+b }}
<p>The controller could not be instantiated, due to relying
on automatic function annotations (which are disabled in
strict mode). As such, the content of this section is not
interpolated, and there should be an error in your web console.
</p>
</div>
</div>
</file>
<file name="script.js">
angular.module('ngAppStrictDemo', [])
// BadController will fail to instantiate, due to relying on automatic function annotation,
// rather than an explicit annotation
.controller('BadController', function($scope) {
$scope.a = 1;
$scope.b = 2;
})
// Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated,
// due to using explicit annotations using the array style and $inject property, respectively.
.controller('GoodController1', ['$scope', function($scope) {
$scope.a = 1;
$scope.b = 2;
}])
.controller('GoodController2', GoodController2);
function GoodController2($scope) {
$scope.name = "World";
}
GoodController2.$inject = ['$scope'];
</file>
<file name="style.css">
div[ng-controller] {
margin-bottom: 1em;
-webkit-border-radius: 4px;
border-radius: 4px;
border: 1px solid;
padding: .5em;
}
div[ng-controller^=Good] {
border-color: #d6e9c6;
background-color: #dff0d8;
color: #3c763d;
}
div[ng-controller^=Bad] {
border-color: #ebccd1;
background-color: #f2dede;
color: #a94442;
margin-bottom: 0;
}
</file>
</example>
*/
function angularInit(element, bootstrap) {
var elements = [element],
appElement,
module,
config = {},
names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
options = {
'boolean': ['strict-di']
},
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;

function append(element) {
Expand Down Expand Up @@ -1225,7 +1323,8 @@ function angularInit(element, bootstrap) {
}
});
if (appElement) {
bootstrap(appElement, module ? [module] : []);
config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
bootstrap(appElement, module ? [module] : [], config);
}
}

Expand Down Expand Up @@ -1281,9 +1380,20 @@ function angularInit(element, bootstrap) {
* Each item in the array should be the name of a predefined module or a (DI annotated)
* function that will be invoked by the injector as a run block.
* See: {@link angular.module modules}
* @param {Object=} config an object for defining configuration options for the application. The
* following keys are supported:
*
* - `strictDi`: disable automatic function annotation for the application. This is meant to
* assist in finding bugs which break minified code.
*
* @returns {auto.$injector} Returns the newly created injector for this app.
*/
function bootstrap(element, modules) {
function bootstrap(element, modules, config) {
if (!isObject(config)) config = {};
var defaultConfig = {
strictDi: false
};
config = extend(defaultConfig, config);
var doBootstrap = function() {
element = jqLite(element);

Expand All @@ -1297,7 +1407,7 @@ function bootstrap(element, modules) {
$provide.value('$rootElement', element);
}]);
modules.unshift('ng');
var injector = createInjector(modules);
var injector = createInjector(modules, config.strictDi);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
function(scope, element, compile, injector, animate) {
scope.$apply(function() {
Expand Down
44 changes: 35 additions & 9 deletions src/auto/injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@ var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
function annotate(fn) {

function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
args = fnText.match(FN_ARGS);
if (args) {
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
}
return 'fn';
}

function annotate(fn, strictDi, name) {
var $inject,
fnText,
argDecl,
Expand All @@ -76,6 +88,13 @@ function annotate(fn) {
if (!($inject = fn.$inject)) {
$inject = [];
if (fn.length) {
if (strictDi) {
if (!isString(name) || !name) {
name = fn.name || anonFn(fn);
}
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
Expand Down Expand Up @@ -587,7 +606,8 @@ function annotate(fn) {
*/


function createInjector(modulesToLoad) {
function createInjector(modulesToLoad, strictDi) {
strictDi = (strictDi === true);
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
Expand All @@ -605,13 +625,13 @@ function createInjector(modulesToLoad) {
providerInjector = (providerCache.$injector =
createInternalInjector(providerCache, function() {
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),
}, strictDi)),
instanceCache = {},
instanceInjector = (instanceCache.$injector =
createInternalInjector(instanceCache, function(servicename) {
var provider = providerInjector.get(servicename + providerSuffix);
return instanceInjector.invoke(provider.$get, provider);
}));
return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
}, strictDi));


forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });
Expand Down Expand Up @@ -743,9 +763,14 @@ function createInjector(modulesToLoad) {
}
}

function invoke(fn, self, locals){
function invoke(fn, self, locals, serviceName){
if (typeof locals === 'string') {
serviceName = locals;
locals = null;
}

var args = [],
$inject = annotate(fn),
$inject = annotate(fn, strictDi, serviceName),
length, i,
key;

Expand All @@ -771,15 +796,15 @@ function createInjector(modulesToLoad) {
return fn.apply(self, args);
}

function instantiate(Type, locals) {
function instantiate(Type, locals, serviceName) {
var Constructor = function() {},
instance, returnedValue;

// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype;
instance = new Constructor();
returnedValue = invoke(Type, instance, locals);
returnedValue = invoke(Type, instance, locals, serviceName);

return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
}
Expand All @@ -796,3 +821,4 @@ function createInjector(modulesToLoad) {
}
}

createInjector.$$annotate = annotate;
2 changes: 1 addition & 1 deletion src/ng/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function $ControllerProvider() {
assertArgFn(expression, constructor, true);
}

instance = $injector.instantiate(expression, locals);
instance = $injector.instantiate(expression, locals, constructor);

if (identifier) {
if (!(locals && typeof locals.$scope == 'object')) {
Expand Down
Loading

0 comments on commit f66dfde

Please sign in to comment.