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

Commit 4b1695e

Browse files
committed
feat(injector): "strict-DI" mode which disables "automatic" function 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
1 parent 24a045c commit 4b1695e

File tree

8 files changed

+337
-33
lines changed

8 files changed

+337
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@ngdoc error
2+
@name $injector:strictdi
3+
@fullName Explicit annotation required
4+
@description
5+
6+
This error occurs when attempting to invoke a function or provider which
7+
has not been explicitly annotated, while the application is running with
8+
strict-di mode enabled.
9+
10+
For example:
11+
12+
```
13+
angular.module("myApp", [])
14+
// BadController cannot be invoked, because
15+
// the dependencies to be injected are not
16+
// explicitly listed.
17+
.controller("BadController", function($scope, $http, $filter) {
18+
// ...
19+
});
20+
```
21+
22+
To fix the error, explicitly annotate the function using either the inline
23+
bracket notation, or with the $inject property:
24+
25+
```
26+
function GoodController1($scope, $http, $filter) {
27+
// ...
28+
}
29+
GoodController1.$inject = ["$scope", "$http", "$filter"];
30+
31+
angular.module("myApp", [])
32+
// GoodController1 can be invoked because it
33+
// had an $inject property, which is an array
34+
// containing the dependency names to be
35+
// injected.
36+
.controller("GoodController1", GoodController1)
37+
38+
// GoodController2 can also be invoked, because
39+
// the dependencies to inject are listed, in
40+
// order, in the array, with the function to be
41+
// invoked trailing on the end.
42+
.controller("GoodController2", [
43+
"$scope",
44+
"$http",
45+
"$filter",
46+
function($scope, $http, $filter) {
47+
// ...
48+
}
49+
]);
50+
51+
```
52+
53+
For more information about strict-di mode, see {@link ng.directive:ngApp ngApp}
54+
and {@link api/angular.bootstrap angular.bootstrap}.

src/Angular.js

+113-3
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,19 @@ function encodeUriQuery(val, pctEncodeSpaces) {
11381138
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
11391139
}
11401140

1141+
var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];
1142+
1143+
function getNgAttribute(element, ngAttr) {
1144+
var attr, i, ii = ngAttrPrefixes.length, j, jj;
1145+
element = jqLite(element);
1146+
for (i=0; i<ii; ++i) {
1147+
attr = ngAttrPrefixes[i] + ngAttr;
1148+
if (isString(attr = element.attr(attr))) {
1149+
return attr;
1150+
}
1151+
}
1152+
return null;
1153+
}
11411154

11421155
/**
11431156
* @ngdoc directive
@@ -1147,6 +1160,11 @@ function encodeUriQuery(val, pctEncodeSpaces) {
11471160
* @element ANY
11481161
* @param {angular.Module} ngApp an optional application
11491162
* {@link angular.module module} name to load.
1163+
* @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be
1164+
* created in "strict-di" mode. This means that the application will fail to invoke functions which
1165+
* do not use explicit function annotation (and are thus unsuitable for minification), as described
1166+
* in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in
1167+
* tracking down the root of these bugs.
11501168
*
11511169
* @description
11521170
*
@@ -1184,12 +1202,92 @@ function encodeUriQuery(val, pctEncodeSpaces) {
11841202
</file>
11851203
</example>
11861204
*
1205+
* Using `ngStrictDi`, you would see something like this:
1206+
*
1207+
<example ng-app-included="true">
1208+
<file name="index.html">
1209+
<div ng-app="ngAppStrictDemo" ng-strict-di>
1210+
<div ng-controller="GoodController1">
1211+
I can add: {{a}} + {{b}} = {{ a+b }}
1212+
1213+
<p>This renders because the controller does not fail to
1214+
instantiate, by using explicit annotation style (see
1215+
script.js for details)
1216+
</p>
1217+
</div>
1218+
1219+
<div ng-controller="GoodController2">
1220+
Name: <input ng-model="name"><br />
1221+
Hello, {{name}}!
1222+
1223+
<p>This renders because the controller does not fail to
1224+
instantiate, by using explicit annotation style
1225+
(see script.js for details)
1226+
</p>
1227+
</div>
1228+
1229+
<div ng-controller="BadController">
1230+
I can add: {{a}} + {{b}} = {{ a+b }}
1231+
1232+
<p>The controller could not be instantiated, due to relying
1233+
on automatic function annotations (which are disabled in
1234+
strict mode). As such, the content of this section is not
1235+
interpolated, and there should be an error in your web console.
1236+
</p>
1237+
</div>
1238+
</div>
1239+
</file>
1240+
<file name="script.js">
1241+
angular.module('ngAppStrictDemo', [])
1242+
// BadController will fail to instantiate, due to relying on automatic function annotation,
1243+
// rather than an explicit annotation
1244+
.controller('BadController', function($scope) {
1245+
$scope.a = 1;
1246+
$scope.b = 2;
1247+
})
1248+
// Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated,
1249+
// due to using explicit annotations using the array style and $inject property, respectively.
1250+
.controller('GoodController1', ['$scope', function($scope) {
1251+
$scope.a = 1;
1252+
$scope.b = 2;
1253+
}])
1254+
.controller('GoodController2', GoodController2);
1255+
function GoodController2($scope) {
1256+
$scope.name = "World";
1257+
}
1258+
GoodController2.$inject = ['$scope'];
1259+
</file>
1260+
<file name="style.css">
1261+
div[ng-controller] {
1262+
margin-bottom: 1em;
1263+
-webkit-border-radius: 4px;
1264+
border-radius: 4px;
1265+
border: 1px solid;
1266+
padding: .5em;
1267+
}
1268+
div[ng-controller^=Good] {
1269+
border-color: #d6e9c6;
1270+
background-color: #dff0d8;
1271+
color: #3c763d;
1272+
}
1273+
div[ng-controller^=Bad] {
1274+
border-color: #ebccd1;
1275+
background-color: #f2dede;
1276+
color: #a94442;
1277+
margin-bottom: 0;
1278+
}
1279+
</file>
1280+
</example>
11871281
*/
11881282
function angularInit(element, bootstrap) {
11891283
var elements = [element],
11901284
appElement,
11911285
module,
1286+
config = {},
11921287
names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
1288+
options = {
1289+
'boolean': ['strict-di']
1290+
},
11931291
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
11941292

11951293
function append(element) {
@@ -1225,7 +1323,8 @@ function angularInit(element, bootstrap) {
12251323
}
12261324
});
12271325
if (appElement) {
1228-
bootstrap(appElement, module ? [module] : []);
1326+
config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
1327+
bootstrap(appElement, module ? [module] : [], config);
12291328
}
12301329
}
12311330

@@ -1271,9 +1370,20 @@ function angularInit(element, bootstrap) {
12711370
* Each item in the array should be the name of a predefined module or a (DI annotated)
12721371
* function that will be invoked by the injector as a run block.
12731372
* See: {@link angular.module modules}
1373+
* @param {Object=} config an object for defining configuration options for the application. The
1374+
* following keys are supported:
1375+
*
1376+
* - `strictDi`: disable automatic function annotation for the application. This is meant to
1377+
* assist in finding bugs which break minified code.
1378+
*
12741379
* @returns {auto.$injector} Returns the newly created injector for this app.
12751380
*/
1276-
function bootstrap(element, modules) {
1381+
function bootstrap(element, modules, config) {
1382+
if (!isObject(config)) config = {};
1383+
var defaultConfig = {
1384+
strictDi: false
1385+
};
1386+
config = extend(defaultConfig, config);
12771387
var doBootstrap = function() {
12781388
element = jqLite(element);
12791389

@@ -1287,7 +1397,7 @@ function bootstrap(element, modules) {
12871397
$provide.value('$rootElement', element);
12881398
}]);
12891399
modules.unshift('ng');
1290-
var injector = createInjector(modules);
1400+
var injector = createInjector(modules, config.strictDi);
12911401
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
12921402
function(scope, element, compile, injector, animate) {
12931403
scope.$apply(function() {

src/auto/injector.js

+35-9
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,19 @@ var FN_ARG_SPLIT = /,/;
6666
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
6767
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
6868
var $injectorMinErr = minErr('$injector');
69-
function annotate(fn) {
69+
70+
function anonFn(fn) {
71+
// For anonymous functions, showing at the very least the function signature can help in
72+
// debugging.
73+
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
74+
args = fnText.match(FN_ARGS);
75+
if (args) {
76+
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
77+
}
78+
return 'fn';
79+
}
80+
81+
function annotate(fn, strictDi, name) {
7082
var $inject,
7183
fnText,
7284
argDecl,
@@ -76,6 +88,13 @@ function annotate(fn) {
7688
if (!($inject = fn.$inject)) {
7789
$inject = [];
7890
if (fn.length) {
91+
if (strictDi) {
92+
if (!isString(name) || !name) {
93+
name = fn.name || anonFn(fn);
94+
}
95+
throw $injectorMinErr('strictdi',
96+
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
97+
}
7998
fnText = fn.toString().replace(STRIP_COMMENTS, '');
8099
argDecl = fnText.match(FN_ARGS);
81100
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
@@ -587,7 +606,8 @@ function annotate(fn) {
587606
*/
588607

589608

590-
function createInjector(modulesToLoad) {
609+
function createInjector(modulesToLoad, strictDi) {
610+
strictDi = (strictDi === true);
591611
var INSTANTIATING = {},
592612
providerSuffix = 'Provider',
593613
path = [],
@@ -605,13 +625,13 @@ function createInjector(modulesToLoad) {
605625
providerInjector = (providerCache.$injector =
606626
createInternalInjector(providerCache, function() {
607627
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
608-
})),
628+
}, strictDi)),
609629
instanceCache = {},
610630
instanceInjector = (instanceCache.$injector =
611631
createInternalInjector(instanceCache, function(servicename) {
612632
var provider = providerInjector.get(servicename + providerSuffix);
613-
return instanceInjector.invoke(provider.$get, provider);
614-
}));
633+
return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
634+
}, strictDi));
615635

616636

617637
forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });
@@ -743,9 +763,14 @@ function createInjector(modulesToLoad) {
743763
}
744764
}
745765

746-
function invoke(fn, self, locals){
766+
function invoke(fn, self, locals, serviceName){
767+
if (typeof locals === 'string') {
768+
serviceName = locals;
769+
locals = null;
770+
}
771+
747772
var args = [],
748-
$inject = annotate(fn),
773+
$inject = annotate(fn, strictDi, serviceName),
749774
length, i,
750775
key;
751776

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

774-
function instantiate(Type, locals) {
799+
function instantiate(Type, locals, serviceName) {
775800
var Constructor = function() {},
776801
instance, returnedValue;
777802

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

784809
return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
785810
}
@@ -796,3 +821,4 @@ function createInjector(modulesToLoad) {
796821
}
797822
}
798823

824+
createInjector.$$annotate = annotate;

src/ng/controller.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function $ControllerProvider() {
7171
assertArgFn(expression, constructor, true);
7272
}
7373

74-
instance = $injector.instantiate(expression, locals);
74+
instance = $injector.instantiate(expression, locals, constructor);
7575

7676
if (identifier) {
7777
if (!(locals && typeof locals.$scope == 'object')) {

0 commit comments

Comments
 (0)