@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.
Files changed (61) hide show
  1. package/.eslintignore +3 -0
  2. package/CHANGELOG.md +77 -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 +51 -0
  13. package/rules/api-deprecation.spec.ts +79 -0
  14. package/rules/api-deprecation.ts +254 -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 +23 -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 +106 -0
  27. package/rules/import-relative-within-subpackage.spec.ts +50 -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 +280 -0
  31. package/rules/migrate-to-mock-http-client.spec.ts +78 -0
  32. package/rules/migrate-to-mock-http-client.ts +122 -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/import-aliases.mock.ts +17 -0
  58. package/utils/import-aliases.ts +91 -0
  59. package/utils/mockModule.js +15 -0
  60. package/utils/ruleTester.js +8 -0
  61. package/utils/utils.ts +90 -0
package/create-rule.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ const yargs = require('yargs').usage('$0 <rulename>', 'create a new eslint rule', (_yargs) =>
3
+ _yargs.positional('rulename', { description: 'the name of the rule' }),
4
+ );
5
+
6
+ const { existsSync } = require('fs');
7
+ const { readFile, writeFile, mkdir } = require('fs').promises;
8
+
9
+ const prettier = require('prettier');
10
+ const glob = require('fast-glob');
11
+ const { camelCase } = require('lodash');
12
+
13
+ const { rulename } = yargs.argv;
14
+ const symbol = camelCase(rulename);
15
+
16
+ const load = async (filename) => readFile(filename, 'utf-8');
17
+ const store = async (filename, content) => writeFile(filename, content, 'utf-8');
18
+
19
+ const prepend = async (filename, insertString) => {
20
+ const content = await load(filename);
21
+ return store(filename, `${insertString}${content}`);
22
+ };
23
+
24
+ const insertAfter = async (filename, insertString, matchString) => {
25
+ const content = await load(filename, 'utf-8');
26
+ const matchIdx = content.indexOf(matchString);
27
+ if (matchIdx === -1) {
28
+ throw new Error(`Did not find '${matchString}' in ${filename}`);
29
+ }
30
+ const prefix = content.substr(0, matchIdx + matchString.length);
31
+ const suffix = content.substr(matchIdx + matchString.length);
32
+ return store(filename, `${prefix}${insertString}${suffix}`);
33
+ };
34
+
35
+ const replaceInFile = async (srcFile, destFile, findPattern, replaceString) => {
36
+ const content = await load(srcFile);
37
+ return store(destFile, content.replace(findPattern, replaceString));
38
+ };
39
+
40
+ const formatFiles = async (globs) => {
41
+ const files = await glob(globs);
42
+ for (const file of files) {
43
+ await formatFile(file);
44
+ }
45
+ };
46
+
47
+ const formatFile = async (filepath) => {
48
+ const fileContent = await load(filepath, 'utf-8');
49
+ const prettierConfig = await prettier.resolveConfig(filepath);
50
+ const options = { ...prettierConfig, filepath };
51
+ return store(filepath, prettier.format(fileContent, options));
52
+ };
53
+
54
+ async function createRule() {
55
+ if (!existsSync('rules')) {
56
+ await mkdir('rules');
57
+ }
58
+
59
+ await replaceInFile(`template/template-rule.ts`, `rules/${rulename}.ts`, /RULENAME/g, rulename);
60
+ await replaceInFile(`template/template-rule.spec.ts`, `rules/${rulename}.spec.ts`, /RULENAME/g, rulename);
61
+ await prepend('eslint-plugin.ts', `import ${camelCase(rulename)} from './rules/${rulename}';\n`);
62
+ await insertAfter('eslint-plugin.ts', `\n '${rulename}': ${camelCase(rulename)},`, 'rules: {');
63
+ await insertAfter('base.config.js', `\n '@spinnaker/${rulename}': 2,`, 'rules: {');
64
+ await formatFiles([`rules/${rulename}.**`, 'eslint-plugin.js', 'base.config.js']);
65
+ }
66
+
67
+ createRule();
@@ -0,0 +1,47 @@
1
+ import apiDeprecation from './rules/api-deprecation';
2
+ import apiNoSlashes from './rules/api-no-slashes';
3
+ import apiNoUnusedChaining from './rules/api-no-unused-chaining';
4
+ import importFromAliasNotNpm from './rules/import-from-alias-not-npm';
5
+ import importFromNpmNotAlias from './rules/import-from-npm-not-alias';
6
+ import importFromNpmNotRelative from './rules/import-from-npm-not-relative';
7
+ import importFromPresentationNotCore from './rules/import-from-presentation-not-core';
8
+ import importRelativeWithinSubpackage from './rules/import-relative-within-subpackage';
9
+ import importSort from './rules/import-sort';
10
+ import migrateToMockHttpClient from './rules/migrate-to-mock-http-client';
11
+ import ngNoComponentClass from './rules/ng-no-component-class';
12
+ import ngNoModuleExport from './rules/ng-no-module-export';
13
+ import ngNoRequireAngularJS from './rules/ng-no-require-angularjs';
14
+ import ngNoRequireModuleDeps from './rules/ng-no-require-module-deps';
15
+ import ngStrictDI from './rules/ng-strictdi';
16
+ import preferPromiseLike from './rules/prefer-promise-like';
17
+ import react2angularWithErrorBoundary from './rules/react2angular-with-error-boundary';
18
+ import restPreferStaticStringsInInitializer from './rules/rest-prefer-static-strings-in-initializer';
19
+
20
+ const plugin = {
21
+ configs: {
22
+ base: require('./base.config.js'),
23
+ none: require('./none.config.js'),
24
+ },
25
+ rules: {
26
+ 'api-deprecation': apiDeprecation,
27
+ 'api-no-slashes': apiNoSlashes,
28
+ 'api-no-unused-chaining': apiNoUnusedChaining,
29
+ 'import-from-alias-not-npm': importFromAliasNotNpm,
30
+ 'import-from-npm-not-alias': importFromNpmNotAlias,
31
+ 'import-from-npm-not-relative': importFromNpmNotRelative,
32
+ 'import-from-presentation-not-core': importFromPresentationNotCore,
33
+ 'import-relative-within-subpackage': importRelativeWithinSubpackage,
34
+ 'import-sort': importSort,
35
+ 'migrate-to-mock-http-client': migrateToMockHttpClient,
36
+ 'ng-no-component-class': ngNoComponentClass,
37
+ 'ng-no-module-export': ngNoModuleExport,
38
+ 'ng-no-require-angularjs': ngNoRequireAngularJS,
39
+ 'ng-no-require-module-deps': ngNoRequireModuleDeps,
40
+ 'ng-strictdi': ngStrictDI,
41
+ 'prefer-promise-like': preferPromiseLike,
42
+ 'react2angular-with-error-boundary': react2angularWithErrorBoundary,
43
+ 'rest-prefer-static-strings-in-initializer': restPreferStaticStringsInInitializer,
44
+ },
45
+ };
46
+
47
+ export default plugin;
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // This registers Typescript compiler instance into node.js's require()
2
+ const path = require('path');
3
+ const project = path.resolve(__dirname, 'tsconfig.json');
4
+ require('ts-node').register({ transpileOnly: true, project });
5
+
6
+ module.exports = require('./eslint-plugin.ts').default;
package/newrule.sh ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ if [[ -z $1 ]] ; then
3
+ echo "Enter name of rule, i.e.:"
4
+ echo "$0 lint-rule-name"
5
+ exit 1
6
+ fi
7
+
8
+ RULENAME=$1;
9
+
10
+
11
+ sed -e "s/^ rules:.*/&'@spinnaker\/${RULENAME}': 2,/" -i '' base.config.js
12
+ sed -e "s/^ rules:.*/&'${RULENAME}': require('.\/rules\/${RULENAME}'),/" -i '' eslint-plugin.js
13
+
14
+ npx prettier --write base.config.js eslint-plugin.js
15
+
16
+
17
+
18
+ # Write RULENAME.js
19
+ (
20
+ cat <<EndOfRuleFile
21
+ 'use strict';
22
+ // @ts-check
23
+
24
+ /**
25
+ * Import AST Types from 'estree'
26
+ * @typedef {import('estree').CallExpression} CallExpression
27
+ * @typedef {import('estree').ImportSpecifier} ImportSpecifier
28
+ */
29
+
30
+ const _ = require('lodash/fp');
31
+
32
+ const { getProgram } = require('../utils/utils');
33
+
34
+ /** @type {RuleModule} */
35
+ module.exports = {
36
+ create(context) {
37
+ return {
38
+ /** @param node {ImportSpecifier} */
39
+ ImportSpecifier(node) {
40
+ /** @type {ImportSpecifier[]} */
41
+ if (node.local && node.local.name === 'API') {
42
+ context.report({
43
+ node,
44
+ message: 'Do not import API',
45
+ fix: (fixer) => fixer.remove(node),
46
+ });
47
+ }
48
+ },
49
+ };
50
+ },
51
+ meta: {
52
+ fixable: 'code',
53
+ type: 'problem',
54
+ docs: {
55
+ description: 'Do not import API',
56
+ },
57
+ },
58
+ };
59
+ EndOfRuleFile
60
+ ) > rules/${RULENAME}.js
61
+
62
+
63
+ # Write RULENAME.spec.js
64
+ (
65
+ cat <<EndOfSpecFile
66
+ 'use strict';
67
+
68
+ const ruleTester = require('../utils/ruleTester');
69
+ const rule = require('../rules/${RULENAME}');
70
+
71
+ ruleTester.run('${RULENAME}', rule, {
72
+ valid: [
73
+ {
74
+ code: "import { Something } from 'somewhere';",
75
+ },
76
+ ],
77
+ invalid: [
78
+ {
79
+ code: "import { API } from '@spinnaker/core';",
80
+ output: "import { } from '@spinnaker/core';",
81
+ errors: ["Do not import API"],
82
+ },
83
+ ],
84
+ });
85
+
86
+ EndOfSpecFile
87
+ ) > test/${RULENAME}.spec.js
88
+
package/none.config.js ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ parserOptions: { sourceType: 'module' },
4
+ plugins: ['@typescript-eslint', '@spinnaker/eslint-plugin'],
5
+ extends: [],
6
+ rules: {},
7
+ env: {
8
+ browser: true,
9
+ node: true,
10
+ es6: true,
11
+ jasmine: true,
12
+ },
13
+ globals: {
14
+ angular: true,
15
+ $: true,
16
+ _: true,
17
+ },
18
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@spinnaker/eslint-plugin",
3
+ "version": "3.0.0",
4
+ "main": "index.js",
5
+ "license": "Apache-2.0",
6
+ "scripts": {
7
+ "create-rule": "create-rule.js",
8
+ "dev": "npm run test:debug",
9
+ "test": "jest",
10
+ "test:debug": "node --inspect ./node_modules/.bin/jest --runInBand --watch"
11
+ },
12
+ "dependencies": {
13
+ "lodash": "^4.17.20",
14
+ "ts-node": "*"
15
+ },
16
+ "devDependencies": {
17
+ "@babel/preset-typescript": "^7.15.0",
18
+ "@types/eslint": "^7.28.0",
19
+ "@types/estree": "*",
20
+ "@types/jest": "^26.0.24",
21
+ "@types/lodash": "^4.14.165",
22
+ "@types/node": "^16.4.13",
23
+ "@typescript-eslint/parser": "^4.29.1",
24
+ "@typescript-eslint/types": "^4.29.1",
25
+ "eslint": "^7.32.0",
26
+ "fast-glob": "^3.2.7",
27
+ "jest": "^27.0.6",
28
+ "prettier": "*",
29
+ "typescript": "^4.3.5"
30
+ },
31
+ "peerDependencies": {
32
+ "@typescript-eslint/eslint-plugin": "4.4.0",
33
+ "@typescript-eslint/parser": "4.4.0",
34
+ "eslint": "7.10.0",
35
+ "eslint-config-prettier": "6.12.0",
36
+ "eslint-plugin-react-hooks": "4.1.2"
37
+ },
38
+ "peerDevDependencies": [
39
+ "@typescript-eslint/eslint-plugin",
40
+ "@typescript-eslint/parser",
41
+ "eslint",
42
+ "eslint-config-prettier",
43
+ "eslint-plugin-react-hooks"
44
+ ],
45
+ "jest": {
46
+ "testPathIgnorePatterns": [
47
+ "<rootDir>/template"
48
+ ]
49
+ },
50
+ "gitHead": "2ddc8bd4f04d13a8712702cf75fca3cada520406"
51
+ }
@@ -0,0 +1,79 @@
1
+ import ruleTester from '../utils/ruleTester';
2
+ import rule from './api-deprecation';
3
+
4
+ ruleTester.run('api-deprecation', rule, {
5
+ valid: [
6
+ {
7
+ code: `REST('/path/to/endpoint').path(id).get();`,
8
+ },
9
+ ],
10
+
11
+ invalid: [
12
+ // Simple renames: .one(), .all(), .getList(), .withParams(), .remove()
13
+ {
14
+ code: `API.one('foo');`,
15
+ output: `API.path('foo');`,
16
+ errors: ['API.one() is deprecated. Migrate from one() to path()'],
17
+ },
18
+ {
19
+ code: `API.all('foo');`,
20
+ output: `API.path('foo');`,
21
+ errors: ['API.all() is deprecated. Migrate from all() to path()'],
22
+ },
23
+ {
24
+ code: `API.path('foo').getList();`,
25
+ output: `API.path('foo').get();`,
26
+ errors: ['API.getList() is deprecated. Migrate from getList() to get()'],
27
+ },
28
+ {
29
+ code: `API.withParams('foo');`,
30
+ output: `API.query('foo');`,
31
+ errors: ['API.withParams() is deprecated. Migrate from withParams() to query()'],
32
+ },
33
+ {
34
+ code: `API.remove('foo');`,
35
+ output: `API.delete('foo');`,
36
+ errors: ['API.remove() is deprecated. Migrate from remove() to delete()'],
37
+ },
38
+ // .data(obj).post() -> .post(obj)
39
+ {
40
+ code: `API.path('foo').data({ key: 'val' }).post();`,
41
+ output: `API.path('foo').post({ key: 'val' });`,
42
+ errors: ['API.data() is deprecated. Migrate from .data({}) to .put({}) or .post({})'],
43
+ },
44
+ // .data(obj).put() -> .put(obj)
45
+ {
46
+ code: `API.path('foo').data({ key: 'val' }).put();`,
47
+ output: `API.path('foo').put({ key: 'val' });`,
48
+ errors: ['API.data() is deprecated. Migrate from .data({}) to .put({}) or .post({})'],
49
+ },
50
+ // .get() doesn't have queryparams args
51
+ {
52
+ code: `API.path('foo').get(queryParams);`,
53
+ output: `API.path('foo').query(queryParams).get();`,
54
+ errors: [
55
+ 'Passing parameters to API.get() is deprecated. Migrate from .get(queryparams) to .query(queryparams).get()',
56
+ ],
57
+ },
58
+ // .delete() doesn't have queryparams args
59
+ {
60
+ code: `API.path('foo').delete(queryParams);`,
61
+ output: `API.path('foo').query(queryParams).delete();`,
62
+ errors: [
63
+ 'Passing parameters to API.delete() is deprecated. Migrate from .delete(queryparams) to .query(queryparams).delete()',
64
+ ],
65
+ },
66
+ // .delete() doesn't have queryparams args
67
+ {
68
+ code: `API.path('foo').path(id);`,
69
+ output: `API.path('foo', id);`,
70
+ errors: ["Prefer API.path('foo', 'bar') over API.path('foo').path('bar')"],
71
+ },
72
+ // Migrate from API to REST()
73
+ {
74
+ code: `API.path('foo', id).withCache().get()`,
75
+ output: `REST().path('foo', id).withCache().get()`,
76
+ errors: ['API is deprecated, switch to REST()'],
77
+ },
78
+ ],
79
+ });
@@ -0,0 +1,254 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { CallExpression, ImportDeclaration, ImportSpecifier, MemberExpression, Node } from 'estree';
3
+ import * as _ from 'lodash/fp';
4
+
5
+ import {
6
+ getCallChain,
7
+ getCallingIdentifier,
8
+ getProgram,
9
+ getVariableInitializer,
10
+ getVariableInScope,
11
+ } from '../utils/utils';
12
+
13
+ function isAPICall(context: Rule.RuleContext, node: CallExpression) {
14
+ // Find the call chain Identifier:
15
+ // API.one().all().get()
16
+ // ^^^
17
+ const callingIdentifier = getCallingIdentifier(node);
18
+ if (callingIdentifier && callingIdentifier.name === 'API') {
19
+ return true;
20
+ }
21
+
22
+ // If the calling identifier is a variable reference ...
23
+ // var foo = API.one(); foo.one().all().get();
24
+ // ^^^
25
+ // then find the variable initializer: var foo = API.one();
26
+ // ^^^^^^^^^
27
+ const variable = getVariableInScope(context, callingIdentifier);
28
+ const initializer = getVariableInitializer(variable);
29
+ const initializerIdentifier = getCallingIdentifier(initializer);
30
+
31
+ return initializerIdentifier && initializerIdentifier.name === 'API';
32
+ }
33
+
34
+ const getCallName = _.get('callee.property.name');
35
+
36
+ function isCallNamed(...callNames: string[]) {
37
+ return function (callExpression: CallExpression) {
38
+ const callName = getCallName(callExpression);
39
+ return callNames.includes(callName);
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Returns a list of deprecated methods that need to be renamed and fixers to do so.
45
+ */
46
+ function findMethodsToRename(callChain: CallExpression[]) {
47
+ const renames = {
48
+ getList: 'get',
49
+ withParams: 'query',
50
+ one: 'path',
51
+ all: 'path',
52
+ remove: 'delete',
53
+ };
54
+
55
+ const calls = callChain.map((call) => {
56
+ const from = getCallName(call) as keyof typeof renames;
57
+ const to = renames[from];
58
+ const fix = (fixer: Rule.RuleFixer) => fixer.replaceText((call.callee as MemberExpression).property, to);
59
+ return { call, fix, from, to };
60
+ });
61
+
62
+ // Only return calls that have a 'to' mapping in the renames object
63
+ return calls.filter((tuple) => !!tuple.to);
64
+ }
65
+
66
+ interface IRename {
67
+ from: string;
68
+ to: string;
69
+ fix: (fixer: Rule.RuleFixer) => Rule.Fix;
70
+ }
71
+
72
+ function reportSimpleRenames(context: Rule.RuleContext, node: Node, renames: IRename[]) {
73
+ // Strings for the message
74
+ const froms = [...new Set(renames.map((tuple) => tuple.from))].join('/');
75
+ const tos = [...new Set(renames.map((tuple) => tuple.to))].join('/');
76
+
77
+ return context.report({
78
+ node,
79
+ message: `API.${froms}() is deprecated. Migrate from ${froms}() to ${tos}()`,
80
+ fix: (fixer) => renames.map((tuple) => tuple.fix(fixer)),
81
+ });
82
+ }
83
+
84
+ function reportDataMethod(context: Rule.RuleContext, node: Node, callChain: CallExpression[]) {
85
+ // Just the .data() calls
86
+ const dataCalls = callChain.filter(isCallNamed('data'));
87
+
88
+ // Find the corresponding put/post
89
+ const putOrPost = callChain.find((n) => ['put', 'post'].includes((n.callee as any).property.name));
90
+ const message = `API.data() is deprecated. Migrate from .data({}) to .put({}) or .post({})`;
91
+
92
+ // If there is a single .data() and a .put() or .post() in the chain...
93
+ if (dataCalls.length !== 1 || !putOrPost) {
94
+ // Can't find a single .data({}) and .post()/.put() to auto-fix, so just report the problem to the user
95
+ return context.report({ node, message });
96
+ }
97
+
98
+ const call = dataCalls[0];
99
+ // get the text of the arguments passed to .data(ARGS)
100
+ const argsText = call.arguments.map((arg) => context.getSourceCode().getText(arg)).join(', ');
101
+ // @ts-ignore find the spot between the parentheses in -> post()
102
+ const putOrPostRangeEnd = putOrPost.callee.property.range[1] + 1;
103
+ // @ts-ignore Just after ".one()" in `.one().data(value)`
104
+ const previousCalleeRangeEnd = call.callee.object.range[1];
105
+ // The end of `.data(value)`
106
+ const dataRangeEnd = call.range[1];
107
+ return context.report({
108
+ node,
109
+ message,
110
+ fix: (fixer) => [
111
+ // Move "value" text from .data(value) into the put/post args, i.e.: .put(value)
112
+ fixer.replaceTextRange([putOrPostRangeEnd, putOrPostRangeEnd], argsText),
113
+ // Remove .data(value) entirely
114
+ fixer.replaceTextRange([previousCalleeRangeEnd, dataRangeEnd], ''),
115
+ ],
116
+ });
117
+ }
118
+
119
+ function reportGetsAndDeletesWithArgs(context: Rule.RuleContext, node: Node, getsAndDeletes: CallExpression[]) {
120
+ const callNames = [...new Set(getsAndDeletes.map(getCallName))].join('/');
121
+ const message = `Passing parameters to API.${callNames}() is deprecated. Migrate from .${callNames}(queryparams) to .query(queryparams).${callNames}()`;
122
+
123
+ // Should be only one get/delete, but just in case, report without fixing:
124
+ if (getsAndDeletes.length > 1) {
125
+ return context.report({ node, message });
126
+ }
127
+
128
+ const call = getsAndDeletes[0];
129
+ const type = getCallName(call);
130
+ const argsText = call.arguments.map((arg) => context.getSourceCode().getText(arg)).join(', ');
131
+ const getCallStart = (call.callee as MemberExpression).property.range[0];
132
+ const getCallEnd = call.range[1];
133
+ const fix = (fixer: Rule.RuleFixer) =>
134
+ fixer.replaceTextRange([getCallStart, getCallEnd], `query(${argsText}).${type}()`);
135
+
136
+ return context.report({ node, message, fix });
137
+ }
138
+
139
+ function reportChainedPathAsVarargs(callChain: CallExpression[], context: Rule.RuleContext, node: Node) {
140
+ const message = `Prefer API.path('foo', 'bar') over API.path('foo').path('bar')`;
141
+
142
+ const fix = (fixer: Rule.RuleFixer) => {
143
+ const [firstPathCall, secondPathCall] = callChain;
144
+
145
+ const firstPathLastArg = firstPathCall['arguments'].slice().pop();
146
+ const secondPathStart = firstPathCall.range[1];
147
+ const secondPathEnd = secondPathCall.range[1];
148
+ const secondPathArgsText = secondPathCall['arguments']
149
+ .map((arg) => context.getSourceCode().getText(arg))
150
+ .join(', ');
151
+
152
+ return [
153
+ // Move the second .path(...) call's args to first .path() args list
154
+ fixer.insertTextAfter(firstPathLastArg, `, ${secondPathArgsText}`),
155
+ // Remove second .path()
156
+ fixer.removeRange([secondPathStart, secondPathEnd]),
157
+ ];
158
+ };
159
+
160
+ return context.report({ message, node, fix });
161
+ }
162
+
163
+ function reportAPIDeprecatedUseREST(node: Node, context: Rule.RuleContext, callChain: CallExpression[]) {
164
+ // Everything else is migrated, now migrate from API.path() to REST().path()
165
+ const message = 'API is deprecated, switch to REST()';
166
+ const API = (callChain[0].callee as MemberExpression).object;
167
+ const program = getProgram(node);
168
+ const allImports = program.body.filter((item) => item.type === 'ImportDeclaration') as ImportDeclaration[];
169
+ const importSpecifiers = allImports
170
+ .map((decl) => decl.specifiers)
171
+ .reduce((acc, x) => acc.concat(x), []) as ImportSpecifier[]; //flatten
172
+
173
+ const apiImport = importSpecifiers.find((specifier) => {
174
+ return specifier.imported && specifier.imported?.name === 'API';
175
+ });
176
+
177
+ return context.report({
178
+ message,
179
+ node,
180
+ fix: (fixer) => {
181
+ if (!apiImport) {
182
+ // Replace API with REST()
183
+ return fixer.replaceText(API, 'REST()');
184
+ }
185
+
186
+ return [
187
+ // Replace API with REST()
188
+ fixer.replaceText(API, 'REST()'),
189
+ fixer.replaceText(apiImport, 'REST'),
190
+ ];
191
+ },
192
+ });
193
+ }
194
+
195
+ const rule: Rule.RuleModule = {
196
+ create(context) {
197
+ return {
198
+ /**
199
+ * Look for chains of CallExpressions that are:
200
+ * - part of an API.xyz() call, e.g.: return API.xyz().get()
201
+ * - part of an xyz() call chained off a variable, e.g.: var foo = API.xyz(); foo.get()
202
+ * @param node {CallExpression}
203
+ */
204
+ CallExpression(node) {
205
+ if (node.parent.type === 'MemberExpression' || !isAPICall(context, node)) {
206
+ return undefined;
207
+ }
208
+
209
+ // an array of CallExpressions, i.e. for API.one().all().get() -> [.one, .all, .get]
210
+ const callChain = getCallChain(node);
211
+
212
+ // Migrate the simple method renames, i.e.: API.one() -> API.path()
213
+ const renames = findMethodsToRename(callChain);
214
+ if (renames.length) {
215
+ return reportSimpleRenames(context, node, renames);
216
+ }
217
+
218
+ // Migrate .data(postdata).post() -> .post(postdata)
219
+ // Migrate .data(putdata).put() -> .put(putdata)
220
+ if (callChain.some(isCallNamed('data'))) {
221
+ return reportDataMethod(context, node, callChain);
222
+ }
223
+
224
+ // Migrate .get(params) -> .query(params).get()
225
+ // Migrate .delete(params) -> .query(params).delete()
226
+ const getsAndDeletes = callChain.filter(isCallNamed('get', 'delete'));
227
+ if (getsAndDeletes.some((x) => x.arguments.length > 0)) {
228
+ return reportGetsAndDeletesWithArgs(context, node, getsAndDeletes);
229
+ }
230
+
231
+ // Migrate .path('foo').path('bar') -> .path('foo', 'bar')
232
+ const hasTwoChainedPathCalls = getCallName(callChain[0]) === 'path' && getCallName(callChain[1]) === 'path';
233
+ if (hasTwoChainedPathCalls) {
234
+ return reportChainedPathAsVarargs(callChain, context, node);
235
+ }
236
+
237
+ // Migrate from API.xyz() to REST().xyz()
238
+ const callingIdentifier = getCallingIdentifier(node);
239
+ if (callingIdentifier && callingIdentifier.name === 'API') {
240
+ return reportAPIDeprecatedUseREST(node, context, callChain);
241
+ }
242
+ },
243
+ };
244
+ },
245
+ meta: {
246
+ fixable: 'code',
247
+ type: 'problem',
248
+ docs: {
249
+ description: 'Migrate from API.xyz() to REST(path)',
250
+ },
251
+ },
252
+ };
253
+
254
+ export default rule;