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

feat(injector): "strict-DI" mode which disables "automatic" function annotation / inferred dependencies #6719

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
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
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 @@ -1271,9 +1370,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 @@ -1287,7 +1397,7 @@ function bootstrap(element, modules) {
$provide.value('$rootElement', element);
}]);
modules.unshift('ng');
var injector = createInjector(modules);
var injector = createInjector(modules, config.strictDi);
Copy link
Member

Choose a reason for hiding this comment

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

Would it be better in the long run to pass the whole config object through to createInjector?

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