@spinnaker/eslint-plugin 2026.2.3 → 2026.3.0

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.
@@ -1,304 +0,0 @@
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.sourceCode.getScope(node) });
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;
@@ -1,75 +0,0 @@
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
- });
@@ -1,108 +0,0 @@
1
- import type { TSESTree } from '@typescript-eslint/types';
2
- import type { Rule } from 'eslint';
3
- import _ from 'lodash';
4
-
5
- /**
6
- * No slashes in string literals passed to API.one() / API.all()
7
- *
8
- * @version 0.1.0
9
- * @category
10
- */
11
- const rule = function (context: Rule.RuleContext) {
12
- return {
13
- TSTypeReference: function (_node) {
14
- const node = _node as TSESTree.TSTypeReference;
15
- // var foo: IPromise<any> = bar()
16
- // ^^^^^^^^
17
- const type_IPromise = {
18
- type: 'TSTypeReference',
19
- typeName: {
20
- type: 'Identifier',
21
- name: 'IPromise',
22
- },
23
- };
24
-
25
- // var foo: ng.IPromise<any> = bar()
26
- // ^^^^^^^^^^^
27
- const type_ng_IPromise = {
28
- type: 'TSTypeReference',
29
- typeName: {
30
- type: 'TSQualifiedName',
31
- left: {
32
- type: 'Identifier',
33
- name: 'ng',
34
- },
35
- right: {
36
- type: 'Identifier',
37
- name: 'IPromise',
38
- },
39
- },
40
- };
41
-
42
- const message = `Prefer using PromiseLike type instead of AngularJS IPromise.`;
43
- const fix = (fixer) => fixer.replaceText(node.typeName, 'PromiseLike');
44
- if (_.isMatch(node, type_IPromise)) {
45
- context.report({ fix, node: node.typeName as Rule.Node, message });
46
- } else if (_.isMatch(node, type_ng_IPromise)) {
47
- context.report({ fix, node: node.typeName as Rule.Node, message });
48
- }
49
- },
50
-
51
- // If there are any unused IPromise imports, remove them
52
- ImportDeclaration: function (_node: any) {
53
- const node = _node;
54
- const importIPromise = {
55
- type: 'ImportSpecifier',
56
- imported: {
57
- type: 'Identifier',
58
- name: 'IPromise',
59
- },
60
- };
61
-
62
- const message = `Unused IPromise import`;
63
-
64
- // import { foo, IPromise, bar } from 'angular';
65
- // ^^^^^^^^
66
- const specifiers = node.specifiers || [];
67
- const foundIPromiseImport = specifiers.find((s) => _.isMatch(s, importIPromise));
68
-
69
- const variables = context.sourceCode.getScope(node).variables;
70
- const variable = variables.find((x) => x.defs.some((def) => def.node === foundIPromiseImport));
71
- const unused = variable && variable.references.length === 0;
72
-
73
- const fix = (fixer: Rule.RuleFixer) => {
74
- const importCount = node.specifiers.length;
75
- if (importCount === 1) {
76
- // Delete the whole import
77
- return fixer.replaceText(node, '');
78
- } else {
79
- // Delete only IPromise from the import
80
- const source = context
81
- .getSourceCode()
82
- .getText(node)
83
- .replace(/,\s*IPromise/g, '')
84
- .replace(/IPromise\s*,\s*/g, '');
85
-
86
- return fixer.replaceText(node, source);
87
- }
88
- };
89
-
90
- if (foundIPromiseImport && unused) {
91
- context.report({ node, message, fix });
92
- }
93
- },
94
- };
95
- };
96
-
97
- const ruleModule: Rule.RuleModule = {
98
- meta: {
99
- type: 'problem',
100
- docs: {
101
- description: ``,
102
- },
103
- fixable: 'code',
104
- },
105
- create: rule,
106
- };
107
-
108
- export default ruleModule;
@@ -1,29 +0,0 @@
1
- import rule from './react2angular-with-error-boundary';
2
- import ruleTester from '../utils/ruleTester';
3
- const errorMessage = `Wrap react2angular components in an error boundary using 'withErrorBoundary()'`;
4
-
5
- ruleTester.run('api-no-slashes', rule, {
6
- valid: [
7
- {
8
- code: `react2angular(withErrorBoundary(MyComponent), ['foo', 'bar']);`,
9
- },
10
- ],
11
-
12
- invalid: [
13
- {
14
- code: `react2angular(MyComponent, ['foo', 'bar']);`,
15
- errors: [errorMessage],
16
- output: `import { withErrorBoundary } from '@spinnaker/core';\nreact2angular(withErrorBoundary(MyComponent, 'react2angular component'), ['foo', 'bar']);`,
17
- },
18
-
19
- {
20
- errors: [errorMessage],
21
- code: `import { SpinnakerContainer } from '@spinnaker/core';
22
- module(SPINNAKER_CONTAINER_COMPONENT, []).component('spinnakerContainer',
23
- react2angular(SpinnakerContainer, ['authenticating', 'routing']));`,
24
- output: `import { SpinnakerContainer, withErrorBoundary } from '@spinnaker/core';
25
- module(SPINNAKER_CONTAINER_COMPONENT, []).component('spinnakerContainer',
26
- react2angular(withErrorBoundary(SpinnakerContainer, 'spinnakerContainer'), ['authenticating', 'routing']));`,
27
- },
28
- ],
29
- });
@@ -1,119 +0,0 @@
1
- import type { Rule } from 'eslint';
2
- import _ from 'lodash';
3
- import { isLiteral } from '../utils/utils';
4
-
5
- /**
6
- * react2angular: Always wrap react components in an error boundary
7
- * Uses withErrorBoundary from core/presentation
8
- * @version 0.1.0
9
- */
10
- const rule = function (context: Rule.RuleContext) {
11
- let coreImport: any;
12
-
13
- return {
14
- // Find an import from @spinnaker/core or core/presentation
15
- // This will be used to add the import for withErrorBoundary
16
- ImportDeclaration: function (_node: any) {
17
- const node = _node;
18
- // import { foo, bar } from 'package';
19
- // ^^^^^^^
20
- const from = node.source.value || '';
21
- if (from === '@spinnaker/core') {
22
- coreImport = node;
23
- }
24
- },
25
- CallExpression: function (_node: any) {
26
- const node = _node;
27
- // Find:
28
- // react2angular(SomeComponent, ...)
29
- const match = {
30
- type: 'CallExpression',
31
- callee: {
32
- type: 'Identifier',
33
- name: 'react2angular',
34
- },
35
- };
36
-
37
- if (!_.isMatch(node, match)) {
38
- return;
39
- }
40
-
41
- const r2aComponent = node.arguments[0];
42
-
43
- const wrappedInErrorBoundaryMatch = {
44
- type: 'CallExpression',
45
- callee: {
46
- type: 'Identifier',
47
- name: 'withErrorBoundary',
48
- },
49
- };
50
-
51
- // The react2angular component is already wrapped, nice!
52
- if (_.isMatch(r2aComponent, wrappedInErrorBoundaryMatch)) {
53
- return;
54
- }
55
-
56
- const message = `Wrap react2angular components in an error boundary using 'withErrorBoundary()'`;
57
- const filename = context.getFilename();
58
- const originalComponentSrc = context.getSourceCode().getText(r2aComponent);
59
-
60
- // Try to determine the angularjs component name
61
- // Look for component('angularComponentName', react2angular(ReactComponent, ....))
62
- // ^^^^^^^^^^^^^^^^^^^^^^
63
-
64
- const parentMatch = {
65
- type: 'CallExpression',
66
- callee: {
67
- property: {
68
- type: 'Identifier',
69
- name: 'component',
70
- },
71
- },
72
- };
73
- const isComponentCallExpression = (node): node is any => _.isMatch(node, parentMatch);
74
-
75
- let componentName = `'react2angular component'`;
76
- const parentNode = node.parent;
77
- if (isComponentCallExpression(parentNode)) {
78
- const [componentNameLiteralNode, arg2] = parentNode.arguments || [];
79
- if (arg2 === node && isLiteral(componentNameLiteralNode)) {
80
- componentName = componentNameLiteralNode.raw;
81
- }
82
- }
83
-
84
- const fix = (fixer: Rule.RuleFixer) => {
85
- const wrapped = `withErrorBoundary(${originalComponentSrc}, ${componentName})`;
86
- const insertErrorBoundary = fixer.replaceText(r2aComponent, wrapped);
87
-
88
- const fixes = [insertErrorBoundary];
89
-
90
- if (coreImport && coreImport.specifiers.length > 0) {
91
- // Append to the existing core/presentation or @spinnaker/core import
92
- const lastImport = coreImport.specifiers[coreImport.specifiers.length - 1];
93
- fixes.push(fixer.insertTextAfter(lastImport, `, withErrorBoundary`));
94
- } else {
95
- const importString = filename.includes('/modules/core/')
96
- ? 'core/presentation/SpinErrorBoundary'
97
- : '@spinnaker/core';
98
- fixes.push(fixer.insertTextBeforeRange([0, 0], `import { withErrorBoundary } from '${importString}';\n`));
99
- }
100
- return fixes;
101
- };
102
-
103
- context.report({ message, node, fix });
104
- },
105
- };
106
- };
107
-
108
- const ruleModule: Rule.RuleModule = {
109
- meta: {
110
- type: 'problem',
111
- docs: {
112
- description: `react2angular: Always wrap react components in an error boundary`,
113
- },
114
- fixable: 'code',
115
- },
116
- create: rule,
117
- };
118
-
119
- export default ruleModule;