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

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 #6719
Closes #6717
Closes #4504
Closes #6069
Closes #3611
  • Loading branch information
caitp committed Apr 11, 2014
1 parent ab92da4 commit f5a04f5
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

4 comments on commit f5a04f5

@IgorMinar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caitp This commit shouldn't have been merged into 1.2.x branch. can you please revert it?

@caitp
Copy link
Contributor Author

@caitp caitp commented on f5a04f5 Apr 15, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was a small enough feature that parity wouldn't hurt, but okay

@IlanFrumer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature was probably removed from the 1.2.x branch and is not part of 1.2.17, yet it is still listed on the CHANGELOG.md as a new feature.

@caitp
Copy link
Contributor Author

@caitp caitp commented on f5a04f5 Jun 8, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IlanFrumer just removed it from the changelog... I think the changelog generator script is broken if its ignoring reverted commits

Please sign in to comment.