@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceQueryKeyTs = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * Rule to enforce the use of centralized router state key constants imported from
8
+ * `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
9
+ * router methods that accept key parameters.
10
+ */
11
+ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
12
+ name: 'enforce-querykey-ts',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: 'Enforce using centralized router state key constants from queryKeys.ts for useRouterState key parameter',
17
+ recommended: 'error',
18
+ },
19
+ fixable: 'code',
20
+ schema: [],
21
+ messages: {
22
+ enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "@/util/routing/queryKeys", "src/util/routing/queryKeys", or a relative path ending in "/util/routing/queryKeys"). Use a QUERY_KEY_* constant instead of string literals.',
23
+ enforceQueryKeyConstant: 'Router state key must use a QUERY_KEY_* constant from queryKeys.ts. Variable "{{variableName}}" is not imported from the correct source.',
24
+ },
25
+ },
26
+ defaultOptions: [],
27
+ create(context) {
28
+ // Track imports from queryKeys.ts
29
+ const queryKeyImports = new Map();
30
+ const localUseRouterStateNames = new Set(['useRouterState']);
31
+ const validQueryKeySources = new Set([
32
+ '@/util/routing/queryKeys',
33
+ 'src/util/routing/queryKeys',
34
+ ]);
35
+ const allowedQueryKeyFactories = new Set(['makeQueryKey', 'getQueryKey']);
36
+ /**
37
+ * Ensure a suggested constant is imported from queryKeys.ts and return combined fixes
38
+ */
39
+ function buildImportFixes(fixer, keyNode, suggestedConstant) {
40
+ return [fixer.replaceText(keyNode, suggestedConstant)];
41
+ }
42
+ /**
43
+ * Check if a source path refers to queryKeys.ts
44
+ */
45
+ function isQueryKeysSource(source) {
46
+ return (validQueryKeySources.has(source) ||
47
+ source.endsWith('/util/routing/queryKeys'));
48
+ }
49
+ /**
50
+ * Check if an identifier is a valid QUERY_KEY constant
51
+ */
52
+ function isValidQueryKeyConstant(name) {
53
+ return name.startsWith('QUERY_KEY_');
54
+ }
55
+ /**
56
+ * Track variable assignments to detect variables derived from query key constants
57
+ */
58
+ const variableAssignments = new Map();
59
+ /**
60
+ * Check if a node represents a valid query key usage
61
+ */
62
+ function isValidQueryKeyUsage(node) {
63
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
64
+ const importInfo = queryKeyImports.get(node.name);
65
+ if (importInfo && isQueryKeysSource(importInfo.source)) {
66
+ return isValidQueryKeyConstant(importInfo.imported);
67
+ }
68
+ // Check if it's a variable derived from a query key constant
69
+ const assignment = variableAssignments.get(node.name);
70
+ if (assignment) {
71
+ return isValidQueryKeyUsage(assignment);
72
+ }
73
+ }
74
+ // Allow member expressions accessing query key constants
75
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
76
+ const member = node;
77
+ if (member.object.type === utils_1.AST_NODE_TYPES.Identifier &&
78
+ !member.computed &&
79
+ member.property.type === utils_1.AST_NODE_TYPES.Identifier) {
80
+ const importInfo = queryKeyImports.get(member.object.name);
81
+ if (importInfo && isQueryKeysSource(importInfo.source)) {
82
+ if (importInfo.imported === '*') {
83
+ return isValidQueryKeyConstant(member.property.name);
84
+ }
85
+ return isValidQueryKeyConstant(member.property.name);
86
+ }
87
+ }
88
+ }
89
+ // Allow template literals only when they contain no static content and all expressions are valid
90
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
91
+ const hasSignificantStaticPart = node.quasis.some((quasi) => {
92
+ const content = quasi.value.raw.trim();
93
+ return content.length > 0 && !/^[-_:/.]+$/.test(content);
94
+ });
95
+ if (node.expressions.length === 0) {
96
+ // Pure static template acts like a string literal
97
+ return false;
98
+ }
99
+ if (hasSignificantStaticPart) {
100
+ return false;
101
+ }
102
+ return node.expressions.some((expr) => isValidQueryKeyUsage(expr));
103
+ }
104
+ if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
105
+ node.operator === '+') {
106
+ return (isValidQueryKeyUsage(node.left) || isValidQueryKeyUsage(node.right));
107
+ }
108
+ // Allow conditional expressions if both branches use valid query keys
109
+ if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
110
+ return (isValidQueryKeyUsage(node.consequent) &&
111
+ isValidQueryKeyUsage(node.alternate));
112
+ }
113
+ // Allow function calls that might return query keys
114
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
115
+ const callee = node.callee;
116
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
117
+ return allowedQueryKeyFactories.has(callee.name);
118
+ }
119
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
120
+ !callee.computed &&
121
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
122
+ return allowedQueryKeyFactories.has(callee.property.name);
123
+ }
124
+ return false;
125
+ }
126
+ return false;
127
+ }
128
+ /**
129
+ * Check if a node contains string literals that should be reported
130
+ */
131
+ function containsInvalidStringLiteral(node) {
132
+ // Direct string literal
133
+ if (node.type === utils_1.AST_NODE_TYPES.Literal &&
134
+ typeof node.value === 'string') {
135
+ return true;
136
+ }
137
+ // String concatenation with + operator containing literals
138
+ if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
139
+ node.operator === '+') {
140
+ return (containsInvalidStringLiteral(node.left) ||
141
+ containsInvalidStringLiteral(node.right));
142
+ }
143
+ // Conditional (ternary) expression with string literals
144
+ if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
145
+ return (containsInvalidStringLiteral(node.consequent) ||
146
+ containsInvalidStringLiteral(node.alternate));
147
+ }
148
+ // Template literal handling
149
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
150
+ const hasSignificantStaticPart = node.quasis.some((quasi) => {
151
+ const content = quasi.value.raw.trim();
152
+ return content.length > 0 && !/^[-_:/.]+$/.test(content);
153
+ });
154
+ if (node.expressions.length === 0) {
155
+ // Pure static template behaves like a string literal
156
+ return hasSignificantStaticPart;
157
+ }
158
+ // Any meaningful static content makes this invalid regardless of expressions
159
+ if (hasSignificantStaticPart) {
160
+ return true;
161
+ }
162
+ // Only dynamic parts remain; all expressions must be valid query key usages
163
+ return !node.expressions.every((expr) => isValidQueryKeyUsage(expr));
164
+ }
165
+ return false;
166
+ }
167
+ /**
168
+ * Generate auto-fix suggestion for string literals
169
+ */
170
+ function generateAutoFix(keyValue) {
171
+ // Simple heuristic to suggest query key constant names
172
+ const normalizedKey = keyValue
173
+ .toUpperCase()
174
+ .replace(/[^A-Z0-9]/g, '_')
175
+ .replace(/_+/g, '_')
176
+ .replace(/^_|_$/g, '');
177
+ return `QUERY_KEY_${normalizedKey}`;
178
+ }
179
+ return {
180
+ // Track imports from queryKeys.ts
181
+ ImportDeclaration(node) {
182
+ if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
183
+ typeof node.source.value === 'string') {
184
+ const source = node.source.value;
185
+ if (isQueryKeysSource(source)) {
186
+ node.specifiers.forEach((spec) => {
187
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
188
+ const imported = spec.imported.name;
189
+ const local = spec.local.name;
190
+ queryKeyImports.set(local, { source, imported });
191
+ }
192
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
193
+ const local = spec.local.name;
194
+ queryKeyImports.set(local, { source, imported: '*' });
195
+ }
196
+ });
197
+ }
198
+ node.specifiers.forEach((spec) => {
199
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
200
+ spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
201
+ spec.imported.name === 'useRouterState') {
202
+ localUseRouterStateNames.add(spec.local.name);
203
+ }
204
+ });
205
+ }
206
+ },
207
+ // Track variable declarations that might derive from query key constants
208
+ VariableDeclarator(node) {
209
+ if (node.id.type === utils_1.AST_NODE_TYPES.Identifier && node.init) {
210
+ variableAssignments.set(node.id.name, node.init);
211
+ }
212
+ },
213
+ AssignmentExpression(node) {
214
+ if (node.left.type === utils_1.AST_NODE_TYPES.Identifier && node.right) {
215
+ variableAssignments.set(node.left.name, node.right);
216
+ }
217
+ },
218
+ // Check useRouterState calls
219
+ CallExpression(node) {
220
+ // Check if this is a call to useRouterState
221
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
222
+ localUseRouterStateNames.has(node.callee.name)) {
223
+ // Check if there are arguments
224
+ if (node.arguments.length > 0) {
225
+ const firstArg = node.arguments[0];
226
+ // Check if the first argument is an object expression
227
+ if (firstArg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
228
+ // Find the key property in the object
229
+ const keyProperty = firstArg.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
230
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
231
+ prop.key.name === 'key');
232
+ // If key property exists, check its value
233
+ if (keyProperty && keyProperty.value) {
234
+ const keyValue = keyProperty.value;
235
+ // Check if it's a valid query key usage
236
+ if (!isValidQueryKeyUsage(keyValue)) {
237
+ // Check if it contains invalid string literals
238
+ if (containsInvalidStringLiteral(keyValue)) {
239
+ context.report({
240
+ node: keyValue,
241
+ messageId: 'enforceQueryKeyImport',
242
+ fix(fixer) {
243
+ // Only provide auto-fix for simple string literals
244
+ if (keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
245
+ typeof keyValue.value === 'string') {
246
+ const suggestedConstant = generateAutoFix(keyValue.value);
247
+ if (suggestedConstant) {
248
+ return buildImportFixes(fixer, keyValue, suggestedConstant);
249
+ }
250
+ }
251
+ return null;
252
+ },
253
+ });
254
+ }
255
+ else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier) {
256
+ // Report variables that aren't from the correct source
257
+ context.report({
258
+ node: keyValue,
259
+ messageId: 'enforceQueryKeyConstant',
260
+ data: {
261
+ variableName: keyValue.name,
262
+ },
263
+ });
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ },
271
+ };
272
+ },
273
+ });
274
+ //# sourceMappingURL=enforce-querykey-ts.js.map
@@ -26,6 +26,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.enforceSingularTypeNames = void 0;
27
27
  const createRule_1 = require("../utils/createRule");
28
28
  const pluralize = __importStar(require("pluralize"));
29
+ const NON_PLURALIZABLE_SUFFIXES = [
30
+ 'Props',
31
+ 'Params',
32
+ 'Options',
33
+ 'Settings',
34
+ 'Data',
35
+ ];
29
36
  exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
30
37
  create(context) {
31
38
  /**
@@ -37,11 +44,8 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
37
44
  // Skip checking if name is too short (less than 3 characters)
38
45
  if (name.length < 3)
39
46
  return false;
40
- // Skip checking if name ends with 'Props' or 'Params'
41
- if (name.endsWith('Props') ||
42
- name.endsWith('Params') ||
43
- name.endsWith('Options') ||
44
- name.endsWith('Settings'))
47
+ // Skip checking if name ends with 'Props', 'Params', 'Data', etc.
48
+ if (NON_PLURALIZABLE_SUFFIXES.some((suffix) => name.toLowerCase().endsWith(suffix.toLowerCase())))
45
49
  return false;
46
50
  // Skip checking if name is already singular according to pluralize
47
51
  if (pluralize.isSingular(name))
@@ -0,0 +1 @@
1
+ export declare const enforceTypescriptMarkdownCodeBlocks: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"missingLanguageSpecifier", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceTypescriptMarkdownCodeBlocks = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ const FENCE = '```';
6
+ function isFenceLine(text, fenceIndex, indent) {
7
+ const lineEnd = text.indexOf('\n', fenceIndex);
8
+ const afterFence = lineEnd === -1 ? text.slice(fenceIndex + FENCE.length) : text.slice(fenceIndex + FENCE.length, lineEnd);
9
+ return afterFence.trim().length === 0 && text.slice(text.lastIndexOf('\n', fenceIndex - 1) + 1, fenceIndex) === indent;
10
+ }
11
+ function findClosingFence(text, startIndex, indent) {
12
+ let searchIndex = startIndex;
13
+ while (searchIndex < text.length) {
14
+ const candidate = text.indexOf(FENCE, searchIndex);
15
+ if (candidate === -1) {
16
+ return null;
17
+ }
18
+ if (isFenceLine(text, candidate, indent)) {
19
+ return candidate;
20
+ }
21
+ searchIndex = candidate + FENCE.length;
22
+ }
23
+ return null;
24
+ }
25
+ function isIndentOnly(value) {
26
+ return /^[\t ]*$/.test(value);
27
+ }
28
+ exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
29
+ name: 'enforce-typescript-markdown-code-blocks',
30
+ meta: {
31
+ type: 'suggestion',
32
+ docs: {
33
+ description: 'Ensure Markdown fenced code blocks without a language specifier default to typescript for consistent highlighting.',
34
+ recommended: 'error',
35
+ },
36
+ fixable: 'code',
37
+ schema: [],
38
+ messages: {
39
+ missingLanguageSpecifier: 'Code block starting on line {{line}} has no language label. Unlabeled fences render as plain text, hiding TypeScript syntax and type cues. Add "typescript" after the opening backticks (```typescript) to keep documentation readable and consistent.',
40
+ },
41
+ },
42
+ defaultOptions: [],
43
+ create(context) {
44
+ const filename = context.getFilename();
45
+ if (!filename.toLowerCase().endsWith('.md')) {
46
+ return {};
47
+ }
48
+ return {
49
+ Program() {
50
+ const sourceCode = context.getSourceCode();
51
+ const text = sourceCode.getText();
52
+ let index = 0;
53
+ while (index < text.length) {
54
+ const openingFence = text.indexOf(FENCE, index);
55
+ if (openingFence === -1) {
56
+ break;
57
+ }
58
+ const lineStart = text.lastIndexOf('\n', openingFence - 1) + 1;
59
+ const indent = text.slice(lineStart, openingFence);
60
+ if (!isIndentOnly(indent)) {
61
+ index = openingFence + FENCE.length;
62
+ continue;
63
+ }
64
+ const lineEnd = text.indexOf('\n', openingFence + FENCE.length);
65
+ if (lineEnd === -1) {
66
+ break;
67
+ }
68
+ const infoString = text.slice(openingFence + FENCE.length, lineEnd);
69
+ const closingFence = findClosingFence(text, lineEnd + 1, indent);
70
+ if (closingFence === null) {
71
+ index = lineEnd + 1;
72
+ continue;
73
+ }
74
+ const content = text.slice(lineEnd + 1, closingFence);
75
+ const hasContent = content.trim().length > 0;
76
+ const hasLanguage = infoString.trim().length > 0;
77
+ if (!hasLanguage && hasContent) {
78
+ const locStart = sourceCode.getLocFromIndex(openingFence);
79
+ const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
80
+ context.report({
81
+ loc: {
82
+ start: locStart,
83
+ end: sourceCode.getLocFromIndex(lineEnd),
84
+ },
85
+ messageId: 'missingLanguageSpecifier',
86
+ data: { line: locStart.line },
87
+ fix: (fixer) => fixer.replaceTextRange([openingFence + FENCE.length, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
88
+ });
89
+ }
90
+ index = closingFence + FENCE.length;
91
+ }
92
+ },
93
+ };
94
+ },
95
+ });
96
+ //# sourceMappingURL=enforce-typescript-markdown-code-blocks.js.map
@@ -4600,18 +4600,86 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4600
4600
  }
4601
4601
  return false;
4602
4602
  }
4603
+ function bodyContainsJsx(node) {
4604
+ const stack = [node];
4605
+ while (stack.length) {
4606
+ const current = stack.pop();
4607
+ if (current.type === utils_1.AST_NODE_TYPES.JSXElement ||
4608
+ current.type === utils_1.AST_NODE_TYPES.JSXFragment) {
4609
+ return true;
4610
+ }
4611
+ for (const key of Object.keys(current)) {
4612
+ if (key === 'parent')
4613
+ continue;
4614
+ const value = current[key];
4615
+ if (Array.isArray(value)) {
4616
+ for (const child of value) {
4617
+ if (child && typeof child.type === 'string') {
4618
+ stack.push(child);
4619
+ }
4620
+ }
4621
+ }
4622
+ else if (value && typeof value.type === 'string') {
4623
+ stack.push(value);
4624
+ }
4625
+ }
4626
+ }
4627
+ return false;
4628
+ }
4603
4629
  function hasJsxReturn(node) {
4604
- const sourceCode = context.getSourceCode();
4605
- const text = sourceCode.getText(node);
4606
- // Check for direct JSX returns
4607
- if (text.includes('return <') || text.includes('=> <')) {
4608
- return true;
4630
+ /**
4631
+ * Recursively checks if an expression looks like JSX
4632
+ */
4633
+ function isJsxExpression(expr) {
4634
+ if (!expr)
4635
+ return false;
4636
+ // Handle parenthesized expressions
4637
+ if (expr.type === 'ParenthesizedExpression') {
4638
+ return isJsxExpression(expr.expression);
4639
+ }
4640
+ switch (expr.type) {
4641
+ case utils_1.AST_NODE_TYPES.JSXElement:
4642
+ case utils_1.AST_NODE_TYPES.JSXFragment:
4643
+ return true;
4644
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
4645
+ return (isJsxExpression(expr.consequent) ||
4646
+ isJsxExpression(expr.alternate));
4647
+ case utils_1.AST_NODE_TYPES.LogicalExpression:
4648
+ // e.g. condition && <div />
4649
+ return isJsxExpression(expr.right);
4650
+ default:
4651
+ return false;
4652
+ }
4609
4653
  }
4610
- // Check for JSX assigned to variables then returned
4611
- if (text.includes('const ') &&
4612
- text.includes('<') &&
4613
- text.includes('return')) {
4614
- return true;
4654
+ /**
4655
+ * Recursively checks if a statement/body returns JSX
4656
+ */
4657
+ function checkBodyForJsx(body) {
4658
+ if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
4659
+ for (const stmt of body.body) {
4660
+ if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
4661
+ if (isJsxExpression(stmt.argument))
4662
+ return true;
4663
+ }
4664
+ else if (stmt.type === utils_1.AST_NODE_TYPES.IfStatement) {
4665
+ if (checkBodyForJsx(stmt.consequent))
4666
+ return true;
4667
+ if (stmt.alternate && checkBodyForJsx(stmt.alternate))
4668
+ return true;
4669
+ }
4670
+ // We could traverse more (try/catch, switch), but this covers most idiomatic React
4671
+ }
4672
+ }
4673
+ else {
4674
+ // Implicit return (arrow function expression body)
4675
+ return isJsxExpression(body);
4676
+ }
4677
+ return false;
4678
+ }
4679
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
4680
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
4681
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
4682
+ return checkBodyForJsx(node.body);
4615
4683
  }
4616
4684
  return false;
4617
4685
  }
@@ -4664,6 +4732,13 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4664
4732
  // 5. Component name ends with "Unmemoized" (common React pattern)
4665
4733
  functionName && functionName.endsWith('Unmemoized'),
4666
4734
  ];
4735
+ // If the function has a PascalCase name AND returns JSX, it's definitely a React component
4736
+ if (functionName && isPascalCase(functionName) && hasJsxReturn(node)) {
4737
+ return true;
4738
+ }
4739
+ if (functionName && isPascalCase(functionName) && bodyContainsJsx(node)) {
4740
+ return true;
4741
+ }
4667
4742
  // Consider it a React component if it matches at least 2 indicators
4668
4743
  // OR if it has the Unmemoized suffix (strong indicator of a React component)
4669
4744
  return (indicators.filter(Boolean).length >= 2 ||
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractGlobalConstants = void 0;
4
4
  const ASTHelpers_1 = require("../utils/ASTHelpers");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const utils_1 = require("@typescript-eslint/utils");
6
7
  function isInsideFunction(node) {
7
8
  let current = node;
8
9
  while (current) {
@@ -19,53 +20,61 @@ function isFunctionDefinition(node) {
19
20
  return (node?.type === 'FunctionExpression' ||
20
21
  node?.type === 'ArrowFunctionExpression');
21
22
  }
22
- function isImmutableValue(node) {
23
+ const unwrapOnce = (node) => {
23
24
  if (!node)
24
- return false;
25
+ return null;
26
+ const maybeParen = node;
27
+ if (maybeParen.type === 'ParenthesizedExpression') {
28
+ return maybeParen.expression ?? null;
29
+ }
25
30
  switch (node.type) {
26
- case 'Literal':
27
- return true;
28
- case 'TemplateLiteral':
29
- return node.expressions.length === 0;
30
- case 'UnaryExpression':
31
- return isImmutableValue(node.argument);
32
- case 'BinaryExpression':
33
- if (node.left.type === 'PrivateIdentifier')
34
- return false;
35
- return isImmutableValue(node.left) && isImmutableValue(node.right);
31
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
32
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
33
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
34
+ case utils_1.AST_NODE_TYPES.ChainExpression:
35
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
36
+ return node.expression;
36
37
  default:
37
- return false;
38
+ return null;
38
39
  }
40
+ };
41
+ function unwrapExpression(node) {
42
+ let current = node;
43
+ while (current) {
44
+ const inner = unwrapOnce(current);
45
+ if (!inner)
46
+ break;
47
+ current = inner;
48
+ }
49
+ return current;
39
50
  }
40
51
  function isMutableValue(node) {
41
- if (!node)
52
+ const unwrapped = unwrapExpression(node);
53
+ if (!unwrapped)
54
+ return false;
55
+ // If explicitly marked readonly with `as const`, treat as immutable
56
+ if (node && isAsConstExpression(node)) {
42
57
  return false;
58
+ }
43
59
  // Check for JSX elements (always mutable due to props/context)
44
- if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
60
+ if (unwrapped.type === 'JSXElement' || unwrapped.type === 'JSXFragment') {
45
61
  return true;
46
62
  }
47
63
  // Check for object expressions (always mutable)
48
- if (node.type === 'ObjectExpression') {
64
+ if (unwrapped.type === 'ObjectExpression') {
49
65
  return true;
50
66
  }
51
- // Check array literals - mutable if empty or if they contain mutable values
52
- if (node.type === 'ArrayExpression') {
53
- // Empty arrays are mutable since they can be modified later
54
- if (node.elements.length === 0)
55
- return true;
56
- // Arrays with spread elements are mutable
57
- if (node.elements.some((element) => !element || element.type === 'SpreadElement'))
58
- return true;
59
- // Arrays with non-immutable values are mutable
60
- return node.elements.some((element) => !isImmutableValue(element));
67
+ // Arrays are mutable objects unless explicitly narrowed with `as const`.
68
+ if (unwrapped.type === 'ArrayExpression') {
69
+ return true;
61
70
  }
62
71
  // Check for new expressions (e.g., new Map(), new Set())
63
- if (node.type === 'NewExpression') {
72
+ if (unwrapped.type === 'NewExpression') {
64
73
  return true;
65
74
  }
66
75
  // Check for array/object methods that return mutable values
67
- if (node.type === 'CallExpression') {
68
- const callee = node.callee;
76
+ if (unwrapped.type === 'CallExpression') {
77
+ const callee = unwrapped.callee;
69
78
  if (callee.type === 'MemberExpression') {
70
79
  // Handle both Identifier and non-Identifier property nodes
71
80
  if (callee.property.type !== 'Identifier') {
@@ -103,14 +112,15 @@ function isZeroOrOne(node) {
103
112
  return value === 0 || value === 1;
104
113
  }
105
114
  function isAsConstExpression(node) {
106
- if (!node)
107
- return false;
108
- if (node.type === 'TSAsExpression') {
109
- if (node.typeAnnotation.type === 'TSTypeReference' &&
110
- node.typeAnnotation.typeName.type === 'Identifier' &&
111
- node.typeAnnotation.typeName.name === 'const') {
115
+ let current = node;
116
+ while (current) {
117
+ if (current.type === 'TSAsExpression' &&
118
+ current.typeAnnotation.type === 'TSTypeReference' &&
119
+ current.typeAnnotation.typeName.type === 'Identifier' &&
120
+ current.typeAnnotation.typeName.name === 'const') {
112
121
  return true;
113
122
  }
123
+ current = unwrapOnce(current);
114
124
  }
115
125
  return false;
116
126
  }
@@ -134,16 +144,18 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
134
144
  if (!hasDependencies &&
135
145
  !hasAsConstAssertion &&
136
146
  (scope.type === 'function' || scope.type === 'block') &&
137
- isInsideFunction(node)) {
138
- const constName = node.declarations[0].id
139
- .name;
140
- context.report({
141
- node,
142
- messageId: 'extractGlobalConstants',
143
- data: {
144
- declarationName: constName,
145
- },
146
- });
147
+ isInsideFunction(node) &&
148
+ node.declarations.some((d) => d.id.type === 'Identifier')) {
149
+ for (const d of node.declarations) {
150
+ if (d.id.type !== 'Identifier')
151
+ continue;
152
+ const constName = d.id.name;
153
+ context.report({
154
+ node: d,
155
+ messageId: 'extractGlobalConstants',
156
+ data: { declarationName: constName },
157
+ });
158
+ }
147
159
  }
148
160
  },
149
161
  FunctionDeclaration(node) {