@spinnaker/eslint-plugin 0.0.0-main-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +3 -0
- package/CHANGELOG.md +96 -0
- package/LICENSE.txt +203 -0
- package/README.md +259 -0
- package/babel.config.js +3 -0
- package/base.config.js +86 -0
- package/create-rule.js +67 -0
- package/eslint-plugin.ts +47 -0
- package/index.js +6 -0
- package/newrule.sh +88 -0
- package/none.config.js +18 -0
- package/package.json +55 -0
- package/rules/api-deprecation.spec.ts +79 -0
- package/rules/api-deprecation.ts +254 -0
- package/rules/api-no-slashes.spec.ts +84 -0
- package/rules/api-no-slashes.ts +148 -0
- package/rules/api-no-unused-chaining.spec.ts +26 -0
- package/rules/api-no-unused-chaining.ts +47 -0
- package/rules/import-from-alias-not-npm.spec.ts +22 -0
- package/rules/import-from-alias-not-npm.ts +53 -0
- package/rules/import-from-npm-not-alias.spec.ts +24 -0
- package/rules/import-from-npm-not-alias.ts +56 -0
- package/rules/import-from-npm-not-relative.spec.ts +22 -0
- package/rules/import-from-npm-not-relative.ts +57 -0
- package/rules/import-from-presentation-not-core.spec.ts +44 -0
- package/rules/import-from-presentation-not-core.ts +106 -0
- package/rules/import-relative-within-subpackage.spec.ts +51 -0
- package/rules/import-relative-within-subpackage.ts +71 -0
- package/rules/import-sort.spec.ts +85 -0
- package/rules/import-sort.ts +280 -0
- package/rules/migrate-to-mock-http-client.spec.ts +78 -0
- package/rules/migrate-to-mock-http-client.ts +122 -0
- package/rules/ng-no-component-class.spec.ts +45 -0
- package/rules/ng-no-component-class.ts +68 -0
- package/rules/ng-no-module-export.spec.ts +26 -0
- package/rules/ng-no-module-export.ts +117 -0
- package/rules/ng-no-require-angularjs.spec.ts +27 -0
- package/rules/ng-no-require-angularjs.ts +94 -0
- package/rules/ng-no-require-module-deps.spec.ts +33 -0
- package/rules/ng-no-require-module-deps.ts +211 -0
- package/rules/ng-strictdi.spec.ts +100 -0
- package/rules/ng-strictdi.ts +304 -0
- package/rules/prefer-promise-like.spec.ts +75 -0
- package/rules/prefer-promise-like.ts +108 -0
- package/rules/react2angular-with-error-boundary.spec.ts +29 -0
- package/rules/react2angular-with-error-boundary.ts +118 -0
- package/rules/rest-prefer-static-strings-in-initializer.spec.ts +34 -0
- package/rules/rest-prefer-static-strings-in-initializer.ts +89 -0
- package/template/template-rule.spec.ts +17 -0
- package/template/template-rule.ts +21 -0
- package/test.eslintrc +20 -0
- package/test_rule_against_deck_source.sh +18 -0
- package/tsconfig.json +17 -0
- package/utils/angular-rule/angular-rule.js +302 -0
- package/utils/angular-rule/false-values.js +6 -0
- package/utils/angular-rule/utils.js +624 -0
- package/utils/import-aliases.mock.ts +17 -0
- package/utils/import-aliases.ts +91 -0
- package/utils/mockModule.js +15 -0
- package/utils/ruleTester.js +8 -0
- package/utils/utils.ts +90 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
import type { ArrayExpression, Node } from 'estree';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prefer exporting a module's NAME instead of the entire angular.module()
|
|
8
|
+
*
|
|
9
|
+
* @version 0.1.0
|
|
10
|
+
* @category conventions
|
|
11
|
+
* @sinceAngularVersion 1.x
|
|
12
|
+
*/
|
|
13
|
+
const rule = function (context: Rule.RuleContext) {
|
|
14
|
+
return {
|
|
15
|
+
ArrayExpression: function (node: ArrayExpression) {
|
|
16
|
+
if (isInAngularModuleCall(node)) {
|
|
17
|
+
const requireDotNames = node.elements.map((element) => getRequireDotNameNode(element)).filter((x) => !!x);
|
|
18
|
+
requireDotNames.forEach(([_node, relativePath]) => {
|
|
19
|
+
const message = `Prefer 'import { ANGULARJS_MODULE } from "./module"' over 'require("./module").name'`;
|
|
20
|
+
const fix = getFixForRequireDotName(_node, context.getFilename(), relativePath);
|
|
21
|
+
context.report({ node: _node, message, fix });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const requireDotAnythings = node.elements
|
|
25
|
+
.map((element) => getRequireDotAnythingNode(element))
|
|
26
|
+
.filter((x) => !!x);
|
|
27
|
+
requireDotAnythings.forEach(([_node, requiredString, propertyName]) => {
|
|
28
|
+
const message = `Prefer 'import { default as ANGULARJS_MODULE } from "./module"' over 'require("./module").default'`;
|
|
29
|
+
const fix = getFixForRequireDotAnything(_node, requiredString, propertyName);
|
|
30
|
+
context.report({ node: _node, message, fix });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const bareRequires = node.elements.map((element) => getBareRequireNode(element)).filter((x) => !!x);
|
|
34
|
+
bareRequires.forEach(([_node, requiredString]) => {
|
|
35
|
+
const message = `Prefer 'import ANGULARJS_LIBRARY from "angularjs-library"' over 'require("angularjs-library")'`;
|
|
36
|
+
const fix = getFixForBareRequire(_node, requiredString);
|
|
37
|
+
context.report({ node: _node, message, fix });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
Given:
|
|
46
|
+
angular.module('module', [
|
|
47
|
+
require('angular-ui-bootstrap')
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
Rewrites to:
|
|
51
|
+
import ANGULAR_UI_BOOTSTRAP from 'angular-ui-bootstrap';
|
|
52
|
+
|
|
53
|
+
angular.module('module', [
|
|
54
|
+
ANGULAR_UI_BOOTSTRAP
|
|
55
|
+
]);
|
|
56
|
+
*/
|
|
57
|
+
function getFixForBareRequire(node: Node, requiredString: string) {
|
|
58
|
+
return function (fixer: Rule.RuleFixer) {
|
|
59
|
+
const variableName = requiredString.replace(/[^\w_]/g, '_').toUpperCase();
|
|
60
|
+
const lastImport = findLastImportStatement(node);
|
|
61
|
+
const importStatement = `\nimport ${variableName} from '${requiredString}';`;
|
|
62
|
+
if (lastImport) {
|
|
63
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextAfter(lastImport, importStatement)];
|
|
64
|
+
} else {
|
|
65
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextBeforeRange([0, 0], importStatement)];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
Given:
|
|
72
|
+
angular.module('module', [
|
|
73
|
+
require('some/require/string').default
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
Rewrites to:
|
|
77
|
+
import { default as SOME_REQUIRE_STRING } from 'some/require/string';
|
|
78
|
+
|
|
79
|
+
angular.module('module', [
|
|
80
|
+
SOME_REQUIRE_STRING
|
|
81
|
+
]);
|
|
82
|
+
*/
|
|
83
|
+
function getFixForRequireDotAnything(node: Node, requiredString: string, property: string) {
|
|
84
|
+
return function (fixer: Rule.RuleFixer) {
|
|
85
|
+
const variableName = requiredString
|
|
86
|
+
.replace(/^[^\w_]*/g, '')
|
|
87
|
+
.replace(/[^\w_]/g, '_')
|
|
88
|
+
.toUpperCase();
|
|
89
|
+
const lastImport = findLastImportStatement(node);
|
|
90
|
+
const importStatement = `\nimport { ${property} as ${variableName} } from '${requiredString}';`;
|
|
91
|
+
if (lastImport) {
|
|
92
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextAfter(lastImport, importStatement)];
|
|
93
|
+
} else {
|
|
94
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextBeforeRange([0, 0], importStatement)];
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
Given:
|
|
101
|
+
angular.module('module', [
|
|
102
|
+
require('./path/to/dependency').name
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
Rewrites to:
|
|
106
|
+
import { DEPENDENCY_SYMBOL } from './path/to/dependency';
|
|
107
|
+
|
|
108
|
+
angular.module('module', [
|
|
109
|
+
DEPENDENCY_SYMBOL
|
|
110
|
+
]);
|
|
111
|
+
*/
|
|
112
|
+
function getFixForRequireDotName(node: Node, filename: string, relativePath: string) {
|
|
113
|
+
const modulesPath = filename.replace(/modules\/.*/, 'modules/');
|
|
114
|
+
|
|
115
|
+
const path1 = path.resolve(filename, '..', relativePath);
|
|
116
|
+
const path2 = path.resolve(modulesPath, relativePath.replace(/^([a-zA-Z]+)\//, '$1/src/'));
|
|
117
|
+
const path3 = path.resolve(modulesPath, relativePath);
|
|
118
|
+
|
|
119
|
+
for (const path of [path1, path2, path3]) {
|
|
120
|
+
for (const extension of ['.ts', '.js']) {
|
|
121
|
+
if (fs.existsSync(path + extension)) {
|
|
122
|
+
const fileSource = fs.readFileSync(path + extension).toString();
|
|
123
|
+
const match = /export const name = ([\w_]*);/.exec(fileSource);
|
|
124
|
+
if (match) {
|
|
125
|
+
const variableName = match[1];
|
|
126
|
+
return function (fixer) {
|
|
127
|
+
const lastImport = findLastImportStatement(node);
|
|
128
|
+
const importStatement = `\nimport { ${variableName} } from '${relativePath}';`;
|
|
129
|
+
if (lastImport) {
|
|
130
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextAfter(lastImport, importStatement)];
|
|
131
|
+
} else {
|
|
132
|
+
return [fixer.replaceText(node, variableName), fixer.insertTextBeforeRange([0, 0], importStatement)];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findLastImportStatement(_node: Node) {
|
|
142
|
+
let program = _node as Rule.Node;
|
|
143
|
+
while (program && program.parent) {
|
|
144
|
+
program = program.parent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (program && program.type === 'Program') {
|
|
148
|
+
const imports = program.body.filter((node) => node.type === 'ImportDeclaration');
|
|
149
|
+
if (imports.length) {
|
|
150
|
+
return imports[imports.length - 1];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// require('./some/nested/angularjs/module').name
|
|
156
|
+
function getRequireDotNameNode(node: Node): [Node, string] {
|
|
157
|
+
if (node.type !== 'MemberExpression') return undefined;
|
|
158
|
+
if (node.property.type !== 'Identifier' || node.property.name !== 'name') return undefined;
|
|
159
|
+
if (node.object.type !== 'CallExpression' || (node.object.callee as any).name !== 'require') return undefined;
|
|
160
|
+
if (node.object.arguments.length !== 1 || node.object.arguments[0].type !== 'Literal') return undefined;
|
|
161
|
+
|
|
162
|
+
// [node, './some/nested/angularjs/module']
|
|
163
|
+
return [node, node.object.arguments[0].value as string];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// require('something').anything
|
|
167
|
+
function getRequireDotAnythingNode(node: Node): [Node, string, string] {
|
|
168
|
+
if (node.type !== 'MemberExpression') return undefined;
|
|
169
|
+
if (node.property.type !== 'Identifier') return undefined;
|
|
170
|
+
if (node.object.type !== 'CallExpression' || (node.object.callee as any).name !== 'require') return undefined;
|
|
171
|
+
if (node.object.arguments.length !== 1 || node.object.arguments[0].type !== 'Literal') return undefined;
|
|
172
|
+
|
|
173
|
+
// [node, 'something', 'anything']
|
|
174
|
+
return [node, node.object.arguments[0].value as string, node.property.name];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// require('something')
|
|
178
|
+
function getBareRequireNode(node): [Node, string] {
|
|
179
|
+
if (node.type !== 'CallExpression' || node.callee.name !== 'require') return undefined;
|
|
180
|
+
if (node.arguments.length !== 1 || node.arguments[0].type !== 'Literal') return undefined;
|
|
181
|
+
|
|
182
|
+
// [node, 'something']
|
|
183
|
+
return [node, node.arguments[0].value as string];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isInAngularModuleCall(arrayExpression) {
|
|
187
|
+
const { parent = {} } = arrayExpression;
|
|
188
|
+
if (parent.type === 'CallExpression') {
|
|
189
|
+
const { callee = {} } = parent;
|
|
190
|
+
const { type, object, property } = callee;
|
|
191
|
+
const isAngularModule =
|
|
192
|
+
type === 'MemberExpression' && object && object.name === 'angular' && property && property.name === 'module';
|
|
193
|
+
const isRawModule = type === 'Identifier' && callee.name === 'module';
|
|
194
|
+
|
|
195
|
+
return isAngularModule || isRawModule;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ruleModule: Rule.RuleModule = {
|
|
201
|
+
meta: {
|
|
202
|
+
type: 'problem',
|
|
203
|
+
docs: {
|
|
204
|
+
description: 'Import an angular module name symbol instead of using require("../foo").name',
|
|
205
|
+
},
|
|
206
|
+
fixable: 'code',
|
|
207
|
+
},
|
|
208
|
+
create: rule,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default ruleModule;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import rule from './ng-strictdi';
|
|
2
|
+
import ruleTester from '../utils/ruleTester';
|
|
3
|
+
ruleTester.run('ng-strictdi', rule, {
|
|
4
|
+
valid: [
|
|
5
|
+
{
|
|
6
|
+
code: `
|
|
7
|
+
import { module } from 'angular';
|
|
8
|
+
module('foo', [])
|
|
9
|
+
.directive('myDirective', ['$scope', function($scope) {}]);
|
|
10
|
+
`,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
code: `
|
|
14
|
+
import { module } from 'angular';
|
|
15
|
+
class Controller {
|
|
16
|
+
static $inject = ['$scope'];
|
|
17
|
+
constructor($scope) {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
module('foo', [])
|
|
21
|
+
.directive('myDirective', ['$scope', function($scope) {}]);
|
|
22
|
+
`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: `
|
|
26
|
+
import { module } from 'angular';
|
|
27
|
+
module('foo', [])
|
|
28
|
+
.directive('myDirective', ['$scope', function($scope) {}]);
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
|
|
33
|
+
invalid: [
|
|
34
|
+
{
|
|
35
|
+
errors: [{ message: 'The injected function has 1 parameter(s): ["$scope"], but no annotation was found' }],
|
|
36
|
+
code: `
|
|
37
|
+
const angular = require('angular');
|
|
38
|
+
angular.module('foo', [])
|
|
39
|
+
.controller('myController', function($scope) {});
|
|
40
|
+
`,
|
|
41
|
+
output: `
|
|
42
|
+
const angular = require('angular');
|
|
43
|
+
angular.module('foo', [])
|
|
44
|
+
.controller('myController', ['$scope', function($scope) {}]);
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
errors: [
|
|
49
|
+
{ message: 'The injected function \'MyClass\' has 1 parameter(s): ["$scope"], but no annotation was found' },
|
|
50
|
+
],
|
|
51
|
+
code: `
|
|
52
|
+
import { module } from 'angular';
|
|
53
|
+
// Do not rename this to MyClassController or the checkDi rule will fire twice
|
|
54
|
+
class MyClass {
|
|
55
|
+
constructor($scope) {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
module('foo', []).controller('myClassController', MyClass);
|
|
59
|
+
`,
|
|
60
|
+
output: `
|
|
61
|
+
import { module } from 'angular';
|
|
62
|
+
// Do not rename this to MyClassController or the checkDi rule will fire twice
|
|
63
|
+
class MyClass {
|
|
64
|
+
constructor($scope) {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
MyClass.$inject = ['$scope'];
|
|
68
|
+
module('foo', []).controller('myClassController', MyClass);
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
errors: [{ message: 'The injected function has 1 parameter(s): ["$scope"], but no annotation was found' }],
|
|
73
|
+
code: `
|
|
74
|
+
const angular = require('angular');
|
|
75
|
+
angular.module('foo', [])
|
|
76
|
+
.directive('myDirective', { controller: function namedFunction($scope) {} });
|
|
77
|
+
`,
|
|
78
|
+
output: `
|
|
79
|
+
const angular = require('angular');
|
|
80
|
+
angular.module('foo', [])
|
|
81
|
+
.directive('myDirective', { controller: ['$scope', function namedFunction($scope) {}] });
|
|
82
|
+
`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
errors: [
|
|
86
|
+
{ message: 'The injected function has 2 parameter(s): ["$scope","$location"], but no annotation was found' },
|
|
87
|
+
],
|
|
88
|
+
code: `
|
|
89
|
+
const angular = require('angular');
|
|
90
|
+
angular.module('foo', [])
|
|
91
|
+
.directive('myDirective', { controller: function ($scope, $location) {} });
|
|
92
|
+
`,
|
|
93
|
+
output: `
|
|
94
|
+
const angular = require('angular');
|
|
95
|
+
angular.module('foo', [])
|
|
96
|
+
.directive('myDirective', { controller: ['$scope', '$location', function ($scope, $location) {}] });
|
|
97
|
+
`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* require a consistent DI syntax
|
|
3
|
+
*
|
|
4
|
+
* All your DI should use the same syntax : the Array, function, or $inject syntaxes ("di": [2, "array, function, or $inject"])
|
|
5
|
+
*
|
|
6
|
+
* @version 0.1.0
|
|
7
|
+
* @category conventions
|
|
8
|
+
* @sinceAngularVersion 1.x
|
|
9
|
+
*/
|
|
10
|
+
import type { Rule } from 'eslint';
|
|
11
|
+
import { isEqual } from 'lodash';
|
|
12
|
+
|
|
13
|
+
import angularRule from '../utils/angular-rule/angular-rule';
|
|
14
|
+
import utils from '../utils/angular-rule/utils';
|
|
15
|
+
|
|
16
|
+
const stripUnderscores = true;
|
|
17
|
+
|
|
18
|
+
function normalizeParameter(param) {
|
|
19
|
+
return stripUnderscores ? param : param.replace(/^_(.+)_$/, (match, p1) => p1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const rule = function (context: Rule.RuleContext) {
|
|
23
|
+
const $injectProperties = {};
|
|
24
|
+
|
|
25
|
+
function maybeNoteInjection(node) {
|
|
26
|
+
if (
|
|
27
|
+
node.left &&
|
|
28
|
+
node.left.property &&
|
|
29
|
+
((utils.isLiteralType(node.left.property) && node.left.property.value === '$inject') ||
|
|
30
|
+
(utils.isIdentifierType(node.left.property) && node.left.property.name === '$inject'))
|
|
31
|
+
) {
|
|
32
|
+
$injectProperties[node.left.object.name] = node.right;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDiStrings(name: string) {
|
|
37
|
+
const $inject = $injectProperties[name];
|
|
38
|
+
const elements = $inject && ($inject.elements || $inject.expression.elements);
|
|
39
|
+
return elements && elements.map((el) => el.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compareParamsAndDI(node, name, type, context, params, diStrings) {
|
|
43
|
+
const paramNames = params.map((p) => normalizeParameter(p));
|
|
44
|
+
const diCount = diStrings ? diStrings.length : 0;
|
|
45
|
+
const paramCount = paramNames.length;
|
|
46
|
+
|
|
47
|
+
if (diCount === 0 && diCount !== paramCount) {
|
|
48
|
+
const message =
|
|
49
|
+
`The injected function${name ? ` '${name}'` : ''} ` +
|
|
50
|
+
`has ${paramCount} parameter(s): ${JSON.stringify(paramNames)}, ` +
|
|
51
|
+
`but no annotation was found`;
|
|
52
|
+
|
|
53
|
+
const injectStrings = params.map((p) => `'${p}'`).join(', ');
|
|
54
|
+
|
|
55
|
+
const fix = (fixer) => {
|
|
56
|
+
if (name && type === 'tsclass') {
|
|
57
|
+
return fixer.insertTextBefore(node, `public static $inject = [${injectStrings}];\n `);
|
|
58
|
+
} else if (name && type === 'class') {
|
|
59
|
+
// find class node
|
|
60
|
+
let classNode = node;
|
|
61
|
+
while (classNode.type !== 'ClassDeclaration' && classNode.parent) {
|
|
62
|
+
classNode = classNode.parent;
|
|
63
|
+
}
|
|
64
|
+
return fixer.insertTextAfter(classNode, `\n${name}.$inject = [${injectStrings}];`);
|
|
65
|
+
} else if (name) {
|
|
66
|
+
return fixer.insertTextAfter(node, `\n${name}.$inject = [${injectStrings}];`);
|
|
67
|
+
} else {
|
|
68
|
+
return [fixer.insertTextBefore(node, `[${injectStrings}, `), fixer.insertTextAfter(node, `]`)];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// let program = node;
|
|
73
|
+
// while(program.parent) {program = program.parent}
|
|
74
|
+
// console.log(context.getSourceCode().getText(program));
|
|
75
|
+
|
|
76
|
+
context.report({ node, message, fix });
|
|
77
|
+
} else if (diCount !== paramCount) {
|
|
78
|
+
const message =
|
|
79
|
+
`The injected function${name ? ` '${name}'` : ''} ` +
|
|
80
|
+
`has ${paramCount} parameter(s): ${JSON.stringify(paramNames)}, ` +
|
|
81
|
+
`but there were ${diCount} DI strings${diCount === 0 ? '' : `: ${JSON.stringify(diStrings)} `}`;
|
|
82
|
+
context.report({ node, message });
|
|
83
|
+
} else if (!isEqual(diStrings, paramNames)) {
|
|
84
|
+
const message =
|
|
85
|
+
`The injected function${name ? ` '${name}'` : ''} ` +
|
|
86
|
+
`parameter names: ${JSON.stringify(paramNames)} ` +
|
|
87
|
+
`do not match the DI strings: ${JSON.stringify(diStrings)}`;
|
|
88
|
+
context.report({ node, message });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fromArray(thisGuy) {
|
|
93
|
+
const { node, scope, callExpression } = thisGuy;
|
|
94
|
+
const args = node.elements.slice(0, -1);
|
|
95
|
+
const fn = node.elements.slice(-1)[0];
|
|
96
|
+
const diStrings = args.map((node) => node.value);
|
|
97
|
+
|
|
98
|
+
if (fn.type === 'Identifier') {
|
|
99
|
+
const name = fn.name;
|
|
100
|
+
const result = fromIdentifier({ node: fn, scope, callExpression });
|
|
101
|
+
const params = result.fn.params && result.fn.params.map((param) => param.name);
|
|
102
|
+
return { type: 'array', fn: result.fn, name, params, diStrings };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const params = fn.params && fn.params.map((param) => param.name);
|
|
106
|
+
return { type: 'array', fn, name: undefined, params, diStrings };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fromIdentifier(thisGuy) {
|
|
110
|
+
const { node, scope } = thisGuy;
|
|
111
|
+
const reference = scope.references.find((r) => r.identifier.name === node.name);
|
|
112
|
+
const resolved = reference && reference.resolved;
|
|
113
|
+
if (resolved) {
|
|
114
|
+
const { defs, scope: resolvedScope } = resolved;
|
|
115
|
+
return processThisGuy({ node: defs[0].node, scope: resolvedScope });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fromVariableDeclarator(thisGuy) {
|
|
120
|
+
const { node, scope } = thisGuy;
|
|
121
|
+
const { name } = node.id;
|
|
122
|
+
const fn = node.init;
|
|
123
|
+
|
|
124
|
+
const variable = scope.variables.find((v) => v.name === name);
|
|
125
|
+
|
|
126
|
+
if (!variable) {
|
|
127
|
+
throw new Error(`Weird, I couldn't find variable '${name}' in scope?`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (variable.defs.length > 1) {
|
|
131
|
+
throw new Error('It is pretty unexpected to find more than one def in this guys variable?');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const params = fn.params.map((param) => param.name);
|
|
135
|
+
const diStrings = getDiStrings(name);
|
|
136
|
+
|
|
137
|
+
// TODO: is this really function?
|
|
138
|
+
return { type: 'function', fn, name, params, diStrings };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function fromClassDeclaration(thisGuy) {
|
|
142
|
+
const { node } = thisGuy;
|
|
143
|
+
const { name } = node.id;
|
|
144
|
+
const ctor = node.body.body.find((node) => node.type === 'MethodDefinition' && node.kind === 'constructor');
|
|
145
|
+
if (!ctor) return null;
|
|
146
|
+
const isTypescript = !!context.getFilename().match(/\.tsx?$/);
|
|
147
|
+
const params = ctor.value.params.map((param) =>
|
|
148
|
+
param.type === 'TSParameterProperty' ? param.parameter.name : param.name,
|
|
149
|
+
);
|
|
150
|
+
const $inject = node.body.body.find(
|
|
151
|
+
(node) => node.type === 'ClassProperty' && node.static && node.key.name === '$inject',
|
|
152
|
+
);
|
|
153
|
+
const diStrings = $inject ? $inject.value.elements.map((el) => el.value) : getDiStrings(name);
|
|
154
|
+
return { type: isTypescript ? 'tsclass' : 'class', fn: ctor, name, params, diStrings };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function fromFunction(thisGuy) {
|
|
158
|
+
const { node: fn } = thisGuy;
|
|
159
|
+
const name = fn.type === 'FunctionDeclaration' ? fn.id.name : fn.name;
|
|
160
|
+
const params = fn.params.map((param) => param.name);
|
|
161
|
+
const diStrings = getDiStrings(name);
|
|
162
|
+
return { type: 'function', fn, name, params, diStrings };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function processThisGuy(thisGuy) {
|
|
166
|
+
if (!thisGuy || !thisGuy.node) {
|
|
167
|
+
throw new Error('processThisGuy: Unexpected null argument');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
switch (thisGuy.node.type) {
|
|
171
|
+
case 'ArrayExpression':
|
|
172
|
+
return fromArray(thisGuy);
|
|
173
|
+
case 'ArrowFunctionExpression':
|
|
174
|
+
case 'FunctionExpression':
|
|
175
|
+
case 'FunctionDeclaration':
|
|
176
|
+
return fromFunction(thisGuy);
|
|
177
|
+
case 'Identifier':
|
|
178
|
+
return fromIdentifier(thisGuy);
|
|
179
|
+
case 'VariableDeclarator':
|
|
180
|
+
return fromVariableDeclarator(thisGuy);
|
|
181
|
+
case 'ClassDeclaration':
|
|
182
|
+
return fromClassDeclaration(thisGuy);
|
|
183
|
+
case 'MemberExpression': {
|
|
184
|
+
const memberExpression = context.getSourceCode().getText(thisGuy.node);
|
|
185
|
+
// allowlist some known symbols
|
|
186
|
+
if (!['angular.noop', 'noop'].includes(memberExpression)) {
|
|
187
|
+
console.warn(`Unable to handle MemberExpression: ${memberExpression}`);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
case 'ImportSpecifier': {
|
|
192
|
+
// const importSpecifier = context.getSourceCode().getText(thisGuy.node);
|
|
193
|
+
// console.warn(`warn: Unable to handle ImportSpecifier: ${importSpecifier} in ${context.getFilename()}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
console.error(context.getSourceCode().getText(thisGuy.node));
|
|
198
|
+
throw new Error(`Unknown type: ${thisGuy.node.type}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function checkDi(callee, thisGuy) {
|
|
203
|
+
if (!thisGuy) {
|
|
204
|
+
throw new Error('checkDi: unexpected null argument');
|
|
205
|
+
} else if (!thisGuy.node) {
|
|
206
|
+
throw new Error('checkDi: missing node in thisGuy');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let result;
|
|
210
|
+
try {
|
|
211
|
+
result = processThisGuy(thisGuy);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`Internal error while processing ${context.getFilename()}`);
|
|
214
|
+
console.error(context.getSourceCode().getText(thisGuy.callExpression));
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
if (!result) return;
|
|
218
|
+
const { type, fn, name, params, diStrings } = result;
|
|
219
|
+
|
|
220
|
+
// If there's an array, validate it
|
|
221
|
+
if (type === 'array') {
|
|
222
|
+
const expectedTypes = ['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration'];
|
|
223
|
+
if (!expectedTypes.includes(fn.type)) {
|
|
224
|
+
const message = `Array-style: The last element should be an injected function, but it was: ${fn.type}`;
|
|
225
|
+
return context.report({ node: fn, message });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!diStrings.every((str) => typeof str === 'string')) {
|
|
229
|
+
return context.report({ node: fn, message: `Array-style: Elements [0..n-2] should all be strings` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (params.length) {
|
|
234
|
+
compareParamsAndDI(fn, name, type, context, params, diStrings);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
'angular?animation': checkDi,
|
|
240
|
+
'angular?config': checkDi,
|
|
241
|
+
'angular?controller': checkDi,
|
|
242
|
+
'angular?component': function (callee, thisGuy) {
|
|
243
|
+
if (thisGuy.node.type === 'ObjectExpression') {
|
|
244
|
+
const property = thisGuy.node.properties.find((prop) => prop.key.name === 'controller');
|
|
245
|
+
if (property) {
|
|
246
|
+
if (property.value.type !== 'Literal') {
|
|
247
|
+
return checkDi(callee, Object.assign({}, thisGuy, { node: property.value }));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
'angular?decorator': checkDi,
|
|
253
|
+
'angular?directive': function (callee, thisGuy) {
|
|
254
|
+
if (thisGuy.node.type === 'ObjectExpression') {
|
|
255
|
+
const property = thisGuy.node.properties.find((prop) => prop.key.name === 'controller');
|
|
256
|
+
if (property) {
|
|
257
|
+
if (property.value.type !== 'Literal') {
|
|
258
|
+
return checkDi(callee, Object.assign({}, thisGuy, { node: property.value }));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
'angular?factory': checkDi,
|
|
264
|
+
'angular?filter': checkDi,
|
|
265
|
+
'angular?inject': checkDi,
|
|
266
|
+
'angular?run': checkDi,
|
|
267
|
+
'angular?service': checkDi,
|
|
268
|
+
'angular?provider': function (callee, providerFn, $get) {
|
|
269
|
+
checkDi(null, providerFn);
|
|
270
|
+
checkDi(null, $get);
|
|
271
|
+
},
|
|
272
|
+
'CallExpression:exit': function (node) {
|
|
273
|
+
const { object, property } = node.callee;
|
|
274
|
+
if (object && object.name === '$provide' && property && property.name === 'decorator') {
|
|
275
|
+
checkDi(null, { node: node.arguments[1], scope: context.getScope() });
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
AssignmentExpression: function (node) {
|
|
279
|
+
maybeNoteInjection(node);
|
|
280
|
+
},
|
|
281
|
+
ClassDeclaration: function (node) {
|
|
282
|
+
const interfaces = ['IController', 'ng.IController'];
|
|
283
|
+
const implementsIController = (node.implements || []).some((impl) => interfaces.includes(impl.expression.name));
|
|
284
|
+
const isNamedSortaLikeOne = node.id.name.match(/(Ctrl|Controller)$/);
|
|
285
|
+
const isClassController = implementsIController || isNamedSortaLikeOne;
|
|
286
|
+
|
|
287
|
+
if (isClassController) {
|
|
288
|
+
checkDi(null, { node: node });
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const ruleModule: Rule.RuleModule = {
|
|
295
|
+
meta: {
|
|
296
|
+
type: 'problem',
|
|
297
|
+
docs: {
|
|
298
|
+
description: 'All angularjs functions must be explicitly annotated',
|
|
299
|
+
},
|
|
300
|
+
fixable: 'code',
|
|
301
|
+
},
|
|
302
|
+
create: angularRule(rule),
|
|
303
|
+
};
|
|
304
|
+
export default ruleModule;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import rule from './prefer-promise-like';
|
|
2
|
+
import ruleTester from '../utils/ruleTester';
|
|
3
|
+
const errorMessage = `Prefer using PromiseLike type instead of AngularJS IPromise.`;
|
|
4
|
+
const unusedImportErrorMessage = `Unused IPromise import`;
|
|
5
|
+
|
|
6
|
+
ruleTester.run('prefer-promise-like', rule, {
|
|
7
|
+
valid: [
|
|
8
|
+
{
|
|
9
|
+
code: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
|
|
13
|
+
invalid: [
|
|
14
|
+
// IPromise in variable
|
|
15
|
+
{
|
|
16
|
+
code: `const foo: IPromise<any> = API.one('foo', 'bar').get();`,
|
|
17
|
+
output: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
|
|
18
|
+
errors: [errorMessage],
|
|
19
|
+
},
|
|
20
|
+
// IPromise in function arg
|
|
21
|
+
{
|
|
22
|
+
code: `function foo(promise: IPromise<any>) {}`,
|
|
23
|
+
output: `function foo(promise: PromiseLike<any>) {}`,
|
|
24
|
+
errors: [errorMessage],
|
|
25
|
+
},
|
|
26
|
+
// IPromise in class method return
|
|
27
|
+
{
|
|
28
|
+
code: `class Foo { foo(): IPromise<any> {} }`,
|
|
29
|
+
output: `class Foo { foo(): PromiseLike<any> {} }`,
|
|
30
|
+
errors: [errorMessage],
|
|
31
|
+
},
|
|
32
|
+
// ng.IPromise in variable
|
|
33
|
+
{
|
|
34
|
+
code: `const foo: ng.IPromise<any> = API.one('foo', 'bar').get();`,
|
|
35
|
+
output: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
|
|
36
|
+
errors: [errorMessage],
|
|
37
|
+
},
|
|
38
|
+
// ng.IPromise in function arg
|
|
39
|
+
{
|
|
40
|
+
code: `function foo(promise: ng.IPromise<any>) {}`,
|
|
41
|
+
output: `function foo(promise: PromiseLike<any>) {}`,
|
|
42
|
+
errors: [errorMessage],
|
|
43
|
+
},
|
|
44
|
+
// ng.IPromise in class method return
|
|
45
|
+
{
|
|
46
|
+
code: `class Foo { foo(): ng.IPromise<any> {} }`,
|
|
47
|
+
output: `class Foo { foo(): PromiseLike<any> {} }`,
|
|
48
|
+
errors: [errorMessage],
|
|
49
|
+
},
|
|
50
|
+
// Unused IPromise import
|
|
51
|
+
{
|
|
52
|
+
code: `import { IPromise } from 'angular';`,
|
|
53
|
+
output: ``,
|
|
54
|
+
errors: [unusedImportErrorMessage],
|
|
55
|
+
},
|
|
56
|
+
// Unused IPromise import 2
|
|
57
|
+
{
|
|
58
|
+
code: `import { module, IPromise } from 'angular';`,
|
|
59
|
+
output: `import { module } from 'angular';`,
|
|
60
|
+
errors: [unusedImportErrorMessage],
|
|
61
|
+
},
|
|
62
|
+
// Unused IPromise import 3
|
|
63
|
+
{
|
|
64
|
+
code: `import { IPromise, module } from 'angular';`,
|
|
65
|
+
output: `import { module } from 'angular';`,
|
|
66
|
+
errors: [unusedImportErrorMessage],
|
|
67
|
+
},
|
|
68
|
+
// Unused IPromise import 4
|
|
69
|
+
{
|
|
70
|
+
code: `import { QService, IPromise, module } from 'angular';`,
|
|
71
|
+
output: `import { QService, module } from 'angular';`,
|
|
72
|
+
errors: [unusedImportErrorMessage],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|