@blumintinc/eslint-plugin-blumint 1.9.1 → 1.10.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.
package/README.md CHANGED
@@ -70,6 +70,7 @@ Or use the recommended config:
70
70
  | [enforce-callback-memo](docs/rules/enforce-callback-memo.md) | Enforce useCallback for inline functions and useMemo for objects/arrays containing functions in JSX props to prevent unnecessary re-renders. This improves React component performance by ensuring stable function references across renders and memoizing complex objects. | ✅ | | | |
71
71
  | [enforce-centralized-mock-firestore](docs/rules/enforce-centralized-mock-firestore.md) | Enforce usage of centralized mockFirestore from predefined location | ✅ | | 🔧 | |
72
72
  | [enforce-dynamic-firebase-imports](docs/rules/enforce-dynamic-firebase-imports.md) | Enforce dynamic importing for modules within the firebaseCloud directory to optimize initial bundle size. This ensures Firebase-related code is only loaded when needed, improving application startup time and reducing the main bundle size. | ✅ | | | |
73
+ | [enforce-dynamic-file-naming](docs/rules/enforce-dynamic-file-naming.md) | Enforce .dynamic.ts(x) file naming when @blumintinc/blumint/enforce-dynamic-imports rule is disabled. This improves code clarity and helps enforce consistent naming conventions across the codebase. | ✅ | | | |
73
74
  | [enforce-exported-function-types](docs/rules/enforce-exported-function-types.md) | Enforce exporting types for function props and return values | ✅ | | | |
74
75
  | [enforce-firestore-doc-ref-generic](docs/rules/enforce-firestore-doc-ref-generic.md) | Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup | ✅ | | | 💭 |
75
76
  | [enforce-firestore-facade](docs/rules/enforce-firestore-facade.md) | Enforce usage of Firestore facades instead of direct Firestore methods | ✅ | | | |
package/lib/index.js CHANGED
@@ -97,10 +97,13 @@ const omit_index_html_1 = require("./rules/omit-index-html");
97
97
  const enforce_id_capitalization_1 = require("./rules/enforce-id-capitalization");
98
98
  const no_unused_usestate_1 = require("./rules/no-unused-usestate");
99
99
  const no_uuidv4_base62_as_key_1 = require("./rules/no-uuidv4-base62-as-key");
100
+ const enforce_dynamic_file_naming_1 = __importDefault(require("./rules/enforce-dynamic-file-naming"));
101
+ const prefer_usecallback_over_usememo_for_functions_1 = __importDefault(require("./rules/prefer-usecallback-over-usememo-for-functions"));
102
+ const no_margin_properties_1 = require("./rules/no-margin-properties");
100
103
  module.exports = {
101
104
  meta: {
102
105
  name: '@blumintinc/eslint-plugin-blumint',
103
- version: '1.9.1',
106
+ version: '1.10.0',
104
107
  },
105
108
  parseOptions: {
106
109
  ecmaVersion: 2020,
@@ -209,6 +212,9 @@ module.exports = {
209
212
  '@blumintinc/blumint/omit-index-html': 'error',
210
213
  '@blumintinc/blumint/enforce-id-capitalization': 'error',
211
214
  '@blumintinc/blumint/no-unused-usestate': 'error',
215
+ '@blumintinc/blumint/enforce-dynamic-file-naming': 'error',
216
+ '@blumintinc/blumint/prefer-usecallback-over-usememo-for-functions': 'error',
217
+ '@blumintinc/blumint/no-margin-properties': 'error',
212
218
  },
213
219
  },
214
220
  },
@@ -238,6 +244,7 @@ module.exports = {
238
244
  'no-unused-props': no_unused_props_1.noUnusedProps,
239
245
  'no-useless-fragment': no_useless_fragment_1.noUselessFragment,
240
246
  'no-uuidv4-base62-as-key': no_uuidv4_base62_as_key_1.noUuidv4Base62AsKey,
247
+ 'enforce-dynamic-file-naming': enforce_dynamic_file_naming_1.default,
241
248
  'prefer-fragment-shorthand': prefer_fragment_shorthand_1.preferFragmentShorthand,
242
249
  'prefer-type-over-interface': prefer_type_over_interface_1.preferTypeOverInterface,
243
250
  'require-memo': require_memo_1.requireMemo,
@@ -307,6 +314,8 @@ module.exports = {
307
314
  'omit-index-html': omit_index_html_1.omitIndexHtml,
308
315
  'enforce-id-capitalization': enforce_id_capitalization_1.enforceIdCapitalization,
309
316
  'no-unused-usestate': no_unused_usestate_1.noUnusedUseState,
317
+ 'prefer-usecallback-over-usememo-for-functions': prefer_usecallback_over_usememo_for_functions_1.default,
318
+ 'no-margin-properties': no_margin_properties_1.noMarginProperties,
310
319
  },
311
320
  };
312
321
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
- type MessageIds = 'missingParamsType' | 'missingResponseType' | 'unusedParamsType' | 'unusedResponseType';
1
+ type MessageIds = 'missingParamsPropsType' | 'missingResponseType' | 'unusedParamsPropsType' | 'unusedResponseType';
2
2
  export declare const enforceCallableTypes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
3
  export {};
@@ -8,14 +8,14 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
8
8
  meta: {
9
9
  type: 'problem',
10
10
  docs: {
11
- description: 'Enforce Params and Response type exports in callable functions',
11
+ description: 'Enforce Props/Params and Response type exports in callable functions',
12
12
  recommended: 'error',
13
13
  },
14
14
  schema: [],
15
15
  messages: {
16
- missingParamsType: 'Missing Params type export in callable function file',
16
+ missingParamsPropsType: 'Missing Props or Params type export in callable function file',
17
17
  missingResponseType: 'Missing Response type export in callable function file',
18
- unusedParamsType: 'Params type is exported but not used in the callable function',
18
+ unusedParamsPropsType: 'Props or Params type is exported but not used in the callable function',
19
19
  unusedResponseType: 'Response type is exported but not used in the callable function',
20
20
  },
21
21
  },
@@ -28,18 +28,18 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
28
28
  filename.includes('/callable/scripts/')) {
29
29
  return {};
30
30
  }
31
- let hasParamsExport = false;
31
+ let hasParamsOrPropsExport = false;
32
32
  let hasResponseExport = false;
33
33
  let hasCallableFunction = false;
34
- let paramsTypeUsed = false;
34
+ let paramsOrPropsTypeUsed = false;
35
35
  let responseTypeUsed = false;
36
36
  return {
37
37
  // Check for type exports
38
38
  ExportNamedDeclaration(node) {
39
39
  if (node.declaration?.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
40
40
  const typeName = node.declaration.id.name;
41
- if (typeName === 'Params') {
42
- hasParamsExport = true;
41
+ if (typeName === 'Params' || typeName === 'Props') {
42
+ hasParamsOrPropsExport = true;
43
43
  }
44
44
  else if (typeName === 'Response') {
45
45
  hasResponseExport = true;
@@ -56,8 +56,8 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
56
56
  // Check for type usage in function parameters and return types
57
57
  TSTypeReference(node) {
58
58
  if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
59
- if (node.typeName.name === 'Params') {
60
- paramsTypeUsed = true;
59
+ if (node.typeName.name === 'Params' || node.typeName.name === 'Props') {
60
+ paramsOrPropsTypeUsed = true;
61
61
  }
62
62
  else if (node.typeName.name === 'Response') {
63
63
  responseTypeUsed = true;
@@ -68,16 +68,16 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
68
68
  if (!hasCallableFunction) {
69
69
  return;
70
70
  }
71
- if (!hasParamsExport) {
71
+ if (!hasParamsOrPropsExport) {
72
72
  context.report({
73
73
  loc: { line: 1, column: 0 },
74
- messageId: 'missingParamsType',
74
+ messageId: 'missingParamsPropsType',
75
75
  });
76
76
  }
77
- else if (!paramsTypeUsed) {
77
+ else if (!paramsOrPropsTypeUsed) {
78
78
  context.report({
79
79
  loc: { line: 1, column: 0 },
80
- messageId: 'unusedParamsType',
80
+ messageId: 'unusedParamsPropsType',
81
81
  });
82
82
  }
83
83
  if (!hasResponseExport) {
@@ -0,0 +1,3 @@
1
+ export declare const RULE_NAME = "enforce-dynamic-file-naming";
2
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireDynamicExtension" | "requireDisableDirective", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export default _default;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RULE_NAME = void 0;
7
+ const createRule_1 = require("../utils/createRule");
8
+ const path_1 = __importDefault(require("path"));
9
+ exports.RULE_NAME = 'enforce-dynamic-file-naming';
10
+ exports.default = (0, createRule_1.createRule)({
11
+ name: exports.RULE_NAME,
12
+ meta: {
13
+ type: 'suggestion',
14
+ docs: {
15
+ description: 'Enforce .dynamic.ts(x) file naming when @blumintinc/blumint/enforce-dynamic-imports rule is disabled',
16
+ recommended: 'error',
17
+ },
18
+ schema: [],
19
+ messages: {
20
+ requireDynamicExtension: 'Files with disabled @blumintinc/blumint/enforce-dynamic-imports rule must use .dynamic.ts(x) extension',
21
+ requireDisableDirective: 'Files with .dynamic.ts(x) extension must have at least one @blumintinc/blumint/enforce-dynamic-imports disable directive',
22
+ },
23
+ },
24
+ defaultOptions: [],
25
+ create(context) {
26
+ // Get the file path and name
27
+ const filePath = context.getFilename();
28
+ const fileName = path_1.default.basename(filePath);
29
+ // Check if the file is a TypeScript file (ends with .ts or .tsx)
30
+ // Ignore files with other extensions like .test.ts, .deprecated.ts, etc.
31
+ const isTypeScriptFile = /^[^.]+\.tsx?$/.test(fileName);
32
+ // Check if the file has .dynamic.ts or .dynamic.tsx extension
33
+ const hasDynamicExtension = /\.dynamic\.tsx?$/.test(fileName);
34
+ // Skip if not a TypeScript file or has other extensions
35
+ if (!isTypeScriptFile && !hasDynamicExtension) {
36
+ return {};
37
+ }
38
+ // Track if we found a disable directive for enforce-dynamic-imports
39
+ let foundDisableDirective = false;
40
+ return {
41
+ Program() {
42
+ // Get the source code
43
+ const sourceCode = context.getSourceCode();
44
+ const comments = sourceCode.getAllComments();
45
+ // Check all comments for disable directives
46
+ for (const comment of comments) {
47
+ const commentText = comment.value.trim();
48
+ // Check for inline disable directive
49
+ if ((commentText.includes('eslint-disable-next-line') ||
50
+ commentText.includes('ednl')) &&
51
+ commentText.includes('@blumintinc/blumint/enforce-dynamic-imports')) {
52
+ foundDisableDirective = true;
53
+ break;
54
+ }
55
+ // Check for block disable directive
56
+ if (commentText.includes('eslint-disable ') &&
57
+ commentText.includes('@blumintinc/blumint/enforce-dynamic-imports')) {
58
+ foundDisableDirective = true;
59
+ break;
60
+ }
61
+ }
62
+ // If we found a disable directive but the file doesn't have .dynamic extension
63
+ if (foundDisableDirective && !hasDynamicExtension) {
64
+ context.report({
65
+ loc: { line: 1, column: 0 },
66
+ messageId: 'requireDynamicExtension',
67
+ });
68
+ }
69
+ // If the file has .dynamic extension but no disable directive
70
+ if (hasDynamicExtension && !foundDisableDirective) {
71
+ context.report({
72
+ loc: { line: 1, column: 0 },
73
+ messageId: 'requireDisableDirective',
74
+ });
75
+ }
76
+ },
77
+ };
78
+ },
79
+ });
80
+ //# sourceMappingURL=enforce-dynamic-file-naming.js.map
@@ -61,6 +61,47 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
61
61
  node.parent.id === node) {
62
62
  return true;
63
63
  }
64
+ // Check if the node is in an object property context
65
+ if (node.parent &&
66
+ node.parent.type === utils_1.AST_NODE_TYPES.Property &&
67
+ node.parent.value === node) {
68
+ // Check if this is a property in an object pattern (destructuring)
69
+ let currentNode = node.parent;
70
+ while (currentNode.parent) {
71
+ if (currentNode.parent.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
72
+ return true;
73
+ }
74
+ currentNode = currentNode.parent;
75
+ }
76
+ }
77
+ // Check if the node is in a property assignment context
78
+ if (node.parent &&
79
+ node.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
80
+ return true;
81
+ }
82
+ // Check if the node is in a property access context
83
+ if (node.parent &&
84
+ node.parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
85
+ return true;
86
+ }
87
+ // Check if the node is a string literal in a type definition context
88
+ // This handles cases like Pick<Type, 'id' | 'name'>
89
+ if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
90
+ let currentNode = node;
91
+ while (currentNode.parent) {
92
+ // Check for TypeScript type contexts
93
+ if (currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeReference ||
94
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeParameterInstantiation ||
95
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSUnionType ||
96
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSIntersectionType ||
97
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
98
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
99
+ currentNode.parent.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
100
+ return true;
101
+ }
102
+ currentNode = currentNode.parent;
103
+ }
104
+ }
64
105
  return false;
65
106
  }
66
107
  /**
@@ -276,6 +276,92 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
276
276
  node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
277
277
  node.typeAnnotation.typeName.name === 'const');
278
278
  }
279
+ /**
280
+ * Check if a logical expression is being used for default value assignment
281
+ * This includes patterns like:
282
+ * - variable || defaultValue
283
+ * - variable ?? defaultValue
284
+ * - variable && expression
285
+ */
286
+ function isDefaultValueAssignment(node) {
287
+ // Check if this is a nullish coalescing operator (always used for defaults)
288
+ if (node.operator === '??') {
289
+ return true;
290
+ }
291
+ // Check if this is in a JSX attribute
292
+ if (node.parent &&
293
+ (node.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
294
+ node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
295
+ return true;
296
+ }
297
+ // Check if this is a variable assignment with a default value
298
+ if (node.parent &&
299
+ (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
300
+ node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression)) {
301
+ return true;
302
+ }
303
+ // Check if this is a function parameter default value
304
+ if (node.parent &&
305
+ node.parent.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
306
+ return true;
307
+ }
308
+ // Check if this is a return statement with a default value
309
+ if (node.parent &&
310
+ node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
311
+ return true;
312
+ }
313
+ // Check if this is a function argument (common for default values)
314
+ if (node.parent &&
315
+ (node.parent.type === utils_1.AST_NODE_TYPES.CallExpression ||
316
+ node.parent.type === utils_1.AST_NODE_TYPES.NewExpression)) {
317
+ return true;
318
+ }
319
+ // Check if this is part of an object or array expression (common for default values)
320
+ if (node.parent &&
321
+ (node.parent.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
322
+ node.parent.type === utils_1.AST_NODE_TYPES.ArrayExpression)) {
323
+ return true;
324
+ }
325
+ // Check if this is inside a property assignment in an object
326
+ if (node.parent &&
327
+ node.parent.type === utils_1.AST_NODE_TYPES.Property) {
328
+ return true;
329
+ }
330
+ // Check if this is inside a template literal expression
331
+ if (node.parent &&
332
+ node.parent.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
333
+ return true;
334
+ }
335
+ // Check if this is inside a template element
336
+ if (node.parent &&
337
+ node.parent.type === utils_1.AST_NODE_TYPES.TemplateElement) {
338
+ return true;
339
+ }
340
+ // Check if this is inside a spread element
341
+ if (node.parent &&
342
+ node.parent.type === utils_1.AST_NODE_TYPES.SpreadElement) {
343
+ return true;
344
+ }
345
+ // Check if this is inside an array method callback (map, filter, etc.)
346
+ if (node.parent &&
347
+ node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
348
+ node.parent.parent &&
349
+ node.parent.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
350
+ node.parent.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
351
+ const methodName = node.parent.parent.callee.property.type === utils_1.AST_NODE_TYPES.Identifier
352
+ ? node.parent.parent.callee.property.name
353
+ : '';
354
+ if (['map', 'filter', 'find', 'findIndex', 'some', 'every', 'forEach'].includes(methodName)) {
355
+ return true;
356
+ }
357
+ }
358
+ // Check if this is inside an arrow function body
359
+ if (node.parent &&
360
+ node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
361
+ return true;
362
+ }
363
+ return false;
364
+ }
279
365
  /**
280
366
  * Check logical expressions (&&, ||)
281
367
  */
@@ -285,6 +371,12 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
285
371
  // Add children to evaluated list to prevent duplicate evaluations
286
372
  evaluatedParentNodes.add(node.left);
287
373
  evaluatedParentNodes.add(node.right);
374
+ // Check if this is a default value assignment pattern
375
+ const isDefaultValuePattern = isDefaultValueAssignment(node);
376
+ if (isDefaultValuePattern) {
377
+ // Don't flag default value assignments as always true/false conditions
378
+ return {};
379
+ }
288
380
  // For &&: if either side is always falsy, the whole expression is falsy
289
381
  if (node.operator === '&&') {
290
382
  if (leftResult.isFalsy) {
@@ -724,6 +816,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
724
816
  }
725
817
  // Template literals
726
818
  if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
819
+ // Skip template literals that are likely used for string interpolation
820
+ // rather than conditions
821
+ if (node.parent &&
822
+ (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
823
+ node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
824
+ node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
825
+ node.parent.type === utils_1.AST_NODE_TYPES.Property ||
826
+ node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
827
+ return {};
828
+ }
727
829
  return checkTemplateLiteral(node);
728
830
  }
729
831
  // Type checking (typeof x === 'string')
@@ -1092,11 +1194,59 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1092
1194
  /**
1093
1195
  * Check if a condition is always truthy or falsy and report it
1094
1196
  */
1197
+ /**
1198
+ * Check if a ternary expression is being used for default value assignment
1199
+ * This includes patterns like:
1200
+ * - variable ? variable : defaultValue
1201
+ */
1202
+ function isDefaultValueTernary(node) {
1203
+ // Check if the condition and the consequent are the same variable
1204
+ // This is a common pattern for default values: status ? status : 'offline'
1205
+ if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
1206
+ node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
1207
+ node.test.name === node.consequent.name) {
1208
+ return true;
1209
+ }
1210
+ // Check if this is in a JSX attribute
1211
+ if (node.parent &&
1212
+ (node.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
1213
+ node.parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer)) {
1214
+ return true;
1215
+ }
1216
+ // Check if this is a variable assignment with a default value
1217
+ if (node.parent &&
1218
+ (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
1219
+ node.parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression)) {
1220
+ // Only if the condition and consequent are the same variable
1221
+ if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
1222
+ node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
1223
+ node.test.name === node.consequent.name) {
1224
+ return true;
1225
+ }
1226
+ }
1227
+ // Check if this is a return statement with a default value
1228
+ if (node.parent &&
1229
+ node.parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
1230
+ // Only if the condition and consequent are the same variable
1231
+ if (node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
1232
+ node.consequent.type === utils_1.AST_NODE_TYPES.Identifier &&
1233
+ node.test.name === node.consequent.name) {
1234
+ return true;
1235
+ }
1236
+ }
1237
+ return false;
1238
+ }
1095
1239
  function checkCondition(node) {
1096
1240
  // Skip if already reported or if it's a part of a larger expression that's been reported
1097
1241
  if (reportedNodes.has(node) || evaluatedParentNodes.has(node)) {
1098
1242
  return;
1099
1243
  }
1244
+ // Check if this is a ternary expression used for default values
1245
+ if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
1246
+ isDefaultValueTernary(node)) {
1247
+ // Skip checking ternaries used for default values
1248
+ return;
1249
+ }
1100
1250
  // We should NOT clear evaluatedParentNodes here as that can lead to duplicate evaluations
1101
1251
  // and miss detection of conditions in nested expressions
1102
1252
  // Check for nested expressions to avoid multiple errors
@@ -244,8 +244,10 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
244
244
  ['WeakMap', 'WeakSet', 'Promise', 'Error'].includes(node.object.name)) {
245
245
  return true;
246
246
  }
247
- // Check for circular references in member expressions
248
- return isComplexValue(node.object) || isComplexValue(node.property);
247
+ // For property access expressions, we only need to check if the object itself is complex
248
+ // We don't need to check the property, as accessing a property will result in a primitive value
249
+ // or another object that we'll check separately
250
+ return isComplexValue(node.object);
249
251
  }
250
252
  // Check for Symbols
251
253
  if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === 'Symbol') {
@@ -34,6 +34,17 @@ const TYPE_MARKERS = [
34
34
  'Type',
35
35
  'Enum',
36
36
  ];
37
+ // Allowed descriptive suffixes that should not be flagged as Hungarian notation
38
+ const ALLOWED_SUFFIXES = [
39
+ 'Formatted',
40
+ 'Parsed',
41
+ 'Processed',
42
+ 'Transformed',
43
+ 'Converted',
44
+ 'Rendered',
45
+ 'Display',
46
+ 'Displayed',
47
+ ];
37
48
  // Common built-in JavaScript prototype methods
38
49
  const BUILT_IN_METHODS = new Set([
39
50
  // String methods
@@ -197,6 +208,12 @@ exports.noHungarian = (0, createRule_1.createRule)({
197
208
  const checkedIdentifiers = new Set();
198
209
  // Check if a variable name contains a type marker with proper word boundaries
199
210
  function hasTypeMarker(variableName) {
211
+ // Check if the variable name ends with one of the allowed descriptive suffixes
212
+ if (ALLOWED_SUFFIXES.some(suffix => variableName.endsWith(suffix) &&
213
+ variableName.length > suffix.length &&
214
+ /[a-z]/.test(variableName[variableName.length - suffix.length - 1]))) {
215
+ return false;
216
+ }
200
217
  const normalizedVarName = variableName.toLowerCase();
201
218
  // Handle SCREAMING_SNAKE_CASE separately
202
219
  if (variableName === variableName.toUpperCase() &&
@@ -0,0 +1 @@
1
+ export declare const noMarginProperties: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noMarginProperties", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noMarginProperties = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ // Convert camelCase to kebab-case
7
+ function toKebabCase(str) {
8
+ return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
9
+ }
10
+ // Normalize property name to kebab-case for consistent lookup
11
+ function normalizePropertyName(name) {
12
+ // If already contains hyphens, assume it's kebab-case
13
+ if (name.includes('-'))
14
+ return name.toLowerCase();
15
+ // Convert camelCase to kebab-case
16
+ return toKebabCase(name).toLowerCase();
17
+ }
18
+ // List of margin properties to flag
19
+ const MARGIN_PROPERTIES = new Set([
20
+ 'margin',
21
+ 'margin-left',
22
+ 'margin-right',
23
+ 'margin-top',
24
+ 'margin-bottom',
25
+ 'marginleft',
26
+ 'marginright',
27
+ 'margintop',
28
+ 'marginbottom',
29
+ 'mx',
30
+ 'my',
31
+ 'mt',
32
+ 'mr',
33
+ 'mb',
34
+ 'ml',
35
+ 'm',
36
+ ]);
37
+ exports.noMarginProperties = (0, createRule_1.createRule)({
38
+ name: 'no-margin-properties',
39
+ meta: {
40
+ type: 'suggestion',
41
+ docs: {
42
+ description: 'Discourage using margin properties (margin, marginLeft, marginRight, marginTop, marginBottom, mx, my, etc.) for spacing in MUI components. Instead, prefer defining spacing with padding, gap, or the spacing prop for more predictable layouts.',
43
+ recommended: 'error',
44
+ },
45
+ schema: [],
46
+ messages: {
47
+ noMarginProperties: 'Avoid using {{property}} for spacing in MUI components. Use padding, gap, or the spacing prop instead for more predictable layouts. See https://www.youtube.com/watch?v=KVQMoEFUee8 for more details.',
48
+ },
49
+ },
50
+ defaultOptions: [],
51
+ create(context) {
52
+ const seenNodes = new WeakSet();
53
+ function checkProperty(propertyName) {
54
+ const normalizedName = normalizePropertyName(propertyName);
55
+ return MARGIN_PROPERTIES.has(normalizedName);
56
+ }
57
+ // Check if a node is within an sx prop context or theme override context
58
+ function isMuiStylingContext(node) {
59
+ let current = node;
60
+ while (current?.parent) {
61
+ // Check for JSX sx attribute (MUI specific)
62
+ if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
63
+ current.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
64
+ current.parent.name.name === 'sx') {
65
+ return true;
66
+ }
67
+ // Check for sx prop in object spread
68
+ if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
69
+ current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
70
+ current.parent.key.name === 'sx') {
71
+ return true;
72
+ }
73
+ // Check for theme overrides (MUI's createTheme)
74
+ if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
75
+ current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
76
+ (current.parent.key.name === 'styleOverrides' ||
77
+ current.parent.key.name === 'components')) {
78
+ return true;
79
+ }
80
+ // Check for MUI's css function
81
+ if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
82
+ current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
83
+ current.parent.callee.name === 'css') {
84
+ return true;
85
+ }
86
+ // Skip if we're in a TypeScript type definition
87
+ if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
88
+ current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
89
+ current.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
90
+ return false;
91
+ }
92
+ current = current.parent;
93
+ }
94
+ return false;
95
+ }
96
+ function checkNode(node) {
97
+ // Skip if we've already processed this node
98
+ if (seenNodes.has(node))
99
+ return;
100
+ seenNodes.add(node);
101
+ let propertyName = '';
102
+ // Get property name
103
+ if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
104
+ propertyName = node.key.name;
105
+ }
106
+ else if (node.key.type === utils_1.AST_NODE_TYPES.Literal) {
107
+ propertyName = String(node.key.value);
108
+ }
109
+ else if (node.computed && node.key.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
110
+ // Handle template literals like [`${prop}Top`]
111
+ const quasis = node.key.quasis.map(q => q.value.raw).join('');
112
+ const expressions = node.key.expressions.map(exp => {
113
+ if (exp.type === utils_1.AST_NODE_TYPES.Identifier) {
114
+ return exp.name;
115
+ }
116
+ return '';
117
+ }).join('');
118
+ propertyName = quasis + expressions;
119
+ }
120
+ if (propertyName && checkProperty(propertyName)) {
121
+ // Check if in MUI styling context
122
+ if (isMuiStylingContext(node)) {
123
+ context.report({
124
+ node,
125
+ messageId: 'noMarginProperties',
126
+ data: {
127
+ property: propertyName,
128
+ },
129
+ });
130
+ }
131
+ }
132
+ }
133
+ // Check object expression for margin properties
134
+ function checkObjectExpression(objExp) {
135
+ objExp.properties.forEach(prop => {
136
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
137
+ checkNode(prop);
138
+ }
139
+ else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement &&
140
+ prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
141
+ // Handle spread elements by looking up the variable
142
+ const variableName = prop.argument.name;
143
+ const scope = context.getScope();
144
+ const variable = scope.variables.find(v => v.name === variableName);
145
+ if (variable && variable.defs.length > 0) {
146
+ const def = variable.defs[0];
147
+ if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
148
+ def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
149
+ checkObjectExpression(def.node.init);
150
+ }
151
+ }
152
+ }
153
+ });
154
+ }
155
+ return {
156
+ // Handle object literal properties (inline styles)
157
+ Property(node) {
158
+ if (node.parent?.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
159
+ return;
160
+ checkNode(node);
161
+ },
162
+ // Handle JSX sx attributes with object expressions
163
+ JSXAttribute(node) {
164
+ if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier ||
165
+ node.name.name !== 'sx')
166
+ return;
167
+ if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
168
+ node.value.expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
169
+ checkObjectExpression(node.value.expression);
170
+ }
171
+ else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
172
+ node.value.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
173
+ // Handle variable reference in sx prop
174
+ const variableName = node.value.expression.name;
175
+ const scope = context.getScope();
176
+ const variable = scope.variables.find(v => v.name === variableName);
177
+ if (variable && variable.defs.length > 0) {
178
+ const def = variable.defs[0];
179
+ if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
180
+ def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
181
+ checkObjectExpression(def.node.init);
182
+ }
183
+ }
184
+ }
185
+ else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
186
+ node.value.expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
187
+ // Handle function-based sx props
188
+ if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
189
+ // Arrow function with object expression body
190
+ checkObjectExpression(node.value.expression.body);
191
+ }
192
+ else if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
193
+ // Arrow function with block body
194
+ const returnStatements = node.value.expression.body.body.filter(stmt => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
195
+ returnStatements.forEach(returnStmt => {
196
+ if (returnStmt.argument?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
197
+ checkObjectExpression(returnStmt.argument);
198
+ }
199
+ });
200
+ }
201
+ }
202
+ else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
203
+ node.value.expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
204
+ // Handle conditional expressions in sx props
205
+ if (node.value.expression.consequent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
206
+ checkObjectExpression(node.value.expression.consequent);
207
+ }
208
+ if (node.value.expression.alternate.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
209
+ checkObjectExpression(node.value.expression.alternate);
210
+ }
211
+ }
212
+ },
213
+ // Handle variable declarations that might be used in sx props
214
+ VariableDeclarator(node) {
215
+ if (node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
216
+ node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
217
+ const variableName = node.id.name;
218
+ const sourceCode = context.getSourceCode().getText();
219
+ // Check for margin properties in the object
220
+ node.init.properties.forEach(prop => {
221
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
222
+ let propertyName = '';
223
+ if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
224
+ propertyName = prop.key.name;
225
+ }
226
+ else if (prop.key.type === utils_1.AST_NODE_TYPES.Literal) {
227
+ propertyName = String(prop.key.value);
228
+ }
229
+ if (propertyName && checkProperty(propertyName)) {
230
+ // Check if this variable is used in an sx prop
231
+ if (sourceCode.includes(`sx={${variableName}}`) ||
232
+ sourceCode.includes(`sx={{ ...${variableName}`) ||
233
+ sourceCode.includes(`sx={Object.assign({}, ${variableName}`)) {
234
+ context.report({
235
+ node: prop,
236
+ messageId: 'noMarginProperties',
237
+ data: {
238
+ property: propertyName,
239
+ },
240
+ });
241
+ }
242
+ }
243
+ }
244
+ });
245
+ }
246
+ },
247
+ // Handle direct margin props on MUI components
248
+ JSXOpeningElement(node) {
249
+ node.attributes.forEach(attr => {
250
+ if (attr.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
251
+ attr.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
252
+ const attrName = attr.name.name;
253
+ if (checkProperty(attrName)) {
254
+ context.report({
255
+ node: attr,
256
+ messageId: 'noMarginProperties',
257
+ data: {
258
+ property: attrName,
259
+ },
260
+ });
261
+ }
262
+ }
263
+ });
264
+ },
265
+ // Handle MUI's css function
266
+ CallExpression(node) {
267
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
268
+ node.callee.name === 'css' &&
269
+ node.arguments.length > 0) {
270
+ const arg = node.arguments[0];
271
+ if (arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
272
+ checkObjectExpression(arg);
273
+ }
274
+ }
275
+ // Handle createTheme for MUI theme overrides
276
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
277
+ node.callee.name === 'createTheme' &&
278
+ node.arguments.length > 0 &&
279
+ node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
280
+ const themeObj = node.arguments[0];
281
+ // Find components property in theme object
282
+ const componentsProperty = themeObj.properties.find(prop => prop.type === utils_1.AST_NODE_TYPES.Property &&
283
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
284
+ prop.key.name === 'components' &&
285
+ prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
286
+ if (componentsProperty && componentsProperty.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
287
+ // Check each component override
288
+ componentsProperty.value.properties.forEach(componentProp => {
289
+ if (componentProp.type === utils_1.AST_NODE_TYPES.Property &&
290
+ componentProp.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
291
+ // Find styleOverrides property
292
+ const styleOverrides = componentProp.value.properties.find(prop => prop.type === utils_1.AST_NODE_TYPES.Property &&
293
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
294
+ prop.key.name === 'styleOverrides' &&
295
+ prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
296
+ if (styleOverrides && styleOverrides.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
297
+ checkObjectExpression(styleOverrides.value);
298
+ }
299
+ }
300
+ });
301
+ }
302
+ }
303
+ },
304
+ };
305
+ },
306
+ });
307
+ //# sourceMappingURL=no-margin-properties.js.map
@@ -98,6 +98,24 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
98
98
  defaultOptions: [defaultOptions],
99
99
  create(context, [options]) {
100
100
  const mergedOptions = { ...defaultOptions, ...options };
101
+ /**
102
+ * Checks if a node is inside a JSX attribute or object property (which could be JSX props)
103
+ */
104
+ function isInsideJSXAttributeOrObjectProperty(node) {
105
+ let current = node;
106
+ while (current?.parent) {
107
+ // Direct JSX attribute
108
+ if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute) {
109
+ return true;
110
+ }
111
+ // Object property (which could be JSX props)
112
+ if (current.parent.type === utils_1.AST_NODE_TYPES.Property) {
113
+ return true;
114
+ }
115
+ current = current.parent;
116
+ }
117
+ return false;
118
+ }
101
119
  /**
102
120
  * Common function to check if a type assertion should be allowed
103
121
  */
@@ -136,13 +154,31 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
136
154
  return true;
137
155
  }
138
156
  // Allow type assertions within conditional expressions, but only if they're not part of a return statement
139
- if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression && !isInsideReturnStatement(node)) {
157
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
158
+ !isInsideReturnStatement(node)) {
140
159
  return true;
141
160
  }
142
161
  // Allow type assertions used to access properties in conditional contexts
143
162
  if (isPropertyAccess(node) && isInsideConditionalStatement(node)) {
144
163
  return true;
145
164
  }
165
+ // Allow type assertions in object instantiations (as constructor arguments)
166
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
167
+ node.parent.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
168
+ return true;
169
+ }
170
+ // Allow type assertions as arguments to constructors
171
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.NewExpression) {
172
+ return true;
173
+ }
174
+ // Allow type assertions in JSX attributes/props or object properties
175
+ if (isInsideJSXAttributeOrObjectProperty(node)) {
176
+ return true;
177
+ }
178
+ // Only flag type assertions in return statements
179
+ if (!isInsideReturnStatement(node)) {
180
+ return true;
181
+ }
146
182
  return false;
147
183
  }
148
184
  /**
@@ -152,11 +188,13 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
152
188
  if (!node.returnType)
153
189
  return;
154
190
  // Allow type predicates if configured
155
- if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
191
+ if (mergedOptions.allowTypePredicates &&
192
+ isTypePredicate(node.returnType)) {
156
193
  return;
157
194
  }
158
195
  // If type predicates are not allowed, report them
159
- if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
196
+ if (!mergedOptions.allowTypePredicates &&
197
+ isTypePredicate(node.returnType)) {
160
198
  context.report({
161
199
  node: node.returnType,
162
200
  messageId: 'useExplicitVariable',
@@ -166,7 +204,8 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
166
204
  // Check if the function has a return statement with a direct value (not a variable)
167
205
  if (node.body && node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
168
206
  for (const statement of node.body.body) {
169
- if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
207
+ if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
208
+ statement.argument) {
170
209
  // If returning a variable reference, that's fine
171
210
  if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
172
211
  continue;
@@ -233,11 +272,13 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
233
272
  // Check for explicit return type
234
273
  if (node.returnType) {
235
274
  // Allow type predicates if configured
236
- if (mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
275
+ if (mergedOptions.allowTypePredicates &&
276
+ isTypePredicate(node.returnType)) {
237
277
  return;
238
278
  }
239
279
  // If type predicates are not allowed, report them
240
- if (!mergedOptions.allowTypePredicates && isTypePredicate(node.returnType)) {
280
+ if (!mergedOptions.allowTypePredicates &&
281
+ isTypePredicate(node.returnType)) {
241
282
  context.report({
242
283
  node: node.returnType,
243
284
  messageId: 'useExplicitVariable',
@@ -0,0 +1,8 @@
1
+ type Options = [
2
+ {
3
+ allowComplexBodies?: boolean;
4
+ allowFunctionFactories?: boolean;
5
+ }
6
+ ];
7
+ export declare const preferUseCallbackOverUseMemoForFunctions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferUseCallback", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export default preferUseCallbackOverUseMemoForFunctions;
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferUseCallbackOverUseMemoForFunctions = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.preferUseCallbackOverUseMemoForFunctions = (0, createRule_1.createRule)({
7
+ name: 'prefer-usecallback-over-usememo-for-functions',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce using useCallback instead of useMemo for memoizing functions',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [
16
+ {
17
+ type: 'object',
18
+ properties: {
19
+ allowComplexBodies: {
20
+ type: 'boolean',
21
+ default: true,
22
+ },
23
+ allowFunctionFactories: {
24
+ type: 'boolean',
25
+ default: true,
26
+ },
27
+ },
28
+ additionalProperties: false,
29
+ },
30
+ ],
31
+ messages: {
32
+ preferUseCallback: 'Use useCallback instead of useMemo for memoizing functions',
33
+ },
34
+ },
35
+ defaultOptions: [{ allowComplexBodies: false, allowFunctionFactories: true }],
36
+ create(context) {
37
+ const options = context.options[0] || { allowComplexBodies: false, allowFunctionFactories: true };
38
+ /**
39
+ * Checks if a node is a function factory (returns an object with functions or a function that generates functions)
40
+ */
41
+ function isFunctionFactory(node) {
42
+ // If we're not checking for function factories, return false
43
+ if (!options.allowFunctionFactories) {
44
+ return false;
45
+ }
46
+ // For arrow functions with implicit return
47
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
48
+ // Check if it's returning an object literal
49
+ if (node.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
50
+ // Check if any property in the object is a function
51
+ return node.body.properties.some((prop) => {
52
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
53
+ const value = prop.value;
54
+ return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
55
+ value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
56
+ }
57
+ return false;
58
+ });
59
+ }
60
+ return false;
61
+ }
62
+ // For arrow functions with block body
63
+ if (node.body.body.length === 1 &&
64
+ node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
65
+ node.body.body[0].argument) {
66
+ const returnValue = node.body.body[0].argument;
67
+ // Check if returning an object literal with functions
68
+ if (returnValue.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
69
+ return returnValue.properties.some((prop) => {
70
+ if (prop.type === utils_1.AST_NODE_TYPES.Property) {
71
+ const value = prop.value;
72
+ return (value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
73
+ value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
74
+ }
75
+ return false;
76
+ });
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * Checks if a function body is complex (more than one statement before returning)
83
+ */
84
+ function hasComplexBody(node) {
85
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
86
+ return false;
87
+ }
88
+ // If there's more than one statement, or the single statement isn't a return
89
+ if (node.body.body.length > 1 ||
90
+ (node.body.body.length === 1 && node.body.body[0].type !== utils_1.AST_NODE_TYPES.ReturnStatement)) {
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+ /**
96
+ * Checks if a node returns a function
97
+ */
98
+ function returnsFunction(node) {
99
+ // For arrow functions with implicit return
100
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
101
+ return (node.body.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
102
+ node.body.type === utils_1.AST_NODE_TYPES.FunctionExpression);
103
+ }
104
+ // For arrow functions with block body
105
+ if (node.body.body.length === 1 &&
106
+ node.body.body[0].type === utils_1.AST_NODE_TYPES.ReturnStatement &&
107
+ node.body.body[0].argument) {
108
+ const returnValue = node.body.body[0].argument;
109
+ return (returnValue.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
110
+ returnValue.type === utils_1.AST_NODE_TYPES.FunctionExpression);
111
+ }
112
+ return false;
113
+ }
114
+ return {
115
+ CallExpression(node) {
116
+ // Check if the call is to useMemo
117
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
118
+ node.callee.name === 'useMemo' &&
119
+ node.arguments.length > 0) {
120
+ const callback = node.arguments[0];
121
+ // Check if the callback is an arrow function or function expression
122
+ if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
123
+ callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
124
+ callback.body) {
125
+ // Skip if it's a function factory and we're allowing those
126
+ if (isFunctionFactory(callback)) {
127
+ return;
128
+ }
129
+ // Skip if it has a complex body and we're allowing those
130
+ if (hasComplexBody(callback) && options.allowComplexBodies) {
131
+ return;
132
+ }
133
+ // Check if it returns a function
134
+ if (returnsFunction(callback)) {
135
+ reportAndFix(node, context);
136
+ }
137
+ }
138
+ }
139
+ },
140
+ };
141
+ },
142
+ });
143
+ function reportAndFix(node, context) {
144
+ const sourceCode = context.getSourceCode();
145
+ const useMemoCallback = node.arguments[0];
146
+ const dependencyArray = node.arguments[1] ? sourceCode.getText(node.arguments[1]) : '[]';
147
+ // Get the returned function from useMemo
148
+ let returnedFunction;
149
+ if (useMemoCallback.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
150
+ // For block body arrow functions or function expressions
151
+ const returnStatement = useMemoCallback.body.body[0];
152
+ returnedFunction = returnStatement.argument;
153
+ }
154
+ else {
155
+ // For implicit return arrow functions
156
+ returnedFunction = useMemoCallback.body;
157
+ }
158
+ // Create the useCallback replacement
159
+ const returnedFunctionText = sourceCode.getText(returnedFunction);
160
+ // Check if useMemo has TypeScript generic type parameters
161
+ const hasTypeParameters = node.typeParameters !== undefined;
162
+ const typeParametersText = hasTypeParameters ? sourceCode.getText(node.typeParameters) : '';
163
+ context.report({
164
+ node,
165
+ messageId: 'preferUseCallback',
166
+ fix: (fixer) => {
167
+ return fixer.replaceText(node, `useCallback${typeParametersText}(${returnedFunctionText}, ${dependencyArray})`);
168
+ },
169
+ });
170
+ }
171
+ exports.default = exports.preferUseCallbackOverUseMemoForFunctions;
172
+ //# sourceMappingURL=prefer-usecallback-over-usememo-for-functions.js.map
@@ -15,6 +15,71 @@ const isJsxElement = (node) => {
15
15
  return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
16
16
  node.type === utils_1.AST_NODE_TYPES.JSXFragment);
17
17
  };
18
+ /**
19
+ * Checks if a variable is used multiple times in a component
20
+ */
21
+ const isUsedMultipleTimes = (variableName, node) => {
22
+ // Find the function component that contains this node
23
+ let currentNode = node;
24
+ let functionNode;
25
+ // Walk up the AST to find the function component
26
+ while (currentNode.parent) {
27
+ currentNode = currentNode.parent;
28
+ if (currentNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
29
+ currentNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
30
+ currentNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
31
+ functionNode = currentNode;
32
+ break;
33
+ }
34
+ }
35
+ if (!functionNode) {
36
+ return false;
37
+ }
38
+ // Count occurrences of the variable in the function body
39
+ let count = 0;
40
+ // Function to recursively search for references to the variable
41
+ const findReferences = (searchNode) => {
42
+ if (!searchNode)
43
+ return;
44
+ // Check if this node is a reference to our variable
45
+ if (searchNode.type === utils_1.AST_NODE_TYPES.Identifier &&
46
+ searchNode.name === variableName &&
47
+ // Exclude the declaration itself
48
+ !(searchNode.parent &&
49
+ searchNode.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
50
+ searchNode.parent.id === searchNode)) {
51
+ count++;
52
+ }
53
+ // Recursively check all properties of the node
54
+ for (const key in searchNode) {
55
+ if (key === 'parent')
56
+ continue; // Skip parent to avoid circular references
57
+ const child = searchNode[key];
58
+ if (child && typeof child === 'object') {
59
+ if (Array.isArray(child)) {
60
+ child.forEach(item => {
61
+ if (item && typeof item === 'object') {
62
+ findReferences(item);
63
+ }
64
+ });
65
+ }
66
+ else {
67
+ findReferences(child);
68
+ }
69
+ }
70
+ }
71
+ };
72
+ // Start the search from the function body
73
+ if (functionNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
74
+ findReferences(functionNode.body);
75
+ }
76
+ else if (functionNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
77
+ functionNode.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
78
+ findReferences(functionNode.body);
79
+ }
80
+ // Return true if the variable is referenced more than once
81
+ return count > 1;
82
+ };
18
83
  /**
19
84
  * Checks if an object contains JSX elements in its properties
20
85
  */
@@ -338,6 +403,17 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
338
403
  return {
339
404
  CallExpression(node) {
340
405
  if (containsJsxInUseMemo(node)) {
406
+ // Check if this is a variable declaration
407
+ if (node.parent &&
408
+ node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
409
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
410
+ const variableName = node.parent.id.name;
411
+ // Check if the variable is used multiple times in the component
412
+ if (isUsedMultipleTimes(variableName, node)) {
413
+ // If the variable is used multiple times, allow it
414
+ return;
415
+ }
416
+ }
341
417
  context.report({
342
418
  node,
343
419
  messageId: 'useMemoShouldBeComponent',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",