@checkdigit/eslint-plugin 7.3.0-PR.75-1f63 → 7.3.0-PR.75-743b
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-mjs/agent/agent-test-wiring.mjs +70 -45
- package/dist-mjs/agent/fetch-response-body-json.mjs +20 -8
- package/dist-mjs/agent/fetch-then.mjs +5 -3
- package/dist-mjs/agent/fetch.mjs +5 -1
- package/dist-mjs/agent/fix-function-call-arguments.mjs +19 -16
- package/dist-mjs/agent/no-fixture.mjs +22 -8
- package/dist-mjs/agent/response-reference.mjs +4 -1
- package/dist-types/agent/fetch-response-body-json.d.ts +1 -1
- package/dist-types/agent/fetch.d.ts +1 -0
- package/dist-types/agent/response-reference.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent/agent-test-wiring.ts +80 -53
- package/src/agent/fetch-response-body-json.ts +30 -11
- package/src/agent/fetch-then.ts +4 -2
- package/src/agent/fetch.ts +4 -0
- package/src/agent/fix-function-call-arguments.ts +31 -19
- package/src/agent/no-fixture.ts +34 -9
- package/src/agent/response-reference.ts +10 -3
- package/src/require-resolve-full-response.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
2
|
export declare const ruleId = "fetch-response-body-json";
|
|
3
|
-
declare const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'>;
|
|
3
|
+
declare const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'refactorNeeded'>;
|
|
4
4
|
export default rule;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Node } from 'estree';
|
|
2
2
|
export declare function getResponseBodyRetrievalText(responseVariableName: string): string;
|
|
3
|
+
export declare function getResponseHeadersRetrievalText(responseVariableName: string): string;
|
|
3
4
|
export declare function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean;
|
|
4
5
|
export declare function hasAssertions(fixtureCall: Node): boolean;
|
|
@@ -11,6 +11,6 @@ export declare function analyzeResponseReferences(variableDeclaration: VariableD
|
|
|
11
11
|
headersReferences: MemberExpression[];
|
|
12
12
|
statusReferences: MemberExpression[];
|
|
13
13
|
destructuringBodyVariable?: Scope.Variable | ObjectPattern;
|
|
14
|
-
destructuringHeadersVariable?: Scope.Variable;
|
|
14
|
+
destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
|
|
15
15
|
destructuringHeadersReferences?: MemberExpression[] | undefined;
|
|
16
16
|
};
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@checkdigit/eslint-plugin","version":"7.3.0-PR.75-
|
|
1
|
+
{"name":"@checkdigit/eslint-plugin","version":"7.3.0-PR.75-743b","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 .","lint:fix":"eslint --max-warnings 0 --fix .","prepare":"","prepublishOnly":"npm run build:dist-types && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"dependencies":{"@typescript-eslint/type-utils":"8.14.0","@typescript-eslint/utils":"8.14.0","debug":"^4.3.7","ts-api-utils":"^1.4.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.1","@checkdigit/typescript-config":"^8.0.0","@eslint/js":"^9.15.0","@types/debug":"^4.1.12","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@typescript-eslint/parser":"^8.14.0","@typescript-eslint/rule-tester":"^8.14.0","eslint":"^9.15.0","eslint-config-prettier":"^9.1.0","eslint-import-resolver-typescript":"^3.6.3","eslint-plugin-eslint-plugin":"^6.3.2","eslint-plugin-import":"^2.31.0","eslint-plugin-no-only-tests":"^3.3.0","eslint-plugin-no-secrets":"^1.1.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"1.0.4","http-status-codes":"^2.3.0","rimraf":"^6.0.1","typescript-eslint":"^8.14.0"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=20.17"}}
|
|
@@ -41,12 +41,14 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
41
41
|
schema: [],
|
|
42
42
|
},
|
|
43
43
|
defaultOptions: [],
|
|
44
|
+
// eslint-disable-next-line max-lines-per-function
|
|
44
45
|
create(context) {
|
|
45
46
|
log('Processing file:', context.filename);
|
|
46
47
|
const sourceCode = context.sourceCode;
|
|
47
48
|
const importDeclarations = new Map<string, TSESTree.ImportDeclaration>();
|
|
48
49
|
let isFixtureUsed = false;
|
|
49
50
|
let beforeAll: TSESTree.CallExpression | undefined;
|
|
51
|
+
let beforeEach: TSESTree.CallExpression | undefined;
|
|
50
52
|
let afterAll: TSESTree.CallExpression | undefined;
|
|
51
53
|
|
|
52
54
|
return {
|
|
@@ -68,11 +70,16 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
68
70
|
'CallExpression[callee.name="beforeAll"]': (callExpression: TSESTree.CallExpression) => {
|
|
69
71
|
beforeAll = callExpression;
|
|
70
72
|
},
|
|
73
|
+
'CallExpression[callee.name="beforeEach"]': (callExpression: TSESTree.CallExpression) => {
|
|
74
|
+
beforeEach = callExpression;
|
|
75
|
+
},
|
|
71
76
|
'CallExpression[callee.name="afterAll"]': (callExpression: TSESTree.CallExpression) => {
|
|
72
77
|
afterAll = callExpression;
|
|
73
78
|
},
|
|
79
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
74
80
|
'Program:exit'(program) {
|
|
75
|
-
if (!isFixtureUsed || beforeAll === undefined) {
|
|
81
|
+
if (!isFixtureUsed || (beforeAll === undefined && beforeEach === undefined)) {
|
|
82
|
+
// only update test wiring if fixture is used
|
|
76
83
|
return;
|
|
77
84
|
}
|
|
78
85
|
|
|
@@ -81,7 +88,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
81
88
|
let agentImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
|
|
82
89
|
let fixturePluginImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
|
|
83
90
|
let agentDeclarationFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
|
|
84
|
-
let
|
|
91
|
+
let beforeAllOrEachFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
|
|
85
92
|
let afterAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
|
|
86
93
|
|
|
87
94
|
const lastImportDeclaration = [...importDeclarations.values()].at(-1);
|
|
@@ -89,18 +96,21 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
89
96
|
|
|
90
97
|
// make sure that afterAll is imported from jest
|
|
91
98
|
const jestImportDeclaration = importDeclarations.get('@jest/globals');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
specifier
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
assert.ok(jestImportDeclaration);
|
|
100
|
+
const importsToAdd = ['afterAll', 'beforeAll'].filter(
|
|
101
|
+
(jestHook) =>
|
|
102
|
+
!jestImportDeclaration.specifiers.some(
|
|
103
|
+
(specifier) =>
|
|
104
|
+
specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
|
|
105
|
+
specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
106
|
+
specifier.imported.name === jestHook,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
if (importsToAdd.length > 0) {
|
|
101
110
|
const firstImportSpecifier = jestImportDeclaration.specifiers[0];
|
|
102
111
|
assert.ok(firstImportSpecifier);
|
|
103
|
-
jestImportFixer = (fixer: RuleFixer) =>
|
|
112
|
+
jestImportFixer = (fixer: RuleFixer) =>
|
|
113
|
+
fixer.insertTextBefore(firstImportSpecifier, `${importsToAdd.join(', ')}, `);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
// make sure that agent is imported
|
|
@@ -124,45 +134,60 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
124
134
|
fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '${fixturePluginImportPath}';`);
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
// inject agent declaration and initialization
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
// inject agent declaration and initialization
|
|
138
|
+
if (beforeAll === undefined) {
|
|
139
|
+
// create `beforeAll` block if it doesn't exist
|
|
140
|
+
beforeAllOrEachFixer = (fixer: RuleFixer) =>
|
|
141
|
+
fixer.insertTextBefore(
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
143
|
+
beforeEach!,
|
|
144
|
+
[
|
|
145
|
+
STATEMENT_AGENT_DECLARATION,
|
|
146
|
+
`beforeAll(async () => {`,
|
|
147
|
+
[STATEMENT_AGENT_CREATION, STATEMENT_AGENT_REGISTER, STATEMENT_AGENT_ENABLE].join('\n'),
|
|
148
|
+
`});\n`,
|
|
149
|
+
].join('\n'),
|
|
137
150
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
} else {
|
|
150
|
-
beforeAllFixer = (fixer: RuleFixer) =>
|
|
151
|
-
fixer.replaceText(
|
|
152
|
-
beforeAllArgument,
|
|
153
|
-
[
|
|
154
|
-
`async () => {`,
|
|
155
|
-
STATEMENT_AGENT_CREATION,
|
|
156
|
-
STATEMENT_AGENT_REGISTER,
|
|
157
|
-
STATEMENT_AGENT_ENABLE,
|
|
158
|
-
STATEMENT_FIXTURE_RESET_AWAITED,
|
|
159
|
-
`}`,
|
|
160
|
-
].join('\n'),
|
|
151
|
+
} else {
|
|
152
|
+
const beforeAllArgument = beforeAll.arguments[0];
|
|
153
|
+
assert.ok(beforeAllArgument !== undefined);
|
|
154
|
+
if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) {
|
|
155
|
+
if (
|
|
156
|
+
beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
157
|
+
beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement
|
|
158
|
+
) {
|
|
159
|
+
const fixtureResetStatement = beforeAllArgument.body.body.find(
|
|
160
|
+
(statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED,
|
|
161
161
|
);
|
|
162
|
+
assert.ok(fixtureResetStatement !== undefined);
|
|
163
|
+
beforeAllOrEachFixer = (fixer: RuleFixer) =>
|
|
164
|
+
fixer.replaceText(
|
|
165
|
+
fixtureResetStatement,
|
|
166
|
+
[
|
|
167
|
+
STATEMENT_AGENT_CREATION,
|
|
168
|
+
STATEMENT_AGENT_REGISTER,
|
|
169
|
+
STATEMENT_AGENT_ENABLE,
|
|
170
|
+
STATEMENT_FIXTURE_RESET_AWAITED,
|
|
171
|
+
].join('\n'),
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
beforeAllOrEachFixer = (fixer: RuleFixer) =>
|
|
175
|
+
fixer.replaceText(
|
|
176
|
+
beforeAllArgument,
|
|
177
|
+
[
|
|
178
|
+
`async () => {`,
|
|
179
|
+
STATEMENT_AGENT_CREATION,
|
|
180
|
+
STATEMENT_AGENT_REGISTER,
|
|
181
|
+
STATEMENT_AGENT_ENABLE,
|
|
182
|
+
STATEMENT_FIXTURE_RESET_AWAITED,
|
|
183
|
+
`}`,
|
|
184
|
+
].join('\n'),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
agentDeclarationFixer = (fixer: RuleFixer) =>
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
189
|
+
fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`);
|
|
162
190
|
}
|
|
163
|
-
agentDeclarationFixer = (fixer: RuleFixer) =>
|
|
164
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
165
|
-
fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`);
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
// inject agent disposal to `afterAll` block
|
|
@@ -182,7 +207,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
182
207
|
afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastStatement, STATEMENT_AGENT_DISPOSE);
|
|
183
208
|
}
|
|
184
209
|
} else {
|
|
185
|
-
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
211
|
+
const nextToken = sourceCode.getTokenAfter(beforeAll ?? beforeEach!);
|
|
186
212
|
afterAllFixer = (fixer: RuleFixer) =>
|
|
187
213
|
fixer.insertTextAfter(
|
|
188
214
|
nextToken !== null && nextToken.type === AST_TOKEN_TYPES.Punctuator
|
|
@@ -198,12 +224,13 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
198
224
|
agentImportFixer !== undefined ||
|
|
199
225
|
fixturePluginImportFixer !== undefined ||
|
|
200
226
|
agentDeclarationFixer !== undefined ||
|
|
201
|
-
|
|
227
|
+
beforeAllOrEachFixer !== undefined ||
|
|
202
228
|
afterAllFixer !== undefined
|
|
203
229
|
) {
|
|
204
230
|
context.report({
|
|
205
231
|
messageId: 'updateTestWiring',
|
|
206
|
-
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
233
|
+
node: beforeAll ?? beforeEach!,
|
|
207
234
|
*fix(fixer) {
|
|
208
235
|
if (jestImportFixer !== undefined) {
|
|
209
236
|
yield jestImportFixer(fixer);
|
|
@@ -217,8 +244,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
|
|
|
217
244
|
if (agentDeclarationFixer !== undefined) {
|
|
218
245
|
yield agentDeclarationFixer(fixer);
|
|
219
246
|
}
|
|
220
|
-
if (
|
|
221
|
-
yield
|
|
247
|
+
if (beforeAllOrEachFixer !== undefined) {
|
|
248
|
+
yield beforeAllOrEachFixer(fixer);
|
|
222
249
|
}
|
|
223
250
|
if (afterAllFixer !== undefined) {
|
|
224
251
|
yield afterAllFixer(fixer);
|
|
@@ -28,7 +28,7 @@ interface Change {
|
|
|
28
28
|
// replacementText: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = createRule({
|
|
31
|
+
const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'refactorNeeded'> = createRule({
|
|
32
32
|
name: ruleId,
|
|
33
33
|
meta: {
|
|
34
34
|
type: 'suggestion',
|
|
@@ -36,6 +36,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
36
36
|
description: 'Replace "response.body" with "await response.json()".',
|
|
37
37
|
},
|
|
38
38
|
messages: {
|
|
39
|
+
refactorNeeded:
|
|
40
|
+
'Please extract the fetch call and check its reponse status code before accessing its response body.',
|
|
39
41
|
replaceBodyWithJson: 'Replace "response.body" with "await response.json()".',
|
|
40
42
|
unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
|
|
41
43
|
},
|
|
@@ -59,6 +61,14 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
59
61
|
responseType.getProperties().some((symbol) => symbol.name === 'json');
|
|
60
62
|
|
|
61
63
|
if (shouldReplace) {
|
|
64
|
+
if (responseBodyNode.object.type !== AST_NODE_TYPES.Identifier) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: responseBodyNode,
|
|
67
|
+
messageId: 'refactorNeeded',
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const enclosingFunction = getAncestor(
|
|
63
73
|
responseBodyNode,
|
|
64
74
|
(node: TSESTree.Node) =>
|
|
@@ -77,14 +87,23 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
77
87
|
const enclosingStatementIndex = (enclosingFunction.body as TSESTree.BlockStatement).body.indexOf(
|
|
78
88
|
enclosingStatement,
|
|
79
89
|
);
|
|
80
|
-
const responseVariableName =
|
|
90
|
+
const responseVariableName = responseBodyNode.object.name;
|
|
81
91
|
const isResponseBodyVariableDeclared =
|
|
82
92
|
enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration &&
|
|
83
|
-
enclosingStatement.declarations.some(
|
|
93
|
+
enclosingStatement.declarations.some(
|
|
94
|
+
(declaration) =>
|
|
95
|
+
declaration.init === responseBodyNode ||
|
|
96
|
+
(declaration.init?.type === AST_NODE_TYPES.TSAsExpression &&
|
|
97
|
+
declaration.init.expression === responseBodyNode),
|
|
98
|
+
);
|
|
84
99
|
const responseBodyVariableName = isResponseBodyVariableDeclared
|
|
85
|
-
? (enclosingStatement.declarations.find(
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
? (enclosingStatement.declarations.find(
|
|
101
|
+
(declaration) =>
|
|
102
|
+
declaration.init === responseBodyNode ||
|
|
103
|
+
(declaration.init?.type === AST_NODE_TYPES.TSAsExpression &&
|
|
104
|
+
declaration.init.expression === responseBodyNode),
|
|
105
|
+
)?.id as unknown as string)
|
|
106
|
+
: `${responseBodyNode.object.name}Body`;
|
|
88
107
|
|
|
89
108
|
const change: Change = {
|
|
90
109
|
enclosingFunction,
|
|
@@ -121,7 +140,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
const fixes: { node: TSESTree.Node; text: string; insert: boolean }[] = [];
|
|
143
|
+
const fixes: { node: TSESTree.Node | TSESTree.Token; text: string; insert: boolean }[] = [];
|
|
125
144
|
for (const changesByFunction of allChanges.values()) {
|
|
126
145
|
for (const changesByResponse of changesByFunction.values()) {
|
|
127
146
|
const orderedChanges = changesByResponse.sort(
|
|
@@ -141,8 +160,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
141
160
|
let remainingChanges;
|
|
142
161
|
if (!isResponseBodyVariableDeclared) {
|
|
143
162
|
fixes.push({
|
|
144
|
-
node: enclosingStatement,
|
|
145
|
-
text:
|
|
163
|
+
node: context.sourceCode.getTokenBefore(enclosingStatement) as TSESTree.Token,
|
|
164
|
+
text: `\nconst ${responseBodyVariableName} = await ${responseVariableName}.json();`,
|
|
146
165
|
insert: true,
|
|
147
166
|
});
|
|
148
167
|
remainingChanges = orderedChanges;
|
|
@@ -161,12 +180,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
|
|
|
161
180
|
}
|
|
162
181
|
}
|
|
163
182
|
|
|
164
|
-
for (const fix of fixes) {
|
|
183
|
+
for (const fix of fixes.reverse()) {
|
|
165
184
|
context.report({
|
|
166
185
|
node: fix.node,
|
|
167
186
|
messageId: 'replaceBodyWithJson',
|
|
168
187
|
fix(fixer) {
|
|
169
|
-
return fix.insert ? fixer.
|
|
188
|
+
return fix.insert ? fixer.insertTextAfter(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text);
|
|
170
189
|
},
|
|
171
190
|
});
|
|
172
191
|
}
|
package/src/agent/fetch-then.ts
CHANGED
|
@@ -103,10 +103,12 @@ function createResponseAssertions(
|
|
|
103
103
|
responseVariableName,
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
|
-
nonStatusAssertions.push(`assert.
|
|
106
|
+
nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
|
|
107
107
|
} else if (assertionArgument.type === 'Identifier') {
|
|
108
108
|
// callback assertion using function reference
|
|
109
|
-
nonStatusAssertions.push(
|
|
109
|
+
nonStatusAssertions.push(
|
|
110
|
+
`assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
|
|
111
|
+
);
|
|
110
112
|
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
111
113
|
// body deep equal assertion
|
|
112
114
|
nonStatusAssertions.push(
|
package/src/agent/fetch.ts
CHANGED
|
@@ -8,6 +8,10 @@ export function getResponseBodyRetrievalText(responseVariableName: string) {
|
|
|
8
8
|
return `await ${responseVariableName}.json()`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function getResponseHeadersRetrievalText(responseVariableName: string) {
|
|
12
|
+
return `${responseVariableName}.headers`;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean {
|
|
12
16
|
const responseHeaderAccessParent = getParent(responseHeadersAccess);
|
|
13
17
|
if (responseHeaderAccessParent?.type === 'VariableDeclarator') {
|
|
@@ -22,6 +22,7 @@ const DEFAULT_OPTIONS = {
|
|
|
22
22
|
'Fixture<ResolvedServices>',
|
|
23
23
|
'InboundContext',
|
|
24
24
|
'{ get: () => string; }',
|
|
25
|
+
'Api',
|
|
25
26
|
],
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -77,7 +78,21 @@ const rule: ESLintUtils.RuleModule<
|
|
|
77
78
|
|
|
78
79
|
log('===== file name:', context.filename);
|
|
79
80
|
log('callExpression:', sourceCode.getText(callExpression));
|
|
81
|
+
|
|
80
82
|
try {
|
|
83
|
+
const actualParameters = callExpression.arguments;
|
|
84
|
+
if (
|
|
85
|
+
!actualParameters.some((actualParameter) => {
|
|
86
|
+
const actualType = typeChecker.getTypeAtLocation(
|
|
87
|
+
parserServices.esTreeNodeToTSNodeMap.get(actualParameter),
|
|
88
|
+
);
|
|
89
|
+
const actualTypeString = typeChecker.typeToString(actualType);
|
|
90
|
+
return typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType');
|
|
91
|
+
})
|
|
92
|
+
) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee);
|
|
82
97
|
const calleeType = typeChecker.getTypeAtLocation(calleeTsNode);
|
|
83
98
|
|
|
@@ -88,13 +103,14 @@ const rule: ESLintUtils.RuleModule<
|
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
const signature = signatures[0];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
assert(signature);
|
|
107
|
+
// if (
|
|
108
|
+
// signature === undefined ||
|
|
109
|
+
// (signature.typeParameters !== undefined && signature.typeParameters.length > 0)
|
|
110
|
+
// ) {
|
|
111
|
+
// // ignore complex signatures with type parameters
|
|
112
|
+
// return;
|
|
113
|
+
// }
|
|
98
114
|
|
|
99
115
|
log('signature:', signature.getDeclaration().getText());
|
|
100
116
|
const expectedParameters = signature.getParameters();
|
|
@@ -105,7 +121,6 @@ const rule: ESLintUtils.RuleModule<
|
|
|
105
121
|
),
|
|
106
122
|
);
|
|
107
123
|
const expectedParametersCount = expectedParameters.length;
|
|
108
|
-
const actualParameters = callExpression.arguments;
|
|
109
124
|
const actualParametersCount = actualParameters.length;
|
|
110
125
|
if (actualParametersCount === 0) {
|
|
111
126
|
return;
|
|
@@ -132,18 +147,15 @@ const rule: ESLintUtils.RuleModule<
|
|
|
132
147
|
);
|
|
133
148
|
log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString);
|
|
134
149
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
log('
|
|
140
|
-
|
|
141
|
-
parametersToKeep.push(actualParameter);
|
|
142
|
-
expectedParameterIndex++;
|
|
143
|
-
log('matched');
|
|
144
|
-
} else {
|
|
145
|
-
log('not matched');
|
|
150
|
+
if (
|
|
151
|
+
(typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType')) &&
|
|
152
|
+
!typeChecker.isTypeAssignableTo(actualType, expectedType)
|
|
153
|
+
) {
|
|
154
|
+
log('removing un-matched parameter', sourceCode.getText(actualParameter));
|
|
155
|
+
continue;
|
|
146
156
|
}
|
|
157
|
+
parametersToKeep.push(actualParameter);
|
|
158
|
+
expectedParameterIndex++;
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
if (parametersToKeep.length === actualParametersCount) {
|
package/src/agent/no-fixture.ts
CHANGED
|
@@ -33,7 +33,7 @@ import getDocumentationUrl from '../get-documentation-url';
|
|
|
33
33
|
import { getIndentation } from '../library/format';
|
|
34
34
|
import { isValidPropertyName } from '../library/variable';
|
|
35
35
|
import { analyzeResponseReferences } from './response-reference';
|
|
36
|
-
import { getResponseBodyRetrievalText, hasAssertions } from './fetch';
|
|
36
|
+
import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, hasAssertions } from './fetch';
|
|
37
37
|
import { replaceEndpointUrlPrefixWithBasePath } from './url';
|
|
38
38
|
|
|
39
39
|
export const ruleId = 'no-fixture';
|
|
@@ -48,6 +48,7 @@ interface FixtureCallInformation {
|
|
|
48
48
|
assertions?: Expression[][];
|
|
49
49
|
inlineStatementNode?: Node;
|
|
50
50
|
inlineBodyReference?: MemberExpression;
|
|
51
|
+
inlineHeadersReference?: MemberExpression;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
@@ -80,6 +81,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
|
|
|
80
81
|
if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') {
|
|
81
82
|
results.inlineBodyReference = awaitParent;
|
|
82
83
|
}
|
|
84
|
+
if (
|
|
85
|
+
awaitParent.property.type === 'Identifier' &&
|
|
86
|
+
(awaitParent.property.name === 'header' || awaitParent.property.name === 'headers')
|
|
87
|
+
) {
|
|
88
|
+
results.inlineHeadersReference = awaitParent;
|
|
89
|
+
}
|
|
83
90
|
} else if (enclosingStatement.type === 'VariableDeclaration') {
|
|
84
91
|
results.variableDeclaration = enclosingStatement;
|
|
85
92
|
results.rootNode = enclosingStatement;
|
|
@@ -156,10 +163,12 @@ function createResponseAssertions(
|
|
|
156
163
|
responseVariableName,
|
|
157
164
|
);
|
|
158
165
|
}
|
|
159
|
-
nonStatusAssertions.push(`assert.
|
|
166
|
+
nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
|
|
160
167
|
} else if (assertionArgument.type === 'Identifier') {
|
|
161
168
|
// callback assertion using function reference
|
|
162
|
-
nonStatusAssertions.push(
|
|
169
|
+
nonStatusAssertions.push(
|
|
170
|
+
`assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
|
|
171
|
+
);
|
|
163
172
|
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
164
173
|
// body deep equal assertion
|
|
165
174
|
nonStatusAssertions.push(
|
|
@@ -338,11 +347,19 @@ const rule: Rule.RuleModule = {
|
|
|
338
347
|
(responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
|
|
339
348
|
const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
|
|
340
349
|
|
|
350
|
+
const isResponseHeadersVariableRedefinitionNeeded =
|
|
351
|
+
(destructuringResponseHeadersVariable !== undefined &&
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
353
|
+
(destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') ||
|
|
354
|
+
fixtureCallInformation.inlineHeadersReference !== undefined;
|
|
355
|
+
const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`;
|
|
356
|
+
|
|
341
357
|
const isResponseVariableRedefinitionNeeded =
|
|
342
358
|
(fixtureCallInformation.variableAssignment === undefined &&
|
|
343
359
|
responseVariable === undefined &&
|
|
344
360
|
fixtureCallInformation.assertions !== undefined) ||
|
|
345
|
-
isResponseBodyVariableRedefinitionNeeded
|
|
361
|
+
isResponseBodyVariableRedefinitionNeeded ||
|
|
362
|
+
isResponseHeadersVariableRedefinitionNeeded;
|
|
346
363
|
|
|
347
364
|
const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
|
|
348
365
|
? [
|
|
@@ -350,16 +367,24 @@ const rule: Rule.RuleModule = {
|
|
|
350
367
|
...(destructuringResponseBodyVariable
|
|
351
368
|
? [
|
|
352
369
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
353
|
-
|
|
370
|
+
`${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
|
|
354
371
|
]
|
|
355
372
|
: isResponseBodyVariableRedefinitionNeeded
|
|
356
373
|
? [
|
|
357
374
|
`const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
|
|
358
375
|
]
|
|
359
376
|
: []),
|
|
377
|
+
// eslint-disable-next-line no-nested-ternary
|
|
360
378
|
...(destructuringResponseHeadersVariable
|
|
361
|
-
? [
|
|
362
|
-
|
|
379
|
+
? [
|
|
380
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
381
|
+
`${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseHeadersVariable as ObjectPattern) : (destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
|
|
382
|
+
]
|
|
383
|
+
: isResponseHeadersVariableRedefinitionNeeded
|
|
384
|
+
? [
|
|
385
|
+
`const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
|
|
386
|
+
]
|
|
387
|
+
: []),
|
|
363
388
|
]
|
|
364
389
|
: [];
|
|
365
390
|
|
|
@@ -367,14 +392,14 @@ const rule: Rule.RuleModule = {
|
|
|
367
392
|
fixtureCallInformation,
|
|
368
393
|
sourceCode,
|
|
369
394
|
responseVariableNameToUse,
|
|
370
|
-
destructuringResponseHeadersVariable,
|
|
395
|
+
destructuringResponseHeadersVariable as Scope.Variable | undefined,
|
|
371
396
|
);
|
|
372
397
|
|
|
373
398
|
// add variable declaration if needed
|
|
374
399
|
const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
|
|
375
400
|
const fetchStatementText = !isResponseVariableRedefinitionNeeded
|
|
376
401
|
? fetchCallText
|
|
377
|
-
:
|
|
402
|
+
: `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`;
|
|
378
403
|
|
|
379
404
|
const nodeToReplace = isResponseVariableRedefinitionNeeded
|
|
380
405
|
? fixtureCallInformation.rootNode
|
|
@@ -29,7 +29,7 @@ export function analyzeResponseReferences(
|
|
|
29
29
|
headersReferences: MemberExpression[];
|
|
30
30
|
statusReferences: MemberExpression[];
|
|
31
31
|
destructuringBodyVariable?: Scope.Variable | ObjectPattern;
|
|
32
|
-
destructuringHeadersVariable?: Scope.Variable;
|
|
32
|
+
destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
|
|
33
33
|
destructuringHeadersReferences?: MemberExpression[] | undefined;
|
|
34
34
|
} {
|
|
35
35
|
const results: {
|
|
@@ -38,7 +38,7 @@ export function analyzeResponseReferences(
|
|
|
38
38
|
headersReferences: MemberExpression[];
|
|
39
39
|
statusReferences: MemberExpression[];
|
|
40
40
|
destructuringBodyVariable?: Scope.Variable | ObjectPattern;
|
|
41
|
-
destructuringHeadersVariable?: Scope.Variable;
|
|
41
|
+
destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
|
|
42
42
|
destructuringHeadersReferences?: MemberExpression[] | undefined;
|
|
43
43
|
} = {
|
|
44
44
|
bodyReferences: [],
|
|
@@ -105,13 +105,20 @@ export function analyzeResponseReferences(
|
|
|
105
105
|
getParent(parent)?.type !== 'CallExpression',
|
|
106
106
|
);
|
|
107
107
|
} else if (identifierParent.type === 'Property') {
|
|
108
|
-
// body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..."
|
|
109
108
|
const parent = getParent(identifierParent);
|
|
110
109
|
if (parent?.type === 'ObjectPattern') {
|
|
110
|
+
// body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..."
|
|
111
111
|
const parent2 = getParent(parent);
|
|
112
112
|
if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') {
|
|
113
113
|
results.destructuringBodyVariable = parent;
|
|
114
114
|
}
|
|
115
|
+
if (
|
|
116
|
+
parent2?.type === 'Property' &&
|
|
117
|
+
parent2.key.type === 'Identifier' &&
|
|
118
|
+
(parent2.key.name === 'header' || parent2.key.name === 'headers')
|
|
119
|
+
) {
|
|
120
|
+
results.destructuringHeadersVariable = parent;
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
} else {
|
|
117
124
|
log('+++++++ can not handle identifierParent', identifierParent);
|
|
@@ -141,7 +141,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu
|
|
|
141
141
|
|
|
142
142
|
const enclosingScopeNode = getEnclosingScopeNode(serviceCall);
|
|
143
143
|
assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined');
|
|
144
|
-
const scope = scopeManager?.acquire(enclosingScopeNode);
|
|
144
|
+
const scope = scopeManager?.acquire(enclosingScopeNode);
|
|
145
145
|
assert.ok(scope, 'scope is undefined');
|
|
146
146
|
const urlArgument = serviceCall.arguments[0];
|
|
147
147
|
if (!isUrlArgumentValid(urlArgument, scope)) {
|