@checkdigit/eslint-plugin 6.6.0-PR.75-3e31 → 6.6.0-PR.75-a3df
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 +140 -46
- package/dist-cjs/metafile.json +5 -5
- package/dist-mjs/ast/tree.mjs +25 -5
- package/dist-mjs/no-fixture.mjs +118 -42
- package/dist-types/ast/tree.d.ts +3 -1
- package/package.json +1 -1
- package/src/ast/tree.ts +27 -4
- package/src/no-fixture.ts +152 -56
package/src/no-fixture.ts
CHANGED
|
@@ -6,19 +6,18 @@
|
|
|
6
6
|
* This code is licensed under the MIT license (see LICENSE.txt for details).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/* eslint-disable no-console */
|
|
10
|
-
|
|
11
9
|
import type {
|
|
12
10
|
AwaitExpression,
|
|
13
11
|
CallExpression,
|
|
14
12
|
Expression,
|
|
15
13
|
MemberExpression,
|
|
14
|
+
Node,
|
|
16
15
|
ReturnStatement,
|
|
17
16
|
SimpleCallExpression,
|
|
18
17
|
VariableDeclaration,
|
|
19
18
|
} from 'estree';
|
|
20
19
|
import type { Rule, Scope, SourceCode } from 'eslint';
|
|
21
|
-
import {
|
|
20
|
+
import { getEnclosingScopeNode, getEnclosingStatement, getParent } from './ast/tree';
|
|
22
21
|
import { strict as assert } from 'node:assert';
|
|
23
22
|
import getDocumentationUrl from './get-documentation-url';
|
|
24
23
|
import { getIndentation } from './ast/format';
|
|
@@ -32,6 +31,8 @@ interface FixtureCallInformation {
|
|
|
32
31
|
requestBody?: Expression;
|
|
33
32
|
requestHeaders?: { name: Expression; value: Expression }[];
|
|
34
33
|
assertions?: Expression[][];
|
|
34
|
+
inlineStatementNode?: Node;
|
|
35
|
+
inlineBodyReference?: MemberExpression;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
@@ -46,11 +47,18 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
|
|
|
46
47
|
results.rootNode = parent;
|
|
47
48
|
} else if (parent.type === 'AwaitExpression') {
|
|
48
49
|
results.fixtureNode = call;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
results.rootNode =
|
|
50
|
+
const enclosingStatement = getEnclosingStatement(parent);
|
|
51
|
+
assert.ok(enclosingStatement);
|
|
52
|
+
const awaitParent = getParent(parent);
|
|
53
|
+
if (awaitParent?.type === 'MemberExpression') {
|
|
54
|
+
results.rootNode = parent;
|
|
55
|
+
results.inlineStatementNode = enclosingStatement;
|
|
56
|
+
if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') {
|
|
57
|
+
results.inlineBodyReference = awaitParent;
|
|
58
|
+
}
|
|
59
|
+
} else if (enclosingStatement.type === 'VariableDeclaration') {
|
|
60
|
+
results.variableDeclaration = enclosingStatement;
|
|
61
|
+
results.rootNode = enclosingStatement;
|
|
54
62
|
} else {
|
|
55
63
|
results.rootNode = parent;
|
|
56
64
|
}
|
|
@@ -90,8 +98,8 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s
|
|
|
90
98
|
bodyReferences: MemberExpression[];
|
|
91
99
|
headersReferences: MemberExpression[];
|
|
92
100
|
statusReferences: MemberExpression[];
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
destructuringBodyVariable?: Scope.Variable;
|
|
102
|
+
destructuringHeadersVariable?: Scope.Variable;
|
|
95
103
|
} = {
|
|
96
104
|
bodyReferences: [],
|
|
97
105
|
headersReferences: [],
|
|
@@ -147,14 +155,14 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s
|
|
|
147
155
|
identifierParent.key.type === 'Identifier' &&
|
|
148
156
|
identifierParent.key.name === 'body'
|
|
149
157
|
) {
|
|
150
|
-
results.
|
|
158
|
+
results.destructuringBodyVariable = responseVariable;
|
|
151
159
|
} else if (
|
|
152
160
|
// header reference through destruction/renaming, e.g. "const { headers } = ..."
|
|
153
161
|
identifierParent.type === 'Property' &&
|
|
154
162
|
identifierParent.key.type === 'Identifier' &&
|
|
155
163
|
identifierParent.key.name === 'headers'
|
|
156
164
|
) {
|
|
157
|
-
results.
|
|
165
|
+
results.destructuringHeadersVariable = responseVariable;
|
|
158
166
|
} else {
|
|
159
167
|
throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
|
|
160
168
|
}
|
|
@@ -173,12 +181,12 @@ function isValidPropertyName(name: unknown) {
|
|
|
173
181
|
return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
176
185
|
function createResponseAssertions(
|
|
177
186
|
fixtureCallInformation: FixtureCallInformation,
|
|
178
187
|
sourceCode: SourceCode,
|
|
179
|
-
|
|
188
|
+
responseVariableName: string,
|
|
180
189
|
) {
|
|
181
|
-
// [TODO:] make sure status assertion is ordered as the first
|
|
182
190
|
let statusAssertion: string | undefined;
|
|
183
191
|
const nonStatusAssertions: string[] = [];
|
|
184
192
|
for (const expectArguments of fixtureCallInformation.assertions ?? []) {
|
|
@@ -189,20 +197,32 @@ function createResponseAssertions(
|
|
|
189
197
|
(assertionArgument.type === 'MemberExpression' &&
|
|
190
198
|
assertionArgument.object.type === 'Identifier' &&
|
|
191
199
|
assertionArgument.object.name === 'StatusCodes') ||
|
|
192
|
-
assertionArgument.type === 'Literal'
|
|
200
|
+
assertionArgument.type === 'Literal' ||
|
|
201
|
+
sourceCode.getText(assertionArgument).includes('StatusCodes.')
|
|
193
202
|
) {
|
|
194
203
|
// status code assertion
|
|
195
|
-
statusAssertion = `assert.equal(${
|
|
204
|
+
statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
|
|
196
205
|
} else if (assertionArgument.type === 'ArrowFunctionExpression') {
|
|
197
|
-
// callback assertion
|
|
198
|
-
|
|
206
|
+
// callback assertion using arrow function
|
|
207
|
+
let functionBody = sourceCode.getText(assertionArgument.body);
|
|
208
|
+
|
|
209
|
+
const [originalResponseArgument] = assertionArgument.params;
|
|
210
|
+
assert.ok(originalResponseArgument?.type === 'Identifier');
|
|
211
|
+
const originalResponseArgumentName = originalResponseArgument.name;
|
|
212
|
+
if (originalResponseArgumentName !== responseVariableName) {
|
|
213
|
+
functionBody = functionBody.replace(
|
|
214
|
+
new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
|
|
215
|
+
responseVariableName,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
nonStatusAssertions.push(`assert.ok(${functionBody})`);
|
|
199
219
|
} else if (assertionArgument.type === 'Identifier') {
|
|
200
|
-
// callback assertion
|
|
201
|
-
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${
|
|
220
|
+
// callback assertion using function reference
|
|
221
|
+
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
|
|
202
222
|
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
203
223
|
// body deep equal assertion
|
|
204
224
|
nonStatusAssertions.push(
|
|
205
|
-
`assert.deepEqual(await ${
|
|
225
|
+
`assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
|
|
206
226
|
);
|
|
207
227
|
} else {
|
|
208
228
|
throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
|
|
@@ -213,11 +233,11 @@ function createResponseAssertions(
|
|
|
213
233
|
assert.ok(headerName && headerValue);
|
|
214
234
|
if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
|
|
215
235
|
nonStatusAssertions.push(
|
|
216
|
-
`assert.ok(${
|
|
236
|
+
`assert.ok(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
|
|
217
237
|
);
|
|
218
238
|
} else {
|
|
219
239
|
nonStatusAssertions.push(
|
|
220
|
-
`assert.equal(${
|
|
240
|
+
`assert.equal(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
|
|
221
241
|
);
|
|
222
242
|
}
|
|
223
243
|
}
|
|
@@ -228,6 +248,52 @@ function createResponseAssertions(
|
|
|
228
248
|
};
|
|
229
249
|
}
|
|
230
250
|
|
|
251
|
+
function getResponseVariableNameToUse(
|
|
252
|
+
scopeManager: Scope.ScopeManager,
|
|
253
|
+
fixtureCallInformation: FixtureCallInformation,
|
|
254
|
+
scopeVariablesMap: Map<Scope.Scope, string[]>,
|
|
255
|
+
) {
|
|
256
|
+
if (fixtureCallInformation.variableDeclaration) {
|
|
257
|
+
const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0];
|
|
258
|
+
// [TODO:] double check if it works for destruction/rename declaration
|
|
259
|
+
if (firstDeclaration && firstDeclaration.id.type === 'Identifier') {
|
|
260
|
+
return firstDeclaration.id.name;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode);
|
|
265
|
+
scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode);
|
|
266
|
+
assert.ok(enclosingScopeNode);
|
|
267
|
+
const scope = scopeManager.acquire(enclosingScopeNode);
|
|
268
|
+
assert.ok(scope !== null);
|
|
269
|
+
let scopeVariables = scopeVariablesMap.get(scope);
|
|
270
|
+
if (!scopeVariables) {
|
|
271
|
+
scopeVariables = [...scope.set.keys()];
|
|
272
|
+
scopeVariablesMap.set(scope, scopeVariables);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let responseVariableCounter = 0;
|
|
276
|
+
let responseVariableNameToUse;
|
|
277
|
+
while (responseVariableNameToUse === undefined) {
|
|
278
|
+
responseVariableCounter++;
|
|
279
|
+
responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`;
|
|
280
|
+
if (scopeVariables.includes(responseVariableNameToUse)) {
|
|
281
|
+
responseVariableNameToUse = undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
scopeVariables.push(responseVariableNameToUse);
|
|
285
|
+
return responseVariableNameToUse;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean {
|
|
289
|
+
const parent = getParent(responseBodyReference);
|
|
290
|
+
return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function getResponseBodyRetrievalText(responseVariableName: string) {
|
|
294
|
+
return `await ${responseVariableName}.json()`;
|
|
295
|
+
}
|
|
296
|
+
|
|
231
297
|
const rule: Rule.RuleModule = {
|
|
232
298
|
meta: {
|
|
233
299
|
type: 'suggestion',
|
|
@@ -238,15 +304,16 @@ const rule: Rule.RuleModule = {
|
|
|
238
304
|
messages: {
|
|
239
305
|
preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
|
|
240
306
|
unknownError:
|
|
241
|
-
'Unknown error occurred: {{ error }}. Please manually convert the fixture API call to fetch API call.',
|
|
307
|
+
'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
|
|
242
308
|
},
|
|
243
309
|
fixable: 'code',
|
|
244
310
|
schema: [],
|
|
245
311
|
},
|
|
312
|
+
// eslint-disable-next-line max-lines-per-function
|
|
246
313
|
create(context) {
|
|
247
314
|
const sourceCode = context.sourceCode;
|
|
248
315
|
const scopeManager = sourceCode.scopeManager;
|
|
249
|
-
|
|
316
|
+
const scopeVariablesMap = new Map<Scope.Scope, string[]>();
|
|
250
317
|
|
|
251
318
|
return {
|
|
252
319
|
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
@@ -269,8 +336,8 @@ const rule: Rule.RuleModule = {
|
|
|
269
336
|
bodyReferences: responseBodyReferences,
|
|
270
337
|
headersReferences: responseHeadersReferences,
|
|
271
338
|
statusReferences: responseStatusReferences,
|
|
272
|
-
|
|
273
|
-
|
|
339
|
+
destructuringBodyVariable: destructuringResponseBodyVariable,
|
|
340
|
+
destructuringHeadersVariable: destructuringResponseHeadersVariable,
|
|
274
341
|
} = analyzeResponseReferences(fixtureCallInformation, scopeManager);
|
|
275
342
|
|
|
276
343
|
// convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
|
|
@@ -300,25 +367,36 @@ const rule: Rule.RuleModule = {
|
|
|
300
367
|
'}',
|
|
301
368
|
].join(`\n${indentation}`);
|
|
302
369
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
370
|
+
const responseVariableNameToUse = getResponseVariableNameToUse(
|
|
371
|
+
scopeManager,
|
|
372
|
+
fixtureCallInformation,
|
|
373
|
+
scopeVariablesMap,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const isResponseBodyVariableRedefinitionNeeded =
|
|
377
|
+
destructuringResponseBodyVariable !== undefined ||
|
|
378
|
+
fixtureCallInformation.inlineBodyReference !== undefined ||
|
|
379
|
+
(responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
|
|
380
|
+
const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
|
|
310
381
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
382
|
+
const isResponseVariableRedefinitionNeeded =
|
|
383
|
+
(responseVariable === undefined && fixtureCallInformation.assertions !== undefined) ||
|
|
384
|
+
isResponseBodyVariableRedefinitionNeeded;
|
|
314
385
|
|
|
315
|
-
const responseBodyHeadersVariableRedefineLines =
|
|
386
|
+
const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
|
|
316
387
|
? [
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
388
|
+
// eslint-disable-next-line no-nested-ternary
|
|
389
|
+
...(destructuringResponseBodyVariable
|
|
390
|
+
? [
|
|
391
|
+
`const ${destructuringResponseBodyVariable.name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
|
|
392
|
+
]
|
|
393
|
+
: isResponseBodyVariableRedefinitionNeeded
|
|
394
|
+
? [
|
|
395
|
+
`const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
|
|
396
|
+
]
|
|
397
|
+
: []),
|
|
398
|
+
...(destructuringResponseHeadersVariable
|
|
399
|
+
? [`const ${destructuringResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`]
|
|
322
400
|
: []),
|
|
323
401
|
]
|
|
324
402
|
: [];
|
|
@@ -331,11 +409,11 @@ const rule: Rule.RuleModule = {
|
|
|
331
409
|
|
|
332
410
|
// add variable declaration if needed
|
|
333
411
|
const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
|
|
334
|
-
const fetchStatementText = !
|
|
412
|
+
const fetchStatementText = !isResponseVariableRedefinitionNeeded
|
|
335
413
|
? fetchCallText
|
|
336
414
|
: `const ${responseVariableNameToUse} = await ${fetchCallText}`;
|
|
337
415
|
|
|
338
|
-
const nodeToReplace =
|
|
416
|
+
const nodeToReplace = isResponseVariableRedefinitionNeeded
|
|
339
417
|
? fixtureCallInformation.rootNode
|
|
340
418
|
: fixtureCallInformation.fixtureNode;
|
|
341
419
|
const appendingAssignmentAndAssertionText = [
|
|
@@ -349,17 +427,33 @@ const rule: Rule.RuleModule = {
|
|
|
349
427
|
node: fixtureCall,
|
|
350
428
|
messageId: 'preferNativeFetch',
|
|
351
429
|
*fix(fixer) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
430
|
+
if (fixtureCallInformation.inlineStatementNode) {
|
|
431
|
+
const preInlineDeclaration = [
|
|
432
|
+
fetchStatementText,
|
|
433
|
+
`${appendingAssignmentAndAssertionText};\n${indentation}`,
|
|
434
|
+
].join(``);
|
|
435
|
+
yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration);
|
|
436
|
+
} else {
|
|
437
|
+
yield fixer.replaceText(nodeToReplace, fetchStatementText);
|
|
438
|
+
|
|
439
|
+
const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
|
|
440
|
+
yield fixer.insertTextAfter(
|
|
441
|
+
nodeToReplace,
|
|
442
|
+
needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
359
445
|
|
|
360
446
|
// handle response body references
|
|
361
447
|
for (const responseBodyReference of responseBodyReferences) {
|
|
362
|
-
yield fixer.replaceText(
|
|
448
|
+
yield fixer.replaceText(
|
|
449
|
+
responseBodyReference,
|
|
450
|
+
isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference)
|
|
451
|
+
? redefineResponseBodyVariableName
|
|
452
|
+
: getResponseBodyRetrievalText(responseVariableNameToUse),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (fixtureCallInformation.inlineBodyReference) {
|
|
456
|
+
yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName);
|
|
363
457
|
}
|
|
364
458
|
|
|
365
459
|
// handle response headers references
|
|
@@ -390,7 +484,7 @@ const rule: Rule.RuleModule = {
|
|
|
390
484
|
}
|
|
391
485
|
}
|
|
392
486
|
|
|
393
|
-
// handle direct return without await
|
|
487
|
+
// handle direct return statement without await, e.g. "return fixture.api.get(...);"
|
|
394
488
|
if (
|
|
395
489
|
fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
|
|
396
490
|
fixtureCallInformation.assertions !== undefined
|
|
@@ -403,12 +497,14 @@ const rule: Rule.RuleModule = {
|
|
|
403
497
|
},
|
|
404
498
|
});
|
|
405
499
|
} catch (error) {
|
|
406
|
-
|
|
500
|
+
// eslint-disable-next-line no-console
|
|
501
|
+
console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
|
|
407
502
|
context.report({
|
|
408
503
|
node: fixtureCall,
|
|
409
504
|
messageId: 'unknownError',
|
|
410
505
|
data: {
|
|
411
|
-
|
|
506
|
+
fileName: context.filename,
|
|
507
|
+
error: error instanceof Error ? error.toString() : JSON.stringify(error),
|
|
412
508
|
},
|
|
413
509
|
});
|
|
414
510
|
}
|