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