@spinnaker/eslint-plugin 0.0.0-2025.1-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.
Files changed (62) hide show
  1. package/.eslintignore +3 -0
  2. package/CHANGELOG.md +96 -0
  3. package/LICENSE.txt +203 -0
  4. package/README.md +259 -0
  5. package/babel.config.js +3 -0
  6. package/base.config.js +86 -0
  7. package/create-rule.js +67 -0
  8. package/eslint-plugin.ts +47 -0
  9. package/index.js +6 -0
  10. package/newrule.sh +88 -0
  11. package/none.config.js +18 -0
  12. package/package.json +56 -0
  13. package/rules/api-deprecation.spec.ts +79 -0
  14. package/rules/api-deprecation.ts +255 -0
  15. package/rules/api-no-slashes.spec.ts +84 -0
  16. package/rules/api-no-slashes.ts +148 -0
  17. package/rules/api-no-unused-chaining.spec.ts +26 -0
  18. package/rules/api-no-unused-chaining.ts +47 -0
  19. package/rules/import-from-alias-not-npm.spec.ts +22 -0
  20. package/rules/import-from-alias-not-npm.ts +53 -0
  21. package/rules/import-from-npm-not-alias.spec.ts +24 -0
  22. package/rules/import-from-npm-not-alias.ts +56 -0
  23. package/rules/import-from-npm-not-relative.spec.ts +22 -0
  24. package/rules/import-from-npm-not-relative.ts +57 -0
  25. package/rules/import-from-presentation-not-core.spec.ts +44 -0
  26. package/rules/import-from-presentation-not-core.ts +107 -0
  27. package/rules/import-relative-within-subpackage.spec.ts +51 -0
  28. package/rules/import-relative-within-subpackage.ts +71 -0
  29. package/rules/import-sort.spec.ts +85 -0
  30. package/rules/import-sort.ts +281 -0
  31. package/rules/migrate-to-mock-http-client.spec.ts +78 -0
  32. package/rules/migrate-to-mock-http-client.ts +123 -0
  33. package/rules/ng-no-component-class.spec.ts +45 -0
  34. package/rules/ng-no-component-class.ts +68 -0
  35. package/rules/ng-no-module-export.spec.ts +26 -0
  36. package/rules/ng-no-module-export.ts +117 -0
  37. package/rules/ng-no-require-angularjs.spec.ts +27 -0
  38. package/rules/ng-no-require-angularjs.ts +94 -0
  39. package/rules/ng-no-require-module-deps.spec.ts +33 -0
  40. package/rules/ng-no-require-module-deps.ts +211 -0
  41. package/rules/ng-strictdi.spec.ts +100 -0
  42. package/rules/ng-strictdi.ts +304 -0
  43. package/rules/prefer-promise-like.spec.ts +75 -0
  44. package/rules/prefer-promise-like.ts +108 -0
  45. package/rules/react2angular-with-error-boundary.spec.ts +29 -0
  46. package/rules/react2angular-with-error-boundary.ts +118 -0
  47. package/rules/rest-prefer-static-strings-in-initializer.spec.ts +34 -0
  48. package/rules/rest-prefer-static-strings-in-initializer.ts +89 -0
  49. package/template/template-rule.spec.ts +17 -0
  50. package/template/template-rule.ts +21 -0
  51. package/test.eslintrc +20 -0
  52. package/test_rule_against_deck_source.sh +18 -0
  53. package/tsconfig.json +17 -0
  54. package/utils/angular-rule/angular-rule.js +302 -0
  55. package/utils/angular-rule/false-values.js +6 -0
  56. package/utils/angular-rule/utils.js +624 -0
  57. package/utils/ast.ts +11 -0
  58. package/utils/import-aliases.mock.ts +17 -0
  59. package/utils/import-aliases.ts +91 -0
  60. package/utils/mockModule.js +15 -0
  61. package/utils/ruleTester.js +8 -0
  62. package/utils/utils.ts +90 -0
@@ -0,0 +1,107 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { ImportDeclaration, ImportSpecifier } from 'estree';
3
+ import { getImportName } from '../utils/ast';
4
+
5
+ const migratedPresentationModules = ['Icon', 'IconNames', 'Illustration', 'IllustrationName'];
6
+
7
+ const removeImportFromCore = (
8
+ context: Rule.RuleContext,
9
+ importSpecifierNode: ImportSpecifier & Rule.NodeParentExtension,
10
+ fixer: Rule.RuleFixer,
11
+ ) => {
12
+ const sourceCode = context.getSourceCode();
13
+ const fixes = [];
14
+
15
+ if ((importSpecifierNode.parent as ImportDeclaration).specifiers.length === 1) {
16
+ // The node is the only import specifier in the declaration, so remove the whole import
17
+ // declaration.
18
+ fixes.push(fixer.remove(importSpecifierNode.parent));
19
+ } else {
20
+ // Remove the import specifier.
21
+ fixes.push(fixer.remove(importSpecifierNode));
22
+ const isNextTokenComma = sourceCode.getTokenAfter(importSpecifierNode).value === ',';
23
+ if (isNextTokenComma) {
24
+ // Remove the trailing comma in the import specifier as well.
25
+ fixes.push(fixer.remove(sourceCode.getTokenAfter(importSpecifierNode)));
26
+ }
27
+ }
28
+
29
+ return fixes;
30
+ };
31
+
32
+ const addImportToPresentation = (
33
+ context: Rule.RuleContext,
34
+ importSpecifierNode: ImportSpecifier,
35
+ fixer: Rule.RuleFixer,
36
+ ) => {
37
+ const sourceCode = context.getSourceCode();
38
+ const fixes = [];
39
+
40
+ // Use the alias if it is available in the old import specifier.
41
+ const importSpecifierText =
42
+ importSpecifierNode.local.name === getImportName(importSpecifierNode.imported)
43
+ ? getImportName(importSpecifierNode.imported)
44
+ : `${getImportName(importSpecifierNode.imported)} as ${importSpecifierNode.local.name}`;
45
+
46
+ // Check if @spinnaker/presentation is already imported.
47
+ const spinnakerPresentationImport = sourceCode.ast.body.find(
48
+ (node) => node.type === 'ImportDeclaration' && node.source.value === '@spinnaker/presentation',
49
+ ) as ImportDeclaration;
50
+
51
+ if (spinnakerPresentationImport) {
52
+ // If @spinnaker/presentation is already imported, then append our import specifier to the last
53
+ // import specifier in this import declaration.
54
+ const lastSpecifier = spinnakerPresentationImport.specifiers[spinnakerPresentationImport.specifiers.length - 1];
55
+ fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${importSpecifierText}`));
56
+ } else {
57
+ // If @spinnaker/presentation import is not available, then add one as the last import declaration
58
+ // along with our module's import specifier.
59
+ const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
60
+ const lastImportDeclaration = importDeclarations[importDeclarations.length - 1];
61
+
62
+ fixes.push(
63
+ fixer.insertTextAfter(lastImportDeclaration, `\nimport {${importSpecifierText}} from '@spinnaker/presentation';`),
64
+ );
65
+ }
66
+
67
+ return fixes;
68
+ };
69
+
70
+ const moveImportToPresentation = (
71
+ context: Rule.RuleContext,
72
+ node: ImportSpecifier & Rule.NodeParentExtension,
73
+ fixer: Rule.RuleFixer,
74
+ ) => [...removeImportFromCore(context, node, fixer), ...addImportToPresentation(context, node, fixer)];
75
+
76
+ const rule = (context: Rule.RuleContext) => {
77
+ return {
78
+ ImportSpecifier(node: ImportSpecifier & Rule.NodeParentExtension) {
79
+ if (
80
+ migratedPresentationModules.includes(getImportName(node.imported)) &&
81
+ (node.parent as ImportDeclaration).source.value === '@spinnaker/core'
82
+ ) {
83
+ const message = `${getImportName(node.imported)} must be imported from @spinnaker/presentation`;
84
+ const fix = (fixer) => moveImportToPresentation(context, node, fixer);
85
+ context.report({
86
+ node,
87
+ message,
88
+ fix,
89
+ });
90
+ }
91
+ },
92
+ };
93
+ };
94
+
95
+ const ruleModule: Rule.RuleModule = {
96
+ meta: {
97
+ type: 'problem',
98
+ docs: {
99
+ description: `Enforces import of presentation modules from @spinnaker/presentation`,
100
+ },
101
+ fixable: 'code',
102
+ },
103
+
104
+ create: rule,
105
+ };
106
+
107
+ export default ruleModule;
@@ -0,0 +1,51 @@
1
+ /* eslint-disable @spinnaker/import-sort */
2
+ import '../utils/import-aliases.mock';
3
+ import rule from './import-relative-within-subpackage';
4
+ import ruleTester from '../utils/ruleTester';
5
+
6
+ ruleTester.run('import-relative-within-subpackage', rule, {
7
+ valid: [
8
+ {
9
+ filename: '/root/spinnaker/deck/packages/amazon/package/amazon_source_file.ts',
10
+ code: `import { Anything } from '../subpackage/foo';`,
11
+ },
12
+ {
13
+ filename: '/root/spinnaker/deck/packages/amazon/src/package/amazon_source_file.ts',
14
+ code: `import { Anything } from '../subpackage/foo';`,
15
+ },
16
+ ],
17
+
18
+ invalid: [
19
+ {
20
+ filename: '/root/spinnaker/deck/packages/core/subpackage/core_source_file.ts',
21
+ code: `import { Anything } from 'core/subpackage/foo';`,
22
+ output: `import { Anything } from './foo';`,
23
+ errors: [
24
+ 'Do not use an alias to import from core/subpackage from code inside core/subpackage. Instead, use a relative import',
25
+ ],
26
+ },
27
+ {
28
+ filename: '/root/spinnaker/deck/packages/core/subpackage/core_source_file.ts',
29
+ code: `import { Anything } from 'core/subpackage/foo/bar';`,
30
+ output: `import { Anything } from './foo/bar';`,
31
+ errors: [
32
+ 'Do not use an alias to import from core/subpackage from code inside core/subpackage. Instead, use a relative import',
33
+ ],
34
+ },
35
+ {
36
+ filename: '/root/spinnaker/deck/packages/core/subpackage/nest/core_source_file.ts',
37
+ code: `import { Anything } from 'core/subpackage/foo/bar';`,
38
+ output: `import { Anything } from '../foo/bar';`,
39
+ errors: [
40
+ 'Do not use an alias to import from core/subpackage from code inside core/subpackage. Instead, use a relative import',
41
+ ],
42
+ },
43
+ {
44
+ filename: '/root/spinnaker/deck/packages/core/subpackage/nest/core_source_file.ts',
45
+ code: `import { Anything } from 'core/subpackage';`,
46
+ errors: [
47
+ 'Do not use an alias to import from core/subpackage from code inside core/subpackage. Instead, use a relative import',
48
+ ],
49
+ },
50
+ ],
51
+ });
@@ -0,0 +1,71 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { ImportDeclaration } from 'estree';
3
+ import path from 'path';
4
+
5
+ import { getAliasImport, getAllSpinnakerPackages, getSourceFileDetails } from '../utils/import-aliases';
6
+
7
+ /**
8
+ * A group of rules that enforce spinnaker ES6 import alias conventions.
9
+ *
10
+ * Source code in a package (i.e., `core/presentation` should not import from the same subpackage using an alias
11
+ * `core/presentation`.
12
+ * Instead, it should import relatively `../../path/file`
13
+ *
14
+ * @version 0.1.0
15
+ * @category conventions
16
+ */
17
+ const rule = function (context: Rule.RuleContext) {
18
+ const sourceFile = context.getFilename();
19
+ const { modulesPath, ownPackage, ownSubPackage, filePath } = getSourceFileDetails(sourceFile);
20
+ if (!ownPackage) {
21
+ return {};
22
+ }
23
+ const allSpinnakerPackages = getAllSpinnakerPackages(modulesPath);
24
+
25
+ return {
26
+ ImportDeclaration: function (node: ImportDeclaration & Rule.NodeParentExtension) {
27
+ if (node.source.type !== 'Literal' || !node.source.value) {
28
+ return;
29
+ }
30
+
31
+ const importString = node.source.value as string;
32
+ const aliasImport = getAliasImport(allSpinnakerPackages, importString);
33
+ if (!aliasImport || aliasImport.pkg !== ownPackage || aliasImport.subPkg !== ownSubPackage) {
34
+ return;
35
+ }
36
+
37
+ const { pkg, subPkg, importPath } = aliasImport;
38
+
39
+ const message =
40
+ `Do not use an alias to import from ${pkg}/${subPkg} from code inside ${pkg}/${subPkg}.` +
41
+ ` Instead, use a relative import`;
42
+
43
+ const fix = (fixer: Rule.RuleFixer) => {
44
+ const relativeDir = path.relative(path.dirname(filePath), path.dirname(importPath)) || '.';
45
+ let newPath = path.join(relativeDir, path.basename(importPath));
46
+ newPath = newPath.match(/^\.?\.\//) ? newPath : './' + newPath;
47
+ return fixer.replaceText(node.source, `'${newPath}'`);
48
+ };
49
+
50
+ if (aliasImport.importPath === aliasImport.subPkg) {
51
+ // Do not try to fix: import from 'alias/subpkg'
52
+ context.report({ node, message });
53
+ } else {
54
+ // Do try to fix: import from 'alias/subpkg/nestedimport'
55
+ context.report({ fix, node, message });
56
+ }
57
+ },
58
+ };
59
+ };
60
+
61
+ const importAliasesRule: Rule.RuleModule = {
62
+ meta: {
63
+ type: 'problem',
64
+ docs: {
65
+ description: `Enforces spinnaker ES6 import conventions for package aliases`,
66
+ },
67
+ fixable: 'code',
68
+ },
69
+ create: rule,
70
+ };
71
+ export default importAliasesRule;
@@ -0,0 +1,85 @@
1
+ import rule from './import-sort';
2
+ import ruleTester from '../utils/ruleTester';
3
+
4
+ ruleTester.run('import-sort', rule, {
5
+ valid: [
6
+ {
7
+ code: `
8
+ import angular from 'angular';
9
+ import 'jquery';
10
+ import React, { useCallback, useState } from 'react';
11
+ import * as Select from 'react-select';
12
+
13
+ import { LabeledValueList as LabeledValueL, SomeThingElse } from '@spinnaker/core';
14
+
15
+ import Bar from './bar';
16
+ import Baz from '../../../test/baz';
17
+
18
+ import 'bootstrap.less';
19
+ import './styles.less';
20
+ `,
21
+ },
22
+ ],
23
+ invalid: [
24
+ {
25
+ code: "import React, {useState, useCallback} from 'react';",
26
+ output: "import React, { useCallback, useState } from 'react';",
27
+ errors: ['Sort the import statements'],
28
+ },
29
+ {
30
+ code: `
31
+ import React, {useState, useCallback} from 'react';
32
+ import angular from 'angular';
33
+ import * as Select from 'react-select';
34
+ `,
35
+ output: `
36
+ import angular from 'angular';
37
+ import React, { useCallback, useState } from 'react';
38
+ import * as Select from 'react-select';
39
+ `,
40
+ errors: ['Sort the import statements'],
41
+ },
42
+ {
43
+ code: `
44
+ import React from 'react';
45
+
46
+ const {useState, useCallback} = React;
47
+
48
+ import { Application } from 'core/application';
49
+ import Bar from "./bar";
50
+ import angular from 'angular';
51
+ // Some comment about react-select
52
+ import * as Select from 'react-select';
53
+ import {
54
+ LabeledValueList as LabeledValueL,
55
+ SomeThingElse,
56
+ } from "@spinnaker/core";
57
+ import './styles.less';
58
+ import Baz from "../../../test/baz";
59
+
60
+ import 'jquery';
61
+ import 'bootstrap.less';
62
+ `,
63
+ // For some strange reason eslint fixer writes additional newline characters
64
+ output: `
65
+ import angular from 'angular';
66
+ import 'jquery';
67
+ import React from 'react';
68
+ // Some comment about react-select
69
+ import * as Select from 'react-select';
70
+
71
+ import { LabeledValueList as LabeledValueL, SomeThingElse } from '@spinnaker/core';
72
+ import { Application } from 'core/application';
73
+
74
+ import Bar from './bar';
75
+ import Baz from '../../../test/baz';
76
+
77
+ import 'bootstrap.less';
78
+ import './styles.less';
79
+
80
+ const {useState, useCallback} = React;
81
+ `,
82
+ errors: ['Sort the import statements'],
83
+ },
84
+ ],
85
+ });
@@ -0,0 +1,281 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier } from 'estree';
3
+ import { getImportName } from '../utils/ast';
4
+
5
+ const CSS_IMPORT = /\.(css|less|scss|sass)$/;
6
+ const MODULE_PATH_REGEX = /[./]*(.*)$/;
7
+ const SPINNAKER_MODULE_PREFIX = /^(core|docker|amazon|titus|google|kubernetes|ecs|huaweicloud|appengine|oracle|cloudfoundry|azure|tencentcloud)\/.*/;
8
+ /**
9
+ * Function supplied to an array's sort method that works on the path and module name skipping ., .. and /
10
+ */
11
+ const relativeModuleSort = (a, b) => {
12
+ const aSource = MODULE_PATH_REGEX.exec(a.source.value)[1];
13
+ const bSource = MODULE_PATH_REGEX.exec(b.source.value)[1];
14
+
15
+ return aSource > bSource ? 1 : aSource === bSource ? 0 : -1;
16
+ };
17
+
18
+ /**
19
+ * Partitions the import specifiers in the given import declaration node based on the import type.
20
+ * i.e ImportDefaultSpecifier, ImportNamespaceSpecifier and ImportSpecifier
21
+ *
22
+ * For example, `import * as React, {useState, useCallback} from 'react';` the following will be returned
23
+ * {
24
+ * namespaceSpecifier: <node for `* as React`> ,
25
+ * importSpecifier: [<node for `useState`>, <node for `useCallback`>]
26
+ * }
27
+ */
28
+ const partitionImportSpecifiers = (importDeclaration: ImportDeclaration) => {
29
+ if (importDeclaration.specifiers.length === 0) {
30
+ return {
31
+ defaultSpecifier: null,
32
+ namespaceSpecifier: null,
33
+ importSpecifiers: null,
34
+ };
35
+ }
36
+
37
+ const defaultSpecifier = importDeclaration.specifiers.find(
38
+ (specifier) => specifier.type === 'ImportDefaultSpecifier',
39
+ ) as ImportDefaultSpecifier;
40
+ const namespaceSpecifier = importDeclaration.specifiers.find(
41
+ (specifier) => specifier.type === 'ImportNamespaceSpecifier',
42
+ ) as ImportNamespaceSpecifier;
43
+ const importSpecifiers = importDeclaration.specifiers.filter(
44
+ (specifier) => specifier.type === 'ImportSpecifier',
45
+ ) as ImportSpecifier[];
46
+
47
+ return { defaultSpecifier, namespaceSpecifier, importSpecifiers };
48
+ };
49
+
50
+ /**
51
+ * Sorts the import specifiers for the given import declaration node in the following order.
52
+ * 1. Default Specifier (i.e `React` in `import React, {useState} from 'react'`)
53
+ * 2. Namespace Specifier (i.e `* as React` in `import * as React, {useState} from 'react'`)
54
+ * 3. Import Specifier (i.e `useState, useCallback` in `import * as React, {useState, useCallback} from 'react'`)
55
+ */
56
+ const sortImportSpecifiers = (importDeclaration: ImportDeclaration) => {
57
+ const { defaultSpecifier, namespaceSpecifier, importSpecifiers } = partitionImportSpecifiers(importDeclaration);
58
+ if (!importSpecifiers) {
59
+ return importDeclaration;
60
+ }
61
+
62
+ importSpecifiers.sort((a, b) => getImportName(a.imported).localeCompare(getImportName(b.imported)));
63
+
64
+ importDeclaration.specifiers = [defaultSpecifier, namespaceSpecifier, ...importSpecifiers].filter(
65
+ (specifier) => !!specifier,
66
+ );
67
+
68
+ return importDeclaration;
69
+ };
70
+
71
+ /**
72
+ * Returns the text representation of the given import specifier node.
73
+ */
74
+ const printImportSpecifier = (importSpecifier) => {
75
+ switch (importSpecifier.type) {
76
+ case 'ImportDefaultSpecifier':
77
+ return importSpecifier.local.name;
78
+ case 'ImportNamespaceSpecifier':
79
+ return `* as ${importSpecifier.local.name}`;
80
+ case 'ImportSpecifier':
81
+ return importSpecifier.local.name !== importSpecifier.imported.name
82
+ ? `${importSpecifier.imported.name} as ${importSpecifier.local.name}`
83
+ : importSpecifier.imported.name;
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Returns the text representation of the given import declaration node.
89
+ *
90
+ * NOTE: The built-in `context.getSourceCode().getText(node)` can also return the text representation of a node, but it
91
+ * preserves the original positions of the import specifiers within the import declaration.
92
+ */
93
+ const printImportDeclaration = (context, importDeclaration) => {
94
+ const source = importDeclaration.source.value;
95
+ const partitionedImportSpecifiers = partitionImportSpecifiers(importDeclaration);
96
+
97
+ const importSpecifiersText = Object.entries(partitionedImportSpecifiers).reduce(
98
+ (importSpecifiersText, [type, importSpecifiers]) => {
99
+ if (importSpecifiers == null || (Array.isArray(importSpecifiers) && importSpecifiers.length === 0)) {
100
+ return importSpecifiersText;
101
+ }
102
+
103
+ importSpecifiersText = importSpecifiersText !== '' ? `${importSpecifiersText}, ` : importSpecifiersText;
104
+
105
+ if (type === 'importSpecifiers') {
106
+ // There could be more than one specifier for type `ImportSpecifier`, so join them together.
107
+ const combinedImportSpecifiersText = (importSpecifiers as ImportSpecifier[])
108
+ .map(printImportSpecifier)
109
+ .join(', ');
110
+ importSpecifiersText = `${importSpecifiersText}{ ${combinedImportSpecifiersText} }`;
111
+ } else {
112
+ // There could only be one import specifier for other types i.e `ImportDefaultSpecifier` and
113
+ // `ImportNamespaceSpecifier`.
114
+ importSpecifiersText = `${importSpecifiersText}${printImportSpecifier(importSpecifiers)}`;
115
+ }
116
+
117
+ return importSpecifiersText;
118
+ },
119
+ '',
120
+ );
121
+
122
+ // Try to preserve the preceding comments for each import declaration.
123
+ const sourceCode = context.getSourceCode();
124
+ const isFirstImport = source === sourceCode.ast.body.filter((s) => s.type === 'ImportDeclaration')[0].source.value;
125
+ let prefix = '';
126
+ if (!isFirstImport) {
127
+ // Don't re-write the preceding comments of the first node since they may not be specific to the import statement.
128
+ const comments = sourceCode
129
+ .getCommentsBefore(importDeclaration)
130
+ .map((comment) => sourceCode.getText(comment))
131
+ .join('\n');
132
+
133
+ prefix = comments ? `${comments}\n` : '';
134
+ }
135
+
136
+ return importSpecifiersText !== ''
137
+ ? `${prefix}import ${importSpecifiersText} from '${source}';`
138
+ : `${prefix}import '${source}';`;
139
+ };
140
+
141
+ /**
142
+ * Returns a custom textual representation of import declarations which will be used to verify if they are already
143
+ * sorted. For example
144
+ *
145
+ * `import React, {useState, useCallback} from 'react';\nimport angular from 'angular';`
146
+ * will be written as
147
+ * `react: React, useState, useCallback\nangular: angular`
148
+ */
149
+ const getText = (importDeclarations) => {
150
+ return importDeclarations.reduce((output, importDeclaration) => {
151
+ const specifiersText = (importDeclaration.specifiers || []).map((s) => s.local.name).join(',');
152
+ return `${output}\n${importDeclaration.source.value}: ${specifiersText}`;
153
+ }, '');
154
+ };
155
+
156
+ /**
157
+ * Returns all non `ImportDeclaration` nodes that appear between the first and last `ImportDeclaration` nodes.
158
+ */
159
+ const getAllNonImportDeclarationNodes = (body) => {
160
+ const importDeclarations = body.filter((node) => node.type === 'ImportDeclaration');
161
+ const startIndex = body.findIndex((node) => node.type === 'ImportDeclaration');
162
+ const lastIndex = body.findIndex((node) => node == importDeclarations[importDeclarations.length - 1]);
163
+
164
+ const nonImportDeclarationNodes = [];
165
+ for (let i = startIndex; i <= lastIndex; i++) {
166
+ if (body[i].type !== 'ImportDeclaration') {
167
+ nonImportDeclarationNodes.push(body[i]);
168
+ }
169
+ }
170
+
171
+ return nonImportDeclarationNodes;
172
+ };
173
+
174
+ /**
175
+ * Ensures the import declarations (along with their import specifiers) are sorted based on the following category
176
+ * and alphabetically within each category.
177
+ * 1. import npm package
178
+ * 2. import @spinnaker package
179
+ * 3. import modules using relative path
180
+ * 4. import css modules
181
+ *
182
+ * NOTE: `ImportDeclaration` refers to the entire import statement. i.e `import foo from './foo';`. `ImportSpecifier`
183
+ * refers to the members that are imported from the module. i.e `foo` in `import foo from './foo';`
184
+ */
185
+ const ruleModule: Rule.RuleModule = {
186
+ create(context) {
187
+ return {
188
+ Program(program) {
189
+ const importDeclarations = program.body.filter(
190
+ (node) => node.type === 'ImportDeclaration',
191
+ ) as ImportDeclaration[];
192
+
193
+ if (!importDeclarations.length) {
194
+ return;
195
+ }
196
+ // Nodes between first and last `ImportDeclarationNodes` that aren't of type `ImportDeclaration`. These nodes
197
+ // must be re-written at the end of the import declarations.
198
+ const nonImportDeclarationNodes = getAllNonImportDeclarationNodes(program.body);
199
+ const start = importDeclarations[0].range[0];
200
+ const end = importDeclarations[importDeclarations.length - 1].range[1];
201
+ const originalTextOfImportDeclarations = getText(importDeclarations);
202
+
203
+ // Partition the import declarations into four groups `package`, `spinnaker`, `relativeModule`, `css` so that
204
+ // import declarations within each partition can be sorted alphabetically and written back in the expected
205
+ // partition order.
206
+ const partitions = importDeclarations.reduce(
207
+ (partitions, declarationNode) => {
208
+ const value = declarationNode.source.value as string;
209
+ if (CSS_IMPORT.test(value)) {
210
+ partitions.css.push(declarationNode);
211
+ } else if (value.startsWith('@spinnaker')) {
212
+ partitions.spinnaker.push(declarationNode);
213
+ } else if (value.startsWith('.')) {
214
+ partitions.relativeModule.push(declarationNode);
215
+ } else if (SPINNAKER_MODULE_PREFIX.test(value)) {
216
+ partitions.spinnaker.push(declarationNode);
217
+ } else {
218
+ partitions.package.push(declarationNode);
219
+ }
220
+ return partitions;
221
+ },
222
+ {
223
+ package: [],
224
+ spinnaker: [],
225
+ relativeModule: [],
226
+ css: [],
227
+ },
228
+ );
229
+
230
+ const sortedImportedDeclarations = Object.values(partitions).map((importDeclarations) =>
231
+ importDeclarations
232
+ // Sort import specifiers within each import declaration
233
+ .map(sortImportSpecifiers)
234
+ // Now sort all import declarations alphabetically within each partition
235
+ .sort(relativeModuleSort),
236
+ );
237
+ const sortedTextOfImportDeclarations = getText(sortedImportedDeclarations.flat());
238
+
239
+ if (originalTextOfImportDeclarations === sortedTextOfImportDeclarations) {
240
+ return;
241
+ }
242
+
243
+ const importDeclarationsText = sortedImportedDeclarations
244
+ .filter((declarationList) => declarationList.length > 0)
245
+ .map((importDeclarations) =>
246
+ // Print the code from sorted import declarations for each partition
247
+ importDeclarations
248
+ .map((importDeclaration) => printImportDeclaration(context, importDeclaration))
249
+ .join('\n'),
250
+ )
251
+ // Combine sorted declarations from each partition
252
+ .join('\n\n');
253
+
254
+ const sourceCode = context.getSourceCode();
255
+ const nonImportDeclarationsText =
256
+ nonImportDeclarationNodes.length > 0
257
+ ? nonImportDeclarationNodes.map((node) => sourceCode.getText(node)).join('\n')
258
+ : null;
259
+
260
+ const fixedText = nonImportDeclarationsText
261
+ ? `${importDeclarationsText}\n\n${nonImportDeclarationsText}`
262
+ : importDeclarationsText;
263
+
264
+ context.report({
265
+ fix: (fixer) => fixer.replaceTextRange([start, end], fixedText),
266
+ message: 'Sort the import statements',
267
+ node: importDeclarations[0],
268
+ });
269
+ },
270
+ };
271
+ },
272
+ meta: {
273
+ fixable: 'code',
274
+ type: 'problem',
275
+ docs: {
276
+ description: 'Sort the import statements',
277
+ },
278
+ },
279
+ };
280
+
281
+ export default ruleModule;
@@ -0,0 +1,78 @@
1
+ import rule from './migrate-to-mock-http-client';
2
+ import ruleTester from '../utils/ruleTester';
3
+
4
+ ruleTester.run('migrate-to-mock-http-client', rule, {
5
+ valid: [
6
+ {
7
+ code: 'it(() => { const http = mockHttpClient(); })',
8
+ },
9
+ ],
10
+ invalid: [
11
+ {
12
+ code: `it('does things', () => { $httpBackend.flush() })`,
13
+ output: `it('does things', async () => { $httpBackend.flush() })`,
14
+ errors: ['Migrate to MockHttpClient (step 1): make test function async'],
15
+ },
16
+
17
+ // Step 1 make async
18
+ {
19
+ code: `
20
+ describe('foo bar', () => {
21
+ it('does things', () => {
22
+ $httpBackend.flush()
23
+ })
24
+ })`,
25
+ output: `
26
+ describe('foo bar', () => {
27
+ it('does things', async () => {
28
+ $httpBackend.flush()
29
+ })
30
+ })`,
31
+ errors: ['Migrate to MockHttpClient (step 1): make test function async'],
32
+ },
33
+
34
+ // Step 2 create mock
35
+ {
36
+ code: `
37
+ describe('foo bar', () => {
38
+ it('does things', async () => {
39
+ $httpBackend.flush()
40
+ })
41
+ })`,
42
+ output: `import { mockHttpClient } from 'core/api/mock/jasmine';
43
+
44
+ describe('foo bar', () => {
45
+ it('does things', async () => {
46
+ const http = mockHttpClient();
47
+ $httpBackend.flush()
48
+ })
49
+ })`,
50
+ errors: ['Migrate to MockHttpClient (step 2): Create a MockHttpClient named "http"'],
51
+ },
52
+
53
+ // Step 3 change variables
54
+ {
55
+ code: `
56
+ import { mockHttpClient } from 'core/api/mock/jasmine';
57
+ describe('foo bar', () => {
58
+ it('does things', async () => {
59
+ const http = mockHttpClient();
60
+ $httpBackend.expectGET('/foo/bar').respond(200, { bar: 15 });
61
+ service.fetchBars();
62
+ $httpBackend.flush()
63
+ })
64
+ })`,
65
+ output: `
66
+ import { mockHttpClient } from 'core/api/mock/jasmine';
67
+ describe('foo bar', () => {
68
+ it('does things', async () => {
69
+ const http = mockHttpClient();
70
+ http.expectGET('/foo/bar').respond(200, { bar: 15 });
71
+ service.fetchBars();
72
+ await http.flush()
73
+ })
74
+ })`,
75
+ errors: ['Migrate to MockHttpClient (step 3): replace $httpBackend with http'],
76
+ },
77
+ ],
78
+ });