@checkdigit/eslint-plugin 6.6.0-PR.75-bbf4 → 6.6.0-PR.75-4211

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.
@@ -0,0 +1,156 @@
1
+ // src/require-resolve-full-response.ts
2
+ import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
3
+ import { DefinitionType } from "@typescript-eslint/scope-manager";
4
+ import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP } from "./agent/url.mjs";
5
+ import { strict as assert } from "node:assert";
6
+ import getDocumentationUrl from "./get-documentation-url.mjs";
7
+ import { getEnclosingScopeNode } from "./library/ts-tree.mjs";
8
+ var ruleId = "require-resolve-full-response";
9
+ var createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
10
+ var rule = createRule({
11
+ name: ruleId,
12
+ meta: {
13
+ type: "suggestion",
14
+ docs: {
15
+ description: "Prefer native fetch over customized service wrapper."
16
+ },
17
+ messages: {
18
+ invalidOptions: '"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.',
19
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.'
20
+ },
21
+ schema: []
22
+ },
23
+ defaultOptions: [],
24
+ create(context) {
25
+ const sourceCode = context.sourceCode;
26
+ const scopeManager = sourceCode.scopeManager;
27
+ const parserService = ESLintUtils.getParserServices(context);
28
+ const typeChecker = parserService.program.getTypeChecker();
29
+ function isUrlArgumentValid(urlArgument, scope) {
30
+ if (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === "string" || urlArgument?.type === AST_NODE_TYPES.TemplateLiteral) {
31
+ const urlText = sourceCode.getText(urlArgument);
32
+ return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText);
33
+ }
34
+ if (urlArgument?.type === AST_NODE_TYPES.Identifier) {
35
+ const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name);
36
+ if (foundVariable) {
37
+ const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable);
38
+ assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`);
39
+ const variableDefinitionNode = variableDefinition.node;
40
+ assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator);
41
+ assert.ok(variableDefinitionNode.init, "Variable definition node has no init property");
42
+ return isUrlArgumentValid(variableDefinitionNode.init, scope);
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ function getType(identifier) {
48
+ const variable = parserService.esTreeNodeToTSNodeMap.get(identifier);
49
+ const variableType = typeChecker.getTypeAtLocation(variable);
50
+ return typeChecker.typeToString(variableType);
51
+ }
52
+ function isServiceLikeName(name) {
53
+ return /.*[Ss]ervice$/u.test(name);
54
+ }
55
+ function isCalleeServiceWrapper(serviceCall) {
56
+ const callee = serviceCall.callee;
57
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) {
58
+ return false;
59
+ }
60
+ const endpoint = callee.object;
61
+ if (endpoint.type === AST_NODE_TYPES.Identifier) {
62
+ return getType(endpoint) === "Endpoint" || isServiceLikeName(endpoint.name);
63
+ }
64
+ if (endpoint.type !== AST_NODE_TYPES.CallExpression) {
65
+ return false;
66
+ }
67
+ const [contextArgument] = endpoint.arguments;
68
+ if (contextArgument?.type !== AST_NODE_TYPES.Identifier) {
69
+ return false;
70
+ }
71
+ if (contextArgument.name !== "EMPTY_CONTEXT" && getType(contextArgument) !== "InboundContext") {
72
+ return false;
73
+ }
74
+ const service = endpoint.callee;
75
+ if (service.type === AST_NODE_TYPES.Identifier) {
76
+ return getType(service) === "ResolvedService";
77
+ }
78
+ if (service.type !== AST_NODE_TYPES.MemberExpression) {
79
+ return false;
80
+ }
81
+ const services = service.object;
82
+ if (services.type === AST_NODE_TYPES.Identifier) {
83
+ return getType(services) === "ResolvedServices";
84
+ }
85
+ if (services.type !== AST_NODE_TYPES.MemberExpression) {
86
+ return false;
87
+ }
88
+ const configuration = services.object;
89
+ if (configuration.type === AST_NODE_TYPES.Identifier) {
90
+ return ["Configuration", "Configuration<ResolvedServices>"].includes(getType(configuration));
91
+ }
92
+ if (configuration.type !== AST_NODE_TYPES.MemberExpression) {
93
+ return false;
94
+ }
95
+ const fixture = configuration.object;
96
+ if (fixture.type === AST_NODE_TYPES.Identifier) {
97
+ return fixture.name === "fixture" || getType(fixture) === "Fixture";
98
+ }
99
+ return false;
100
+ }
101
+ return {
102
+ "CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]": (serviceCall) => {
103
+ try {
104
+ if (!isCalleeServiceWrapper(serviceCall)) {
105
+ return;
106
+ }
107
+ const enclosingScopeNode = getEnclosingScopeNode(serviceCall);
108
+ assert.ok(enclosingScopeNode, "enclosingScopeNode is undefined");
109
+ const scope = scopeManager?.acquire(enclosingScopeNode);
110
+ assert.ok(scope, "scope is undefined");
111
+ const urlArgument = serviceCall.arguments[0];
112
+ if (!isUrlArgumentValid(urlArgument, scope)) {
113
+ return;
114
+ }
115
+ assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression);
116
+ assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier);
117
+ const method = serviceCall.callee.property.name;
118
+ const optionsArgument = ["get", "head", "del"].includes(method) ? serviceCall.arguments[1] : serviceCall.arguments[2];
119
+ if (optionsArgument === void 0 || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) {
120
+ context.report({
121
+ node: serviceCall,
122
+ messageId: "invalidOptions"
123
+ });
124
+ return;
125
+ }
126
+ const resolveWithFullResponseProperty = optionsArgument.properties.find(
127
+ (property) => property.type === AST_NODE_TYPES.Property && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === "resolveWithFullResponse"
128
+ );
129
+ if (resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || resolveWithFullResponseProperty.value.value !== true) {
130
+ context.report({
131
+ node: optionsArgument,
132
+ messageId: "invalidOptions"
133
+ });
134
+ return;
135
+ }
136
+ } catch (error) {
137
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
138
+ context.report({
139
+ node: serviceCall,
140
+ messageId: "unknownError",
141
+ data: {
142
+ fileName: context.filename,
143
+ error: error instanceof Error ? error.toString() : JSON.stringify(error)
144
+ }
145
+ });
146
+ }
147
+ }
148
+ };
149
+ }
150
+ });
151
+ var require_resolve_full_response_default = rule;
152
+ export {
153
+ require_resolve_full_response_default as default,
154
+ ruleId
155
+ };
156
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vc3JjL3JlcXVpcmUtcmVzb2x2ZS1mdWxsLXJlc3BvbnNlLnRzIl0sCiAgIm1hcHBpbmdzIjogIjtBQVFBLFNBQVMsZ0JBQWdCLG1CQUE2QjtBQUN0RCxTQUFTLHNCQUFrQztBQUMzQyxTQUFTLGtCQUFrQiw0QkFBNEI7QUFDdkQsU0FBUyxVQUFVLGNBQWM7QUFDakMsT0FBTyx5QkFBeUI7QUFDaEMsU0FBUyw2QkFBNkI7QUFFL0IsSUFBTSxTQUFTO0FBRXRCLElBQU0sYUFBYSxZQUFZLFlBQVksQ0FBQyxTQUFTLG9CQUFvQixJQUFJLENBQUM7QUFFOUUsSUFBTSxPQUFPLFdBQVc7QUFBQSxFQUN0QixNQUFNO0FBQUEsRUFDTixNQUFNO0FBQUEsSUFDSixNQUFNO0FBQUEsSUFDTixNQUFNO0FBQUEsTUFDSixhQUFhO0FBQUEsSUFDZjtBQUFBLElBQ0EsVUFBVTtBQUFBLE1BQ1IsZ0JBQ0U7QUFBQSxNQUNGLGNBQWM7QUFBQSxJQUNoQjtBQUFBLElBQ0EsUUFBUSxDQUFDO0FBQUEsRUFDWDtBQUFBLEVBQ0EsZ0JBQWdCLENBQUM7QUFBQSxFQUNqQixPQUFPLFNBQVM7QUFDZCxVQUFNLGFBQWEsUUFBUTtBQUMzQixVQUFNLGVBQWUsV0FBVztBQUNoQyxVQUFNLGdCQUFnQixZQUFZLGtCQUFrQixPQUFPO0FBQzNELFVBQU0sY0FBYyxjQUFjLFFBQVEsZUFBZTtBQUV6RCxhQUFTLG1CQUFtQixhQUF3QyxPQUFjO0FBQ2hGLFVBQ0csYUFBYSxTQUFTLGVBQWUsV0FBVyxPQUFPLFlBQVksVUFBVSxZQUM5RSxhQUFhLFNBQVMsZUFBZSxpQkFDckM7QUFDQSxjQUFNLFVBQVUsV0FBVyxRQUFRLFdBQVc7QUFDOUMsZUFBTyxpQkFBaUIsS0FBSyxPQUFPLEtBQUsscUJBQXFCLEtBQUssT0FBTztBQUFBLE1BQzVFO0FBRUEsVUFBSSxhQUFhLFNBQVMsZUFBZSxZQUFZO0FBQ25ELGNBQU0sZ0JBQWdCLE1BQU0sVUFBVSxLQUFLLENBQUMsYUFBYSxTQUFT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
@@ -19,6 +19,7 @@ declare const _default: {
19
19
  "fetch-response-header-getter-ts": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unknownError" | "useGetter", never[], import("@typescript-eslint/utils/ts-eslint").RuleListener>;
20
20
  "add-url-domain": import("@typescript-eslint/utils/ts-eslint").RuleModule<"addDomain" | "unknownError", never[], import("@typescript-eslint/utils/ts-eslint").RuleListener>;
21
21
  "no-full-response": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unknownError" | "removeFullResponse", never[], import("@typescript-eslint/utils/ts-eslint").RuleListener>;
22
+ "require-resolve-full-response": import("@typescript-eslint/utils/ts-eslint").RuleModule<"unknownError" | "invalidOptions", never[], import("@typescript-eslint/utils/ts-eslint").RuleListener>;
22
23
  };
23
24
  configs: {
24
25
  all: {
@@ -34,6 +35,15 @@ declare const _default: {
34
35
  '@checkdigit/no-test-import': string;
35
36
  "@checkdigit/invalid-json-stringify": string;
36
37
  "@checkdigit/no-promise-instance-method": string;
38
+ "@checkdigit/no-full-response": string;
39
+ "@checkdigit/require-resolve-full-response": string;
40
+ "@checkdigit/add-url-domain": string;
41
+ "@checkdigit/no-fixture": string;
42
+ "@checkdigit/no-service-wrapper": string;
43
+ "@checkdigit/no-status-code": string;
44
+ "@checkdigit/fetch-response-body-json": string;
45
+ "@checkdigit/fetch-response-header-getter-ts": string;
46
+ "@checkdigit/fetch-then": string;
37
47
  };
38
48
  };
39
49
  recommended: {
@@ -51,16 +61,31 @@ declare const _default: {
51
61
  "@checkdigit/no-promise-instance-method": string;
52
62
  };
53
63
  };
54
- agent: {
64
+ 'agent-serve-runtime': {
65
+ ignorePatterns: string[];
55
66
  rules: {
67
+ "@checkdigit/no-full-response": string;
68
+ "@checkdigit/require-resolve-full-response": string;
69
+ "@checkdigit/add-url-domain": string;
56
70
  "@checkdigit/no-fixture": string;
57
- "@checkdigit/fetch-then": string;
58
71
  "@checkdigit/no-service-wrapper": string;
59
72
  "@checkdigit/no-status-code": string;
60
73
  "@checkdigit/fetch-response-body-json": string;
61
74
  "@checkdigit/fetch-response-header-getter-ts": string;
62
- "@checkdigit/add-url-domain": string;
75
+ "@checkdigit/fetch-then": string;
76
+ };
77
+ };
78
+ 'agent-fixture': {
79
+ rules: {
63
80
  "@checkdigit/no-full-response": string;
81
+ "@checkdigit/require-resolve-full-response": string;
82
+ "@checkdigit/add-url-domain": string;
83
+ "@checkdigit/no-fixture": string;
84
+ "@checkdigit/no-service-wrapper": string;
85
+ "@checkdigit/no-status-code": string;
86
+ "@checkdigit/fetch-response-body-json": string;
87
+ "@checkdigit/fetch-response-header-getter-ts": string;
88
+ "@checkdigit/fetch-then": string;
64
89
  };
65
90
  };
66
91
  };
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const ruleId = "require-resolve-full-response";
3
+ declare const rule: ESLintUtils.RuleModule<"unknownError" | "invalidOptions", never[], ESLintUtils.RuleListener>;
4
+ export default rule;
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-bbf4","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","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs","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 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && 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":"7.18.0","@typescript-eslint/utils":"7.18.0","ts-api-utils":"^1.3.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"6.0.0","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.18.0","@typescript-eslint/parser":"^7.18.0","@typescript-eslint/rule-tester":"7.18.0","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0","http-status-codes":"^2.3.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-4211","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","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs","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 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && 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":"7.18.0","@typescript-eslint/utils":"7.18.0","ts-api-utils":"^1.3.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"6.0.0","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.18.0","@typescript-eslint/parser":"^7.18.0","@typescript-eslint/rule-tester":"7.18.0","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0","http-status-codes":"^2.3.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
@@ -193,8 +193,7 @@ const rule: Rule.RuleModule = {
193
193
  messages: {
194
194
  preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
195
195
  shouldUseHeaderGetter: 'Getter should be used to access response headers.',
196
- unknownError:
197
- 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
196
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
198
197
  },
199
198
  fixable: 'code',
200
199
  schema: [],
@@ -228,8 +228,7 @@ const rule: Rule.RuleModule = {
228
228
  },
229
229
  messages: {
230
230
  preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
231
- unknownError:
232
- 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
231
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
233
232
  },
234
233
  fixable: 'code',
235
234
  schema: [],
@@ -281,9 +280,11 @@ const rule: Rule.RuleModule = {
281
280
  // fetch request argument
282
281
  const methodNode = fixtureFunction.property; // get/put/etc.
283
282
  assert.ok(methodNode.type === 'Identifier');
283
+ const methodName = methodNode.name.toUpperCase();
284
+
284
285
  const fetchRequestArgumentLines = [
285
286
  '{',
286
- ` method: '${methodNode.name.toUpperCase()}',`,
287
+ ` method: '${methodName === 'DEL' ? 'DELETE' : methodName}',`,
287
288
  ...(fixtureCallInformation.requestBody
288
289
  ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
289
290
  : []),
@@ -23,7 +23,7 @@ const rule = createRule({
23
23
  description: 'Remove the usage of FullResponse type.',
24
24
  },
25
25
  messages: {
26
- removeFullResponse: 'Remove the usage of FullResponse type.',
26
+ removeFullResponse: 'Removing the usage of FullResponse type.',
27
27
  unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
28
28
  },
29
29
  fixable: 'code',
@@ -210,7 +210,7 @@ const rule = createRule({
210
210
 
211
211
  const fetchText = [
212
212
  `fetch(${replacedUrl}, {`,
213
- ` method: '${method.toUpperCase()}',`,
213
+ ` method: '${method.toLowerCase() === 'del' ? 'DELETE' : method.toUpperCase()}',`,
214
214
  ...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []),
215
215
  ...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []),
216
216
  '})',
package/src/index.ts CHANGED
@@ -18,6 +18,9 @@ import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-
18
18
  import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method';
19
19
  import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper';
20
20
  import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code';
21
+ import requireResolveFullResponse, {
22
+ ruleId as requireResolveFullResponseRuleId,
23
+ } from './require-resolve-full-response';
21
24
  import filePathComment from './file-path-comment';
22
25
  import noCardNumbers from './no-card-numbers';
23
26
  import noTestImport from './no-test-import';
@@ -49,6 +52,7 @@ export default {
49
52
  [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter,
50
53
  [addUrlDomainRuleId]: addUrlDomain,
51
54
  [noFullResponseRuleId]: noFullResponse,
55
+ [requireResolveFullResponseRuleId]: requireResolveFullResponse,
52
56
  },
53
57
  configs: {
54
58
  all: {
@@ -64,6 +68,15 @@ export default {
64
68
  '@checkdigit/no-test-import': 'error',
65
69
  [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error',
66
70
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
71
+ [`@checkdigit/${noFullResponseRuleId}`]: 'error',
72
+ [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error',
73
+ [`@checkdigit/${addUrlDomainRuleId}`]: 'off',
74
+ [`@checkdigit/${noFixtureRuleId}`]: 'off',
75
+ [`@checkdigit/${noServiceWrapperRuleId}`]: 'off',
76
+ [`@checkdigit/${noStatusCodeRuleId}`]: 'off',
77
+ [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off',
78
+ [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off',
79
+ [`@checkdigit/${fetchThenRuleId}`]: 'off',
67
80
  },
68
81
  },
69
82
  recommended: {
@@ -81,16 +94,31 @@ export default {
81
94
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
82
95
  },
83
96
  },
84
- agent: {
97
+ 'agent-serve-runtime': {
98
+ ignorePatterns: ['*.spec.ts', '*.test.ts'],
85
99
  rules: {
86
- [`@checkdigit/${noFixtureRuleId}`]: 'error',
87
- [`@checkdigit/${fetchThenRuleId}`]: 'error',
100
+ [`@checkdigit/${noFullResponseRuleId}`]: 'error',
101
+ [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error',
102
+ [`@checkdigit/${addUrlDomainRuleId}`]: 'error',
103
+ [`@checkdigit/${noFixtureRuleId}`]: 'off',
88
104
  [`@checkdigit/${noServiceWrapperRuleId}`]: 'error',
89
105
  [`@checkdigit/${noStatusCodeRuleId}`]: 'error',
90
106
  [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error',
91
107
  [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error',
92
- [`@checkdigit/${addUrlDomainRuleId}`]: 'error',
108
+ [`@checkdigit/${fetchThenRuleId}`]: 'error',
109
+ },
110
+ },
111
+ 'agent-fixture': {
112
+ rules: {
93
113
  [`@checkdigit/${noFullResponseRuleId}`]: 'error',
114
+ [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error',
115
+ [`@checkdigit/${addUrlDomainRuleId}`]: 'error',
116
+ [`@checkdigit/${noFixtureRuleId}`]: 'error',
117
+ [`@checkdigit/${noServiceWrapperRuleId}`]: 'error',
118
+ [`@checkdigit/${noStatusCodeRuleId}`]: 'error',
119
+ [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error',
120
+ [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error',
121
+ [`@checkdigit/${fetchThenRuleId}`]: 'error',
94
122
  },
95
123
  },
96
124
  },
@@ -0,0 +1,199 @@
1
+ // require-resolve-full-response.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 { DefinitionType, type Scope } from '@typescript-eslint/scope-manager';
11
+ import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP } from './agent/url';
12
+ import { strict as assert } from 'node:assert';
13
+ import getDocumentationUrl from './get-documentation-url';
14
+ import { getEnclosingScopeNode } from './library/ts-tree';
15
+
16
+ export const ruleId = 'require-resolve-full-response';
17
+
18
+ const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
19
+
20
+ const rule = createRule({
21
+ name: ruleId,
22
+ meta: {
23
+ type: 'suggestion',
24
+ docs: {
25
+ description: 'Prefer native fetch over customized service wrapper.',
26
+ },
27
+ messages: {
28
+ invalidOptions:
29
+ '"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.',
30
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
31
+ },
32
+ schema: [],
33
+ },
34
+ defaultOptions: [],
35
+ create(context) {
36
+ const sourceCode = context.sourceCode;
37
+ const scopeManager = sourceCode.scopeManager;
38
+ const parserService = ESLintUtils.getParserServices(context);
39
+ const typeChecker = parserService.program.getTypeChecker();
40
+
41
+ function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) {
42
+ if (
43
+ (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === 'string') ||
44
+ urlArgument?.type === AST_NODE_TYPES.TemplateLiteral
45
+ ) {
46
+ const urlText = sourceCode.getText(urlArgument);
47
+ return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText);
48
+ }
49
+
50
+ if (urlArgument?.type === AST_NODE_TYPES.Identifier) {
51
+ const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name);
52
+ if (foundVariable) {
53
+ const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable);
54
+ assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`);
55
+ const variableDefinitionNode = variableDefinition.node;
56
+ assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator);
57
+ assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property');
58
+ return isUrlArgumentValid(variableDefinitionNode.init, scope);
59
+ }
60
+ }
61
+
62
+ return false;
63
+ }
64
+
65
+ function getType(identifier: TSESTree.Identifier) {
66
+ const variable = parserService.esTreeNodeToTSNodeMap.get(identifier);
67
+ const variableType = typeChecker.getTypeAtLocation(variable);
68
+ return typeChecker.typeToString(variableType);
69
+ }
70
+
71
+ function isServiceLikeName(name: string) {
72
+ return /.*[Ss]ervice$/u.test(name);
73
+ }
74
+
75
+ function isCalleeServiceWrapper(serviceCall: TSESTree.CallExpression) {
76
+ const callee = serviceCall.callee;
77
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) {
78
+ return false;
79
+ }
80
+
81
+ const endpoint = callee.object;
82
+ if (endpoint.type === AST_NODE_TYPES.Identifier) {
83
+ return getType(endpoint) === 'Endpoint' || isServiceLikeName(endpoint.name);
84
+ }
85
+ if (endpoint.type !== AST_NODE_TYPES.CallExpression) {
86
+ return false;
87
+ }
88
+
89
+ const [contextArgument] = endpoint.arguments;
90
+ if (contextArgument?.type !== AST_NODE_TYPES.Identifier) {
91
+ return false;
92
+ }
93
+ if (contextArgument.name !== 'EMPTY_CONTEXT' && getType(contextArgument) !== 'InboundContext') {
94
+ return false;
95
+ }
96
+ const service = endpoint.callee;
97
+ if (service.type === AST_NODE_TYPES.Identifier) {
98
+ return getType(service) === 'ResolvedService';
99
+ }
100
+
101
+ if (service.type !== AST_NODE_TYPES.MemberExpression) {
102
+ return false;
103
+ }
104
+ const services = service.object;
105
+ if (services.type === AST_NODE_TYPES.Identifier) {
106
+ return getType(services) === 'ResolvedServices';
107
+ }
108
+
109
+ if (services.type !== AST_NODE_TYPES.MemberExpression) {
110
+ return false;
111
+ }
112
+ const configuration = services.object;
113
+ if (configuration.type === AST_NODE_TYPES.Identifier) {
114
+ return ['Configuration', 'Configuration<ResolvedServices>'].includes(getType(configuration));
115
+ }
116
+
117
+ // following applies only to test code (fixture)
118
+ if (configuration.type !== AST_NODE_TYPES.MemberExpression) {
119
+ return false;
120
+ }
121
+ const fixture = configuration.object;
122
+ if (fixture.type === AST_NODE_TYPES.Identifier) {
123
+ return fixture.name === 'fixture' || getType(fixture) === 'Fixture';
124
+ }
125
+
126
+ return false;
127
+ }
128
+
129
+ return {
130
+ 'CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]': (
131
+ serviceCall: TSESTree.CallExpression,
132
+ ) => {
133
+ try {
134
+ if (!isCalleeServiceWrapper(serviceCall)) {
135
+ return;
136
+ }
137
+
138
+ const enclosingScopeNode = getEnclosingScopeNode(serviceCall);
139
+ assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined');
140
+ const scope = scopeManager?.acquire(enclosingScopeNode);
141
+ assert.ok(scope, 'scope is undefined');
142
+ const urlArgument = serviceCall.arguments[0];
143
+ if (!isUrlArgumentValid(urlArgument, scope)) {
144
+ return;
145
+ }
146
+
147
+ assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression);
148
+ assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier);
149
+
150
+ // method
151
+ const method = serviceCall.callee.property.name;
152
+
153
+ // options
154
+ const optionsArgument = ['get', 'head', 'del'].includes(method)
155
+ ? serviceCall.arguments[1]
156
+ : serviceCall.arguments[2];
157
+ if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) {
158
+ context.report({
159
+ node: serviceCall,
160
+ messageId: 'invalidOptions',
161
+ });
162
+ return;
163
+ }
164
+
165
+ const resolveWithFullResponseProperty = optionsArgument.properties.find(
166
+ (property) =>
167
+ property.type === AST_NODE_TYPES.Property &&
168
+ property.key.type === AST_NODE_TYPES.Identifier &&
169
+ property.key.name === 'resolveWithFullResponse',
170
+ );
171
+ if (
172
+ resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property ||
173
+ resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal ||
174
+ resolveWithFullResponseProperty.value.value !== true
175
+ ) {
176
+ context.report({
177
+ node: optionsArgument,
178
+ messageId: 'invalidOptions',
179
+ });
180
+ return;
181
+ }
182
+ } catch (error) {
183
+ // eslint-disable-next-line no-console
184
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
185
+ context.report({
186
+ node: serviceCall,
187
+ messageId: 'unknownError',
188
+ data: {
189
+ fileName: context.filename,
190
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
191
+ },
192
+ });
193
+ }
194
+ },
195
+ };
196
+ },
197
+ });
198
+
199
+ export default rule;