@blumintinc/eslint-plugin-blumint 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +170 -139
  2. package/lib/index.js +190 -1
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  5. package/lib/rules/consistent-callback-naming.js +1 -1
  6. package/lib/rules/dynamic-https-errors.js +3 -4
  7. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
  9. package/lib/rules/enforce-callback-memo.js +1 -1
  10. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  11. package/lib/rules/enforce-console-error.js +35 -7
  12. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  13. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  14. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  15. package/lib/rules/enforce-early-destructuring.js +980 -0
  16. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  17. package/lib/rules/enforce-empty-object-check.js +502 -0
  18. package/lib/rules/enforce-exported-function-types.js +1 -1
  19. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  20. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  21. package/lib/rules/enforce-firestore-facade.js +4 -2
  22. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  23. package/lib/rules/enforce-global-constants.js +1 -1
  24. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  25. package/lib/rules/enforce-memoize-async.js +2 -6
  26. package/lib/rules/enforce-memoize-getters.js +39 -15
  27. package/lib/rules/enforce-microdiff.js +1 -1
  28. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  29. package/lib/rules/enforce-positive-naming.js +4 -0
  30. package/lib/rules/enforce-props-argument-name.js +2 -2
  31. package/lib/rules/enforce-safe-stringify.js +1 -1
  32. package/lib/rules/enforce-serializable-params.js +3 -3
  33. package/lib/rules/enforce-singular-type-names.js +1 -1
  34. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  35. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  36. package/lib/rules/enforce-storage-context.d.ts +9 -0
  37. package/lib/rules/enforce-storage-context.js +646 -0
  38. package/lib/rules/enforce-timestamp-now.js +23 -7
  39. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  40. package/lib/rules/enforce-transform-memoization.js +416 -0
  41. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  42. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  43. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  44. package/lib/rules/enforce-verb-noun-naming.js +5 -4
  45. package/lib/rules/ensure-pointer-events-none.js +28 -2
  46. package/lib/rules/export-if-in-doubt.js +27 -4
  47. package/lib/rules/extract-global-constants.js +40 -13
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  49. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  50. package/lib/rules/flatten-push-calls.d.ts +2 -0
  51. package/lib/rules/flatten-push-calls.js +428 -0
  52. package/lib/rules/global-const-style.js +50 -19
  53. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  54. package/lib/rules/jsdoc-above-field.js +208 -0
  55. package/lib/rules/key-only-outermost-element.js +21 -3
  56. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  57. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  58. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  59. package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
  60. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  61. package/lib/rules/memo-nested-react-components.js +410 -0
  62. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  63. package/lib/rules/no-always-true-false-conditions.js +68 -8
  64. package/lib/rules/no-array-length-in-deps.js +9 -3
  65. package/lib/rules/no-async-foreach.js +210 -12
  66. package/lib/rules/no-circular-references.js +18 -17
  67. package/lib/rules/no-class-instance-destructuring.js +55 -12
  68. package/lib/rules/no-complex-cloud-params.js +7 -3
  69. package/lib/rules/no-compositing-layer-props.js +2 -2
  70. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  71. package/lib/rules/no-console-error.d.ts +9 -0
  72. package/lib/rules/no-console-error.js +525 -0
  73. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  74. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  75. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  76. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  77. package/lib/rules/no-entire-object-hook-deps.js +4 -6
  78. package/lib/rules/no-excessive-parent-chain.js +1 -1
  79. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  80. package/lib/rules/no-explicit-return-type.js +215 -26
  81. package/lib/rules/no-filter-without-return.js +5 -1
  82. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  83. package/lib/rules/no-firestore-jest-mock.js +126 -8
  84. package/lib/rules/no-firestore-object-arrays.js +67 -12
  85. package/lib/rules/no-handler-suffix.d.ts +12 -0
  86. package/lib/rules/no-handler-suffix.js +305 -0
  87. package/lib/rules/no-hungarian.js +1 -1
  88. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  89. package/lib/rules/no-inline-component-prop.js +456 -0
  90. package/lib/rules/no-jsx-in-hooks.js +6 -1
  91. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  92. package/lib/rules/no-margin-properties.js +6 -6
  93. package/lib/rules/no-memoize-on-static.js +9 -1
  94. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  95. package/lib/rules/no-misused-switch-case.js +22 -1
  96. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  97. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  98. package/lib/rules/no-mock-firebase-admin.js +5 -2
  99. package/lib/rules/no-object-values-on-strings.js +10 -2
  100. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  101. package/lib/rules/no-passthrough-getters.js +38 -2
  102. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  103. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  104. package/lib/rules/no-redundant-param-types.js +16 -6
  105. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  106. package/lib/rules/no-redundant-this-params.js +459 -0
  107. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  108. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  109. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  110. package/lib/rules/no-restricted-properties-fix.js +11 -10
  111. package/lib/rules/no-separate-loading-state.js +7 -16
  112. package/lib/rules/no-stale-state-across-await.js +1 -1
  113. package/lib/rules/no-type-assertion-returns.js +43 -49
  114. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  115. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  116. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  117. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  118. package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
  119. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  120. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  121. package/lib/rules/no-unpinned-dependencies.js +36 -5
  122. package/lib/rules/no-unused-props.d.ts +2 -2
  123. package/lib/rules/no-unused-props.js +295 -91
  124. package/lib/rules/no-unused-usestate.js +6 -2
  125. package/lib/rules/no-useless-fragment.js +28 -2
  126. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  127. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  128. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  129. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  130. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  131. package/lib/rules/omit-index-html.d.ts +2 -1
  132. package/lib/rules/omit-index-html.js +62 -7
  133. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  134. package/lib/rules/parallelize-async-operations.js +24 -5
  135. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  136. package/lib/rules/prefer-batch-operations.js +32 -5
  137. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  138. package/lib/rules/prefer-clone-deep.js +3 -3
  139. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  140. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  141. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  142. package/lib/rules/prefer-docsetter-setall.js +243 -0
  143. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  144. package/lib/rules/prefer-fragment-component.js +1 -1
  145. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  146. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  147. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  148. package/lib/rules/prefer-global-router-state-key.js +88 -13
  149. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  150. package/lib/rules/prefer-memoized-props.js +445 -0
  151. package/lib/rules/prefer-next-dynamic.js +60 -69
  152. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
  153. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  154. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  155. package/lib/rules/prefer-params-over-parent-id.js +220 -171
  156. package/lib/rules/prefer-settings-object.js +2 -2
  157. package/lib/rules/prefer-type-over-interface.js +7 -2
  158. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  159. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  160. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  161. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  162. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  163. package/lib/rules/prevent-children-clobber.js +536 -0
  164. package/lib/rules/react-memoize-literals.d.ts +4 -0
  165. package/lib/rules/react-memoize-literals.js +495 -0
  166. package/lib/rules/react-usememo-should-be-component.js +2 -2
  167. package/lib/rules/require-hooks-default-params.js +17 -16
  168. package/lib/rules/require-https-error-cause.d.ts +4 -0
  169. package/lib/rules/require-https-error-cause.js +136 -0
  170. package/lib/rules/require-https-error.js +43 -21
  171. package/lib/rules/require-image-optimized.js +1 -1
  172. package/lib/rules/require-memo.js +1 -1
  173. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  174. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  175. package/lib/rules/require-usememo-object-literals.js +2 -3
  176. package/lib/rules/test-file-location-enforcement.js +1 -1
  177. package/lib/rules/use-latest-callback.js +6 -5
  178. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  179. package/lib/rules/vertically-group-related-functions.js +480 -0
  180. package/lib/utils/ASTHelpers.d.ts +13 -1
  181. package/lib/utils/ASTHelpers.js +24 -0
  182. package/lib/utils/getMethodName.d.ts +27 -0
  183. package/lib/utils/getMethodName.js +35 -0
  184. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  185. package/lib/utils/tsTypeClassifier.js +149 -0
  186. package/package.json +3 -1
@@ -0,0 +1,521 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireHttpsErrorInOnRequestHandlers = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
7
+ const ON_REQUEST_MODULE = 'functions/src/v2/https/onRequest';
8
+ const HTTPS_ERROR_IMPORT_PATH = 'functions/src/util/errors/HttpsError';
9
+ const isFunctionLike = (node) => !!node &&
10
+ (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
11
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
12
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
13
+ const isIdentifierWithName = (node, name) => !!node && node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === name;
14
+ const unwrapTypedExpression = (expression) => {
15
+ let current = expression;
16
+ while (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
17
+ current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
18
+ current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
19
+ current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
20
+ current.type === utils_1.AST_NODE_TYPES.TSInstantiationExpression) {
21
+ current = current.expression;
22
+ }
23
+ return current;
24
+ };
25
+ const getResponseParamNames = (node) => {
26
+ const RESPONSE_PARAM_KEYS = new Set(['res', 'response', 'resp']);
27
+ const extractName = (param) => {
28
+ if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
29
+ return { name: param.name, isExplicitResponseBinding: false };
30
+ }
31
+ if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
32
+ param.left.type === utils_1.AST_NODE_TYPES.Identifier) {
33
+ return { name: param.left.name, isExplicitResponseBinding: false };
34
+ }
35
+ if (param.type === utils_1.AST_NODE_TYPES.RestElement &&
36
+ param.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
37
+ return { name: param.argument.name, isExplicitResponseBinding: false };
38
+ }
39
+ if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
40
+ for (const property of param.properties) {
41
+ if (property.type !== utils_1.AST_NODE_TYPES.Property || property.computed) {
42
+ continue;
43
+ }
44
+ const keyName = property.key.type === utils_1.AST_NODE_TYPES.Identifier
45
+ ? property.key.name
46
+ : property.key.type === utils_1.AST_NODE_TYPES.Literal &&
47
+ typeof property.key.value === 'string'
48
+ ? property.key.value
49
+ : null;
50
+ if (!keyName || !RESPONSE_PARAM_KEYS.has(keyName.toLowerCase())) {
51
+ continue;
52
+ }
53
+ if (property.value.type === utils_1.AST_NODE_TYPES.Identifier) {
54
+ return { name: property.value.name, isExplicitResponseBinding: true };
55
+ }
56
+ if (property.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
57
+ property.value.left.type === utils_1.AST_NODE_TYPES.Identifier) {
58
+ return {
59
+ name: property.value.left.name,
60
+ isExplicitResponseBinding: true,
61
+ };
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ if (param.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
67
+ for (const element of param.elements) {
68
+ if (!element) {
69
+ continue;
70
+ }
71
+ const name = element.type === utils_1.AST_NODE_TYPES.Identifier
72
+ ? element.name
73
+ : element.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
74
+ element.left.type === utils_1.AST_NODE_TYPES.Identifier
75
+ ? element.left.name
76
+ : element.type === utils_1.AST_NODE_TYPES.RestElement &&
77
+ element.argument.type === utils_1.AST_NODE_TYPES.Identifier
78
+ ? element.argument.name
79
+ : null;
80
+ if (name && RESPONSE_PARAM_KEYS.has(name.toLowerCase())) {
81
+ return { name, isExplicitResponseBinding: false };
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ return null;
87
+ };
88
+ return node.params.reduce((acc, param, index) => {
89
+ const extracted = extractName(param);
90
+ if (!extracted)
91
+ return acc;
92
+ const lowered = extracted.name.toLowerCase();
93
+ const likelyResponse = extracted.isExplicitResponseBinding || RESPONSE_PARAM_KEYS.has(lowered);
94
+ /**
95
+ * In typical Express handlers, the response param is the second argument (req, res),
96
+ * so we treat the second parameter as a likely response name even if it doesn't match
97
+ * 'res', 'response', or 'resp'.
98
+ */
99
+ if (likelyResponse || index === 1) {
100
+ acc.push(extracted.name);
101
+ }
102
+ return acc;
103
+ }, []);
104
+ };
105
+ const getStatusCodeFromArg = (arg) => {
106
+ if (!arg || arg.type === utils_1.AST_NODE_TYPES.SpreadElement) {
107
+ return null;
108
+ }
109
+ if (arg.type === utils_1.AST_NODE_TYPES.Literal && typeof arg.value === 'number') {
110
+ return arg.value;
111
+ }
112
+ return null;
113
+ };
114
+ const mapStatusToHttpsCode = (status) => {
115
+ if (status === 400)
116
+ return 'invalid-argument';
117
+ if (status === 401)
118
+ return 'unauthenticated';
119
+ if (status === 403)
120
+ return 'permission-denied';
121
+ if (status === 404)
122
+ return 'not-found';
123
+ if (status === 409)
124
+ return 'already-exists';
125
+ if (status === 412 || status === 422)
126
+ return 'failed-precondition';
127
+ if (status === 429)
128
+ return 'resource-exhausted';
129
+ if (status === 499 || status === 504)
130
+ return 'deadline-exceeded';
131
+ if (status === 500)
132
+ return 'internal';
133
+ if (status === 502 || status === 503)
134
+ return 'unavailable';
135
+ return 'unknown';
136
+ };
137
+ const findNearestFunction = (ancestors) => {
138
+ for (let idx = ancestors.length - 1; idx >= 0; idx -= 1) {
139
+ const ancestor = ancestors[idx];
140
+ if (isFunctionLike(ancestor)) {
141
+ return ancestor;
142
+ }
143
+ }
144
+ return null;
145
+ };
146
+ const getMessageExample = (call) => {
147
+ const firstArg = call.arguments[0];
148
+ if (firstArg &&
149
+ firstArg.type === utils_1.AST_NODE_TYPES.Literal &&
150
+ typeof firstArg.value === 'string') {
151
+ return firstArg.value;
152
+ }
153
+ if (firstArg &&
154
+ firstArg.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
155
+ firstArg.expressions.length === 0) {
156
+ return firstArg.quasis.map((part) => part.value.raw).join('');
157
+ }
158
+ return 'provide a descriptive message';
159
+ };
160
+ const getObjectNameFromExpression = (node) => {
161
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
162
+ return node.name;
163
+ }
164
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
165
+ !node.computed &&
166
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
167
+ node.object.type === utils_1.AST_NODE_TYPES.Identifier) {
168
+ return `${node.object.name}.${node.property.name}`;
169
+ }
170
+ return null;
171
+ };
172
+ const getCalleeDisplayName = (callee) => {
173
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
174
+ return callee.name;
175
+ }
176
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
177
+ !callee.computed &&
178
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
179
+ const objectName = getObjectNameFromExpression(callee.object);
180
+ if (objectName) {
181
+ return `${objectName}.${callee.property.name}`;
182
+ }
183
+ return callee.property.name;
184
+ }
185
+ return 'helper';
186
+ };
187
+ const getIdentifierNameFromExpression = (expression) => {
188
+ if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
189
+ return expression.name;
190
+ }
191
+ if (expression.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
192
+ return null;
193
+ }
194
+ const unwrapped = unwrapTypedExpression(expression);
195
+ return unwrapped.type === utils_1.AST_NODE_TYPES.Identifier ? unwrapped.name : null;
196
+ };
197
+ const isStatusMember = (member, propertyName) => {
198
+ if (member.computed ||
199
+ member.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
200
+ member.property.name !== propertyName) {
201
+ return null;
202
+ }
203
+ const responseName = getIdentifierNameFromExpression(member.object);
204
+ return responseName ? { responseName } : null;
205
+ };
206
+ const getStatusInfoFromStatusCall = (call) => {
207
+ if (call.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
208
+ const statusMember = isStatusMember(call.callee, 'status');
209
+ if (!statusMember) {
210
+ return null;
211
+ }
212
+ return {
213
+ responseName: statusMember.responseName,
214
+ statusCode: getStatusCodeFromArg(call.arguments[0]),
215
+ };
216
+ }
217
+ return null;
218
+ };
219
+ const findStatusCall = (node) => {
220
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
221
+ const sendStatusMember = isStatusMember(node.callee, 'sendStatus');
222
+ if (sendStatusMember) {
223
+ return {
224
+ responseName: sendStatusMember.responseName,
225
+ statusCode: getStatusCodeFromArg(node.arguments[0]),
226
+ method: 'sendStatus',
227
+ };
228
+ }
229
+ }
230
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
231
+ const statusMember = isStatusMember(node.callee, 'status');
232
+ if (statusMember) {
233
+ const parent = node.parent;
234
+ if (parent &&
235
+ parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
236
+ parent.object === node &&
237
+ parent.parent &&
238
+ parent.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
239
+ return null;
240
+ }
241
+ return {
242
+ responseName: statusMember.responseName,
243
+ statusCode: getStatusCodeFromArg(node.arguments[0]),
244
+ method: 'status',
245
+ };
246
+ }
247
+ }
248
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
249
+ node.callee.object.type === utils_1.AST_NODE_TYPES.CallExpression) {
250
+ const statusInfo = getStatusInfoFromStatusCall(node.callee.object);
251
+ if (!statusInfo) {
252
+ return null;
253
+ }
254
+ const method = node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
255
+ ? node.callee.property.name
256
+ : 'status';
257
+ return {
258
+ ...statusInfo,
259
+ method,
260
+ };
261
+ }
262
+ return null;
263
+ };
264
+ const isErrorStatus = (statusCode) => statusCode === null || statusCode >= 400;
265
+ exports.requireHttpsErrorInOnRequestHandlers = (0, createRule_1.createRule)({
266
+ name: 'no-res-error-status-in-onrequest',
267
+ meta: {
268
+ type: 'problem',
269
+ docs: {
270
+ description: 'Forbid sending 4xx/5xx Express responses inside onRequest handlers; throw structured HttpsError instances instead so the wrapper can format, log, and map errors consistently.',
271
+ recommended: 'error',
272
+ },
273
+ schema: [],
274
+ messages: {
275
+ useHttpsErrorForStatus: "HTTP status {{statusCode}} sent via `{{responseName}}.{{method}}` inside an onRequest handler bypasses the centralized HttpsError pipeline → This creates observability gaps (errors won't appear in structured logs), inconsistent client error shapes (breaking API contracts), and missing error context (details object unavailable for debugging). → Throw new HttpsError('{{httpsCode}}', '{{messageExample}}', details) instead and import HttpsError from {{httpsErrorImport}} so the wrapper captures context, logs structured data, and returns consistent error responses.",
276
+ useHttpsErrorForComputedStatus: "Computed HTTP status sent via `{{responseName}}.{{method}}` inside an onRequest handler cannot be mapped to the expected HttpsError codes → This bypasses the HttpsError pipeline, leading to inconsistent error logging and unstandardized response shapes. → Choose an explicit HttpsError code (e.g., 'invalid-argument', 'permission-denied') and throw new HttpsError('{{httpsCode}}', '{{messageExample}}', details) after importing from {{httpsErrorImport}} so the wrapper can standardize logging, capture error context, and maintain a consistent API contract.",
277
+ useHttpsErrorForWrapper: "`{{calleeName}}` receives the Express response object `{{responseName}}` inside an onRequest handler, which writes HTTP errors directly and skips the HttpsError wrapper → This results in missing structured logs, inconsistent error responses for clients, and makes debugging difficult due to lost error context. → Refactor this helper to throw new HttpsError('{{httpsCode}}', '{{messageExample}}', details) (import from {{httpsErrorImport}}) so the onRequest wrapper can handle formatting, status mapping, and centralized logging.",
278
+ },
279
+ },
280
+ defaultOptions: [],
281
+ create(context) {
282
+ const onRequestIdentifiers = new Set();
283
+ const onRequestNamespaces = new Set();
284
+ const handlerFunctions = new Set();
285
+ const responseNamesByFunction = new Map();
286
+ const pendingViolations = [];
287
+ const recordFunction = (node) => {
288
+ responseNamesByFunction.set(node, getResponseParamNames(node));
289
+ };
290
+ const extractFunctionFromDefinition = (def) => {
291
+ const defNode = def.node;
292
+ if (isFunctionLike(defNode)) {
293
+ return defNode;
294
+ }
295
+ if (defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator && defNode.init) {
296
+ const initializer = unwrapTypedExpression(defNode.init);
297
+ if (isFunctionLike(initializer)) {
298
+ return initializer;
299
+ }
300
+ }
301
+ return null;
302
+ };
303
+ const resolveFunctionFromIdentifier = (identifier) => {
304
+ let scope = context.getScope();
305
+ while (scope) {
306
+ const variable = scope.set.get(identifier.name) ??
307
+ scope.variables.find((candidate) => candidate.name === identifier.name);
308
+ if (variable) {
309
+ for (const def of variable.defs) {
310
+ const fn = extractFunctionFromDefinition(def);
311
+ if (fn) {
312
+ return fn;
313
+ }
314
+ }
315
+ }
316
+ scope = scope.upper;
317
+ }
318
+ return null;
319
+ };
320
+ const isDirectOnRequestHandler = (fn) => {
321
+ return handlerFunctions.has(fn);
322
+ };
323
+ function findInFunctionChain(startNode, callback) {
324
+ let current = startNode;
325
+ while (current) {
326
+ if (isFunctionLike(current)) {
327
+ const result = callback(current);
328
+ if (result) {
329
+ return result;
330
+ }
331
+ }
332
+ current = current.parent;
333
+ }
334
+ return null;
335
+ }
336
+ const isWithinOnRequest = (fn) => !!findInFunctionChain(fn, (current) => isDirectOnRequestHandler(current));
337
+ const hasResponseBinding = (fn, responseName) => !!findInFunctionChain(fn, (current) => {
338
+ const params = responseNamesByFunction.get(current) ?? [];
339
+ return params.includes(responseName);
340
+ });
341
+ const findResponseIdentifierInArgs = (fn, call) => findInFunctionChain(fn, (current) => {
342
+ const params = responseNamesByFunction.get(current) ?? [];
343
+ return params.find((name) => call.arguments.some((arg) => {
344
+ const unwrapped = unwrapTypedExpression(arg);
345
+ return isIdentifierWithName(unwrapped, name);
346
+ }));
347
+ });
348
+ const handleOnRequestTracking = (node) => {
349
+ const isOnRequestCallee = (callee) => {
350
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
351
+ return onRequestIdentifiers.has(callee.name);
352
+ }
353
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
354
+ !callee.computed &&
355
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
356
+ callee.property.name === 'onRequest' &&
357
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier) {
358
+ return onRequestNamespaces.has(callee.object.name);
359
+ }
360
+ return false;
361
+ };
362
+ if (isOnRequestCallee(node.callee)) {
363
+ const handlerArg = [...node.arguments]
364
+ .reverse()
365
+ .find((arg) => arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
366
+ arg.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
367
+ arg.type === utils_1.AST_NODE_TYPES.Identifier ||
368
+ arg.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
369
+ arg.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
370
+ arg.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
371
+ arg.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
372
+ arg.type === utils_1.AST_NODE_TYPES.TSInstantiationExpression);
373
+ if (handlerArg) {
374
+ const expression = unwrapTypedExpression(handlerArg);
375
+ if (isFunctionLike(expression)) {
376
+ handlerFunctions.add(expression);
377
+ }
378
+ else if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
379
+ const resolvedHandler = resolveFunctionFromIdentifier(expression);
380
+ if (resolvedHandler) {
381
+ handlerFunctions.add(resolvedHandler);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ };
387
+ const handleStatusViolation = (node) => {
388
+ const statusInfo = findStatusCall(node);
389
+ if (!statusInfo) {
390
+ return false;
391
+ }
392
+ const { responseName, statusCode, method } = statusInfo;
393
+ if (!isErrorStatus(statusCode)) {
394
+ return true;
395
+ }
396
+ const functionNode = findNearestFunction(ASTHelpers_1.ASTHelpers.getAncestors(context, node));
397
+ if (!functionNode) {
398
+ return true;
399
+ }
400
+ if (!hasResponseBinding(functionNode, responseName)) {
401
+ return true;
402
+ }
403
+ pendingViolations.push({
404
+ node,
405
+ functionNode,
406
+ responseName,
407
+ kind: statusCode === null ? 'computedStatus' : 'status',
408
+ statusCode,
409
+ method,
410
+ messageExample: getMessageExample(node),
411
+ });
412
+ return true;
413
+ };
414
+ const handleWrapperViolation = (node) => {
415
+ const functionNode = findNearestFunction(ASTHelpers_1.ASTHelpers.getAncestors(context, node));
416
+ if (!functionNode) {
417
+ return;
418
+ }
419
+ const responseName = findResponseIdentifierInArgs(functionNode, node);
420
+ /**
421
+ * Skip direct method calls on the response object (e.g., res.send(res)) so the
422
+ * wrapper detection only reports helpers that receive the response object as
423
+ * an argument.
424
+ */
425
+ const calleeUsesResponseDirectly = node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
426
+ node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
427
+ responseName === node.callee.object.name;
428
+ if (!responseName || calleeUsesResponseDirectly) {
429
+ return;
430
+ }
431
+ pendingViolations.push({
432
+ node,
433
+ functionNode,
434
+ responseName,
435
+ kind: 'wrapper',
436
+ statusCode: null,
437
+ method: 'wrapper',
438
+ messageExample: getMessageExample(node),
439
+ calleeName: getCalleeDisplayName(node.callee),
440
+ });
441
+ };
442
+ return {
443
+ ImportDeclaration(node) {
444
+ if (node.source.value !== ON_REQUEST_MODULE) {
445
+ return;
446
+ }
447
+ node.specifiers.forEach((specifier) => {
448
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
449
+ onRequestIdentifiers.add(specifier.local.name);
450
+ }
451
+ else if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
452
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
453
+ specifier.imported.name === 'onRequest') {
454
+ onRequestIdentifiers.add(specifier.local.name);
455
+ }
456
+ else if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
457
+ onRequestNamespaces.add(specifier.local.name);
458
+ }
459
+ });
460
+ },
461
+ FunctionDeclaration: recordFunction,
462
+ FunctionExpression: recordFunction,
463
+ ArrowFunctionExpression: recordFunction,
464
+ CallExpression(node) {
465
+ handleOnRequestTracking(node);
466
+ if (handleStatusViolation(node)) {
467
+ return;
468
+ }
469
+ handleWrapperViolation(node);
470
+ },
471
+ 'Program:exit'() {
472
+ pendingViolations.forEach((violation) => {
473
+ if (!isWithinOnRequest(violation.functionNode)) {
474
+ return;
475
+ }
476
+ const httpsCode = mapStatusToHttpsCode(violation.statusCode);
477
+ const commonData = {
478
+ responseName: violation.responseName,
479
+ messageExample: violation.messageExample,
480
+ httpsCode,
481
+ httpsErrorImport: HTTPS_ERROR_IMPORT_PATH,
482
+ };
483
+ if (violation.kind === 'wrapper') {
484
+ context.report({
485
+ node: violation.node,
486
+ messageId: 'useHttpsErrorForWrapper',
487
+ data: {
488
+ ...commonData,
489
+ calleeName: violation.calleeName ?? 'helper',
490
+ },
491
+ });
492
+ return;
493
+ }
494
+ if (violation.kind === 'computedStatus') {
495
+ context.report({
496
+ node: violation.node,
497
+ messageId: 'useHttpsErrorForComputedStatus',
498
+ data: {
499
+ ...commonData,
500
+ method: violation.method,
501
+ },
502
+ });
503
+ return;
504
+ }
505
+ context.report({
506
+ node: violation.node,
507
+ messageId: 'useHttpsErrorForStatus',
508
+ data: {
509
+ ...commonData,
510
+ method: violation.method,
511
+ statusCode: violation.statusCode !== null
512
+ ? violation.statusCode.toString()
513
+ : 'computed status',
514
+ },
515
+ });
516
+ });
517
+ },
518
+ };
519
+ },
520
+ });
521
+ //# sourceMappingURL=no-res-error-status-in-onrequest.js.map
@@ -37,7 +37,7 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
37
37
  },
38
38
  ],
39
39
  messages: {
40
- restrictedProperty: "Disallowed object property: '{{objectName}}.{{propertyName}}'{{message}}",
40
+ restrictedProperty: 'Access to "{{objectName}}.{{propertyName}}" is restricted. {{restrictionReason}}Restricted properties often bypass safer APIs, hide side effects, or encourage patterns this codebase forbids. Use the allowed alternative from your rule configuration or remove this property access.',
41
41
  },
42
42
  },
43
43
  defaultOptions: [[]],
@@ -56,6 +56,13 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
56
56
  'slice',
57
57
  'concat',
58
58
  ]);
59
+ /**
60
+ * Keeps the templated message readable by only adding a trailing space
61
+ * when a restriction reason is provided.
62
+ */
63
+ function formatRestrictionReason(message) {
64
+ return message ? `${message} ` : '';
65
+ }
59
66
  /**
60
67
  * Checks if the given node is a result of Object.keys() or Object.values()
61
68
  * @param node The node to check
@@ -109,9 +116,7 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
109
116
  data: {
110
117
  objectName: restrictedProp.object,
111
118
  propertyName: restrictedProp.property,
112
- message: restrictedProp.message
113
- ? `: ${restrictedProp.message}`
114
- : '',
119
+ restrictionReason: formatRestrictionReason(restrictedProp.message),
115
120
  },
116
121
  fix: () => null,
117
122
  });
@@ -135,9 +140,7 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
135
140
  data: {
136
141
  objectName,
137
142
  propertyName: restrictedProp.property,
138
- message: restrictedProp.message
139
- ? `: ${restrictedProp.message}`
140
- : '',
143
+ restrictionReason: formatRestrictionReason(restrictedProp.message),
141
144
  },
142
145
  fix: () => null,
143
146
  });
@@ -157,9 +160,7 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
157
160
  data: {
158
161
  objectName: restrictedProp.object,
159
162
  propertyName,
160
- message: restrictedProp.message
161
- ? `: ${restrictedProp.message}`
162
- : '',
163
+ restrictionReason: formatRestrictionReason(restrictedProp.message),
163
164
  },
164
165
  fix: () => null,
165
166
  });
@@ -29,7 +29,7 @@ exports.noSeparateLoadingState = (0, createRule_1.createRule)({
29
29
  },
30
30
  ],
31
31
  messages: {
32
- separateLoadingState: 'Avoid separate loading state. Encode loading status directly in the primary state using a sentinel value like "loading".',
32
+ separateLoadingState: 'Loading flag "{{stateName}}" splits the source of truth for data fetching. Boolean toggles drift from the actual data and add extra renders. Encode the loading phase inside the primary state instead (use a "loading" sentinel or discriminated union) so components read a single authoritative value.',
33
33
  },
34
34
  },
35
35
  defaultOptions: [{}],
@@ -63,16 +63,6 @@ exports.noSeparateLoadingState = (0, createRule_1.createRule)({
63
63
  }
64
64
  return false;
65
65
  }
66
- function getSetterName(declarator) {
67
- if (declarator.id.type === utils_1.AST_NODE_TYPES.ArrayPattern &&
68
- declarator.id.elements.length >= 2) {
69
- const setterElement = declarator.id.elements[1];
70
- if (setterElement?.type === utils_1.AST_NODE_TYPES.Identifier) {
71
- return setterElement.name;
72
- }
73
- }
74
- return null;
75
- }
76
66
  return {
77
67
  VariableDeclarator(node) {
78
68
  // Check for useState destructuring patterns
@@ -95,12 +85,13 @@ exports.noSeparateLoadingState = (0, createRule_1.createRule)({
95
85
  declarator: node,
96
86
  setterVar,
97
87
  usage: { truthy: false, falsy: false },
88
+ stateName: stateElement.name,
98
89
  });
99
90
  }
100
91
  }
101
92
  }
102
93
  },
103
- CallExpression(_node) {
94
+ CallExpression() {
104
95
  // Setter usage is resolved via scope references in Program:exit
105
96
  return;
106
97
  },
@@ -123,15 +114,15 @@ exports.noSeparateLoadingState = (0, createRule_1.createRule)({
123
114
  }
124
115
  }
125
116
  }
126
- const { declarator, usage } = tracker;
127
- const setterName = getSetterName(declarator);
128
- if (!setterName)
129
- continue;
117
+ const { declarator, usage, stateName } = tracker;
130
118
  // If we have both truthy and falsy setter calls, it's likely a loading state pattern
131
119
  if (usage.truthy && usage.falsy) {
132
120
  context.report({
133
121
  node: declarator,
134
122
  messageId: 'separateLoadingState',
123
+ data: {
124
+ stateName,
125
+ },
135
126
  });
136
127
  }
137
128
  }
@@ -22,7 +22,7 @@ exports.noStaleStateAcrossAwait = (0, createRule_1.createRule)({
22
22
  return {
23
23
  // Check functions for violations
24
24
  'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
25
- const sourceCode = context.getSourceCode();
25
+ const sourceCode = context.sourceCode;
26
26
  const scopeManager = sourceCode.scopeManager;
27
27
  if (!scopeManager)
28
28
  return;