@checkdigit/eslint-plugin 6.6.0-PR.75-0dbb → 6.6.0-PR.75-3712
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/dist-cjs/index.cjs +9929 -394
- package/dist-cjs/metafile.json +4715 -10
- package/dist-mjs/ast/format.mjs +2 -1
- package/dist-mjs/ast/ts-tree.mjs +65 -0
- package/dist-mjs/fixture/no-service-wrapper.mjs +154 -0
- package/dist-mjs/index.mjs +6 -3
- package/dist-types/ast/format.d.ts +2 -1
- package/dist-types/ast/ts-tree.d.ts +8 -0
- package/dist-types/fixture/no-service-wrapper.d.ts +4 -0
- package/dist-types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/ast/format.ts +2 -1
- package/src/ast/ts-tree.ts +90 -0
- package/src/fixture/no-service-wrapper.ts +223 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// fixture/no-service-wrapper.ts
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2021-2024 Check Digit, LLC
|
|
5
|
+
*
|
|
6
|
+
* This code is licensed under the MIT license (see LICENSE.txt for details).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
|
10
|
+
import { type Scope } from '@typescript-eslint/scope-manager';
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import getDocumentationUrl from '../get-documentation-url';
|
|
13
|
+
import { getEnclosingScopeNode } from '../ast/ts-tree';
|
|
14
|
+
import { getIndentation } from '../ast/format';
|
|
15
|
+
|
|
16
|
+
export const ruleId = 'no-service-wrapper';
|
|
17
|
+
|
|
18
|
+
// interface ServiceCallInformation {
|
|
19
|
+
// rootNode:
|
|
20
|
+
// | TSESTree.AwaitExpression
|
|
21
|
+
// | TSESTree.ReturnStatement
|
|
22
|
+
// | TSESTree.VariableDeclaration
|
|
23
|
+
// | TSESTree.CallExpression;
|
|
24
|
+
// fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression;
|
|
25
|
+
// variableDeclaration?: TSESTree.VariableDeclaration;
|
|
26
|
+
// requestBody?: TSESTree.Expression;
|
|
27
|
+
// requestHeaders?: TSESTree.Expression;
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
|
|
31
|
+
|
|
32
|
+
const rule = createRule({
|
|
33
|
+
name: ruleId,
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'suggestion',
|
|
36
|
+
docs: {
|
|
37
|
+
description: 'Prefer native fetch over customized service wrapper.',
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
preferNativeFetch: 'Prefer native fetch over customized service wrapper.',
|
|
41
|
+
invalidOptions:
|
|
42
|
+
'"options" argument should be provided with "resolveWithFullResponse" property set as "true". Otherwise, it indicates that the response body will be obtained without status code assertion which could result in unexpected issue. Please manually convert the usage of customized service wrapper call to native fetch.',
|
|
43
|
+
// unknownError:
|
|
44
|
+
// 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.',
|
|
45
|
+
},
|
|
46
|
+
fixable: 'code',
|
|
47
|
+
schema: [],
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [],
|
|
50
|
+
// eslint-disable-next-line max-lines-per-function
|
|
51
|
+
create(context) {
|
|
52
|
+
const sourceCode = context.sourceCode;
|
|
53
|
+
const scopeManager = sourceCode.scopeManager;
|
|
54
|
+
const parserService = ESLintUtils.getParserServices(context);
|
|
55
|
+
const typeChecker = parserService.program.getTypeChecker();
|
|
56
|
+
|
|
57
|
+
// function reportUnknownError(node: TSESTree.Node, error: string) {
|
|
58
|
+
// context.report({
|
|
59
|
+
// node,
|
|
60
|
+
// messageId: 'unknownError',
|
|
61
|
+
// data: { error, fileName: context.filename },
|
|
62
|
+
// });
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
function isUrlArgumentTemplateLiteral(urlArgument: TSESTree.Node | undefined, scope: Scope) {
|
|
66
|
+
return (
|
|
67
|
+
urlArgument?.type === AST_NODE_TYPES.TemplateLiteral ||
|
|
68
|
+
(urlArgument?.type === AST_NODE_TYPES.Identifier &&
|
|
69
|
+
scope.variables.some((variable) => variable.name === urlArgument.name))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getType(identifier: TSESTree.Identifier) {
|
|
74
|
+
const variable = parserService.esTreeNodeToTSNodeMap.get(identifier);
|
|
75
|
+
const variableType = typeChecker.getTypeAtLocation(variable);
|
|
76
|
+
return typeChecker.typeToString(variableType);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isServiceLikeName(name: string) {
|
|
80
|
+
return /.*[Ss]ervice$/u.test(name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isCalleeServiceWrapper(serviceCall: TSESTree.CallExpression) {
|
|
84
|
+
const callee = serviceCall.callee;
|
|
85
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const endpoint = callee.object;
|
|
90
|
+
if (endpoint.type === AST_NODE_TYPES.Identifier) {
|
|
91
|
+
return getType(endpoint) === 'Endpoint' || isServiceLikeName(endpoint.name);
|
|
92
|
+
}
|
|
93
|
+
if (endpoint.type !== AST_NODE_TYPES.CallExpression) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [contextArgument] = endpoint.arguments;
|
|
98
|
+
if (contextArgument?.type !== AST_NODE_TYPES.Identifier) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (contextArgument.name !== 'EMPTY_CONTEXT' && getType(contextArgument) !== 'InboundContext') {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const service = endpoint.callee;
|
|
105
|
+
if (service.type === AST_NODE_TYPES.Identifier) {
|
|
106
|
+
return getType(service) === 'ResolvedService';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (service.type !== AST_NODE_TYPES.MemberExpression) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const services = service.object;
|
|
113
|
+
if (services.type === AST_NODE_TYPES.Identifier) {
|
|
114
|
+
return getType(services) === 'ResolvedServices';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (services.type !== AST_NODE_TYPES.MemberExpression) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const configuration = services.object;
|
|
121
|
+
if (configuration.type === AST_NODE_TYPES.Identifier) {
|
|
122
|
+
return getType(configuration) === 'Configuration';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// following applies only to test code (fixture)
|
|
126
|
+
if (configuration.type !== AST_NODE_TYPES.MemberExpression) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const fixture = configuration.object;
|
|
130
|
+
if (fixture.type === AST_NODE_TYPES.Identifier) {
|
|
131
|
+
return fixture.name === 'fixture' || getType(fixture) === 'Fixture';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
'CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]': (
|
|
139
|
+
serviceCall: TSESTree.CallExpression,
|
|
140
|
+
) => {
|
|
141
|
+
const enclosingScopeNode = getEnclosingScopeNode(serviceCall);
|
|
142
|
+
assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined');
|
|
143
|
+
const scope = scopeManager?.acquire(enclosingScopeNode);
|
|
144
|
+
assert.ok(scope, 'scope is undefined');
|
|
145
|
+
|
|
146
|
+
const urlArgument = serviceCall.arguments[0];
|
|
147
|
+
|
|
148
|
+
if (!isUrlArgumentTemplateLiteral(urlArgument, scope)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!isCalleeServiceWrapper(serviceCall)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression);
|
|
157
|
+
assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier);
|
|
158
|
+
|
|
159
|
+
// method
|
|
160
|
+
const method = serviceCall.callee.property.name;
|
|
161
|
+
|
|
162
|
+
// body
|
|
163
|
+
const requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined;
|
|
164
|
+
|
|
165
|
+
// options
|
|
166
|
+
const optionsArgument = ['get', 'head', 'del'].includes(method)
|
|
167
|
+
? serviceCall.arguments[1]
|
|
168
|
+
: serviceCall.arguments[2];
|
|
169
|
+
if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
170
|
+
context.report({
|
|
171
|
+
node: serviceCall,
|
|
172
|
+
messageId: 'invalidOptions',
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const resolveWithFullResponseProperty = optionsArgument.properties.find(
|
|
177
|
+
(property) =>
|
|
178
|
+
property.type === AST_NODE_TYPES.Property &&
|
|
179
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
180
|
+
property.key.name === 'resolveWithFullResponse',
|
|
181
|
+
);
|
|
182
|
+
if (
|
|
183
|
+
resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property ||
|
|
184
|
+
resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal ||
|
|
185
|
+
resolveWithFullResponseProperty.value.value !== true
|
|
186
|
+
) {
|
|
187
|
+
context.report({
|
|
188
|
+
node: optionsArgument,
|
|
189
|
+
messageId: 'invalidOptions',
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// headers
|
|
195
|
+
const requestHeadersProperty = optionsArgument.properties.find(
|
|
196
|
+
(property) =>
|
|
197
|
+
property.type === AST_NODE_TYPES.Property &&
|
|
198
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
199
|
+
property.key.name === 'headers',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
context.report({
|
|
203
|
+
messageId: 'preferNativeFetch',
|
|
204
|
+
node: serviceCall,
|
|
205
|
+
fix(fixer) {
|
|
206
|
+
const url = sourceCode.getText(urlArgument);
|
|
207
|
+
const indentation = getIndentation(serviceCall, sourceCode);
|
|
208
|
+
const fetchText = [
|
|
209
|
+
`fetch(${url}, {`,
|
|
210
|
+
` method: '${method.toUpperCase()}',`,
|
|
211
|
+
...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []),
|
|
212
|
+
...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []),
|
|
213
|
+
'})',
|
|
214
|
+
].join(`\n${indentation}`);
|
|
215
|
+
return fixer.replaceText(serviceCall, fetchText);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export default rule;
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then';
|
|
|
11
11
|
import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify';
|
|
12
12
|
import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture';
|
|
13
13
|
import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method';
|
|
14
|
+
import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './fixture/no-service-wrapper';
|
|
14
15
|
import filePathComment from './file-path-comment';
|
|
15
16
|
import noCardNumbers from './no-card-numbers';
|
|
16
17
|
import noTestImport from './no-test-import';
|
|
@@ -37,6 +38,7 @@ export default {
|
|
|
37
38
|
[noFixtureRuleId]: noFixture,
|
|
38
39
|
[fetchHeaderGetterRuleId]: fetchHeaderGetter,
|
|
39
40
|
[fetchThenRuleId]: fetchThen,
|
|
41
|
+
[noServiceWrapperRuleId]: noServiceWrapper,
|
|
40
42
|
},
|
|
41
43
|
configs: {
|
|
42
44
|
all: {
|
|
@@ -55,6 +57,7 @@ export default {
|
|
|
55
57
|
[`@checkdigit/${noFixtureRuleId}`]: 'error',
|
|
56
58
|
[`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error',
|
|
57
59
|
[`@checkdigit/${fetchThenRuleId}`]: 'error',
|
|
60
|
+
[`@checkdigit/${noServiceWrapperRuleId}`]: 'error',
|
|
58
61
|
},
|
|
59
62
|
},
|
|
60
63
|
recommended: {
|