@blumintinc/eslint-plugin-blumint 1.3.2 → 1.5.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 (36) hide show
  1. package/README.md +41 -40
  2. package/lib/index.js +37 -1
  3. package/lib/rules/array-methods-this-context.js +2 -2
  4. package/lib/rules/enforce-callback-memo.js +3 -3
  5. package/lib/rules/enforce-dynamic-firebase-imports.js +2 -2
  6. package/lib/rules/enforce-exported-function-types.js +88 -85
  7. package/lib/rules/enforce-firestore-doc-ref-generic.js +7 -2
  8. package/lib/rules/enforce-firestore-path-utils.js +2 -2
  9. package/lib/rules/enforce-firestore-set-merge.js +45 -8
  10. package/lib/rules/enforce-identifiable-firestore-type.js +2 -2
  11. package/lib/rules/enforce-memoize-async.js +2 -2
  12. package/lib/rules/enforce-mock-firestore.js +3 -3
  13. package/lib/rules/enforce-realtimedb-path-utils.js +1 -1
  14. package/lib/rules/enforce-safe-stringify.js +2 -2
  15. package/lib/rules/enforce-serializable-params.js +3 -3
  16. package/lib/rules/enforce-verb-noun-naming.js +41 -7
  17. package/lib/rules/no-async-array-filter.js +2 -2
  18. package/lib/rules/no-async-foreach.js +1 -1
  19. package/lib/rules/no-class-instance-destructuring.d.ts +1 -0
  20. package/lib/rules/no-class-instance-destructuring.js +95 -0
  21. package/lib/rules/no-compositing-layer-props.js +1 -1
  22. package/lib/rules/no-conditional-literals-in-jsx.js +1 -1
  23. package/lib/rules/no-entire-object-hook-deps.js +1 -1
  24. package/lib/rules/no-explicit-return-type.d.ts +3 -1
  25. package/lib/rules/no-explicit-return-type.js +30 -17
  26. package/lib/rules/no-filter-without-return.js +1 -1
  27. package/lib/rules/no-misused-switch-case.js +2 -2
  28. package/lib/rules/no-redundant-param-types.d.ts +2 -0
  29. package/lib/rules/no-redundant-param-types.js +129 -0
  30. package/lib/rules/no-unused-props.js +1 -1
  31. package/lib/rules/no-useless-fragment.js +1 -1
  32. package/lib/rules/prefer-destructuring-no-class.d.ts +8 -0
  33. package/lib/rules/prefer-destructuring-no-class.js +200 -0
  34. package/lib/rules/require-usememo-object-literals.js +1 -1
  35. package/lib/rules/semantic-function-prefixes.js +43 -4
  36. package/package.json +2 -2
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noRedundantParamTypes = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ function isIdentifierWithTypeAnnotation(node) {
7
+ return (node.type === utils_1.AST_NODE_TYPES.Identifier && node.typeAnnotation !== undefined);
8
+ }
9
+ function isRestElementWithTypeAnnotation(node) {
10
+ return (node.type === utils_1.AST_NODE_TYPES.RestElement &&
11
+ node.typeAnnotation !== undefined);
12
+ }
13
+ function isObjectPatternWithTypeAnnotation(node) {
14
+ return (node.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
15
+ node.typeAnnotation !== undefined);
16
+ }
17
+ function isArrayPatternWithTypeAnnotation(node) {
18
+ return (node.type === utils_1.AST_NODE_TYPES.ArrayPattern &&
19
+ node.typeAnnotation !== undefined);
20
+ }
21
+ function isAssignmentPatternWithTypeAnnotation(node) {
22
+ return (node.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
23
+ node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
24
+ node.left.typeAnnotation !== undefined);
25
+ }
26
+ function removeTypeAnnotation(fixer, typeAnnotation, sourceCode) {
27
+ const typeStart = typeAnnotation.range[0];
28
+ const typeEnd = typeAnnotation.range[1];
29
+ // Check if there's a question mark before the type annotation
30
+ const hasQuestionMark = typeStart > 0 && sourceCode.getText().charAt(typeStart - 1) === '?';
31
+ const startPos = hasQuestionMark ? typeStart - 1 : typeStart;
32
+ return fixer.removeRange([startPos, typeEnd]);
33
+ }
34
+ function hasRedundantTypeAnnotation(node) {
35
+ const parent = node.parent;
36
+ if (!parent)
37
+ return false;
38
+ // Check variable declarations
39
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
40
+ parent.id.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
41
+ return true;
42
+ }
43
+ // Check class property assignments
44
+ if (parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
45
+ parent.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
46
+ return true;
47
+ }
48
+ // Check assignments
49
+ if (parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
50
+ parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
51
+ parent.left.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ exports.noRedundantParamTypes = (0, createRule_1.createRule)({
57
+ name: 'no-redundant-param-types',
58
+ meta: {
59
+ type: 'suggestion',
60
+ docs: {
61
+ description: 'Disallow redundant parameter type annotations',
62
+ recommended: 'error',
63
+ },
64
+ fixable: 'code',
65
+ schema: [],
66
+ messages: {
67
+ redundantParamType: 'Parameter type annotation is redundant',
68
+ },
69
+ },
70
+ defaultOptions: [],
71
+ create(context) {
72
+ return {
73
+ ArrowFunctionExpression(node) {
74
+ if (!hasRedundantTypeAnnotation(node))
75
+ return;
76
+ const params = node.params;
77
+ params.forEach((param) => {
78
+ if (isIdentifierWithTypeAnnotation(param)) {
79
+ context.report({
80
+ node: param,
81
+ messageId: 'redundantParamType',
82
+ fix(fixer) {
83
+ return removeTypeAnnotation(fixer, param.typeAnnotation, context.getSourceCode());
84
+ },
85
+ });
86
+ }
87
+ else if (isRestElementWithTypeAnnotation(param)) {
88
+ context.report({
89
+ node: param,
90
+ messageId: 'redundantParamType',
91
+ fix(fixer) {
92
+ return removeTypeAnnotation(fixer, param.typeAnnotation, context.getSourceCode());
93
+ },
94
+ });
95
+ }
96
+ else if (isObjectPatternWithTypeAnnotation(param)) {
97
+ context.report({
98
+ node: param,
99
+ messageId: 'redundantParamType',
100
+ fix(fixer) {
101
+ return removeTypeAnnotation(fixer, param.typeAnnotation, context.getSourceCode());
102
+ },
103
+ });
104
+ }
105
+ else if (isArrayPatternWithTypeAnnotation(param)) {
106
+ context.report({
107
+ node: param,
108
+ messageId: 'redundantParamType',
109
+ fix(fixer) {
110
+ return removeTypeAnnotation(fixer, param.typeAnnotation, context.getSourceCode());
111
+ },
112
+ });
113
+ }
114
+ else if (isAssignmentPatternWithTypeAnnotation(param)) {
115
+ const { left } = param;
116
+ context.report({
117
+ node: param,
118
+ messageId: 'redundantParamType',
119
+ fix(fixer) {
120
+ return removeTypeAnnotation(fixer, left.typeAnnotation, context.getSourceCode());
121
+ },
122
+ });
123
+ }
124
+ });
125
+ },
126
+ };
127
+ },
128
+ });
129
+ //# sourceMappingURL=no-redundant-param-types.js.map
@@ -13,7 +13,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
13
13
  },
14
14
  schema: [],
15
15
  messages: {
16
- unusedProp: 'Prop "{{propName}}" is defined in type but not used in component',
16
+ unusedProp: 'Prop "{{propName}}" is defined in the Props type but not used in the component. Either use the prop in your component or remove it from the Props type. If you need to forward all props, use a rest spread operator: `const MyComponent = ({ usedProp, ...rest }: Props) => ...`',
17
17
  },
18
18
  fixable: 'code',
19
19
  },
@@ -38,7 +38,7 @@ exports.noUselessFragment = {
38
38
  recommended: 'error',
39
39
  },
40
40
  messages: {
41
- noUselessFragment: 'React fragment is unnecessary when wrapping a single child',
41
+ noUselessFragment: 'React fragment is unnecessary when wrapping a single child. Instead of `<>{"text"}</> or <Fragment>{"text"}</Fragment>`, use just `{"text"}`. Fragments are only needed when returning multiple elements.',
42
42
  },
43
43
  schema: [],
44
44
  fixable: 'code',
@@ -0,0 +1,8 @@
1
+ type Options = [
2
+ {
3
+ object?: boolean;
4
+ enforceForRenamedProperties?: boolean;
5
+ }
6
+ ];
7
+ export declare const preferDestructuringNoClass: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferDestructuring", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export {};
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferDestructuringNoClass = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const defaultOptions = [
7
+ {
8
+ object: true,
9
+ enforceForRenamedProperties: false,
10
+ },
11
+ ];
12
+ function isClassInstance(node, context) {
13
+ // Check if node is a MemberExpression
14
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
15
+ const object = node.object;
16
+ // If object is a NewExpression, it's a class instance
17
+ if (object.type === utils_1.AST_NODE_TYPES.NewExpression) {
18
+ return true;
19
+ }
20
+ // If object is an identifier, check if it refers to a class instance
21
+ if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
22
+ const variable = object.name;
23
+ const scope = context.getScope();
24
+ const ref = scope.references.find((ref) => ref.identifier.name === variable);
25
+ if (ref?.resolved?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
26
+ const init = ref.resolved.defs[0].node.init;
27
+ return init?.type === utils_1.AST_NODE_TYPES.NewExpression;
28
+ }
29
+ // Check if the identifier refers to a class (not an instance)
30
+ if (ref?.resolved?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
31
+ return false;
32
+ }
33
+ }
34
+ // Recursively check if parent object is a class instance
35
+ if (object.type === utils_1.AST_NODE_TYPES.MemberExpression) {
36
+ return isClassInstance(object, context);
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ function isStaticClassMember(node, context) {
42
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
43
+ const object = node.object;
44
+ if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
45
+ const variable = object.name;
46
+ const scope = context.getScope();
47
+ const ref = scope.references.find((ref) => ref.identifier.name === variable);
48
+ return (ref?.resolved?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.ClassDeclaration);
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+ /**
54
+ * Check if the property name matches the variable name in an assignment
55
+ */
56
+ function isMatchingPropertyName(propertyNode, variableName) {
57
+ if (propertyNode.type === utils_1.AST_NODE_TYPES.Identifier) {
58
+ return propertyNode.name === variableName;
59
+ }
60
+ if (propertyNode.type === utils_1.AST_NODE_TYPES.Literal) {
61
+ return propertyNode.value === variableName;
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * Get the property text for destructuring
67
+ */
68
+ function getPropertyText(property, computed, sourceCode) {
69
+ if (computed) {
70
+ return sourceCode.getText(property);
71
+ }
72
+ if (property.type === utils_1.AST_NODE_TYPES.Identifier) {
73
+ return property.name;
74
+ }
75
+ if (property.type === utils_1.AST_NODE_TYPES.Literal) {
76
+ return String(property.value);
77
+ }
78
+ // For any other type, use the source text
79
+ return sourceCode.getText(property);
80
+ }
81
+ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
82
+ name: 'prefer-destructuring-no-class',
83
+ meta: {
84
+ type: 'suggestion',
85
+ docs: {
86
+ description: 'Enforce destructuring when accessing object properties, except for class instances',
87
+ recommended: 'error',
88
+ },
89
+ fixable: 'code',
90
+ schema: [
91
+ {
92
+ type: 'object',
93
+ properties: {
94
+ object: {
95
+ type: 'boolean',
96
+ default: true,
97
+ },
98
+ enforceForRenamedProperties: {
99
+ type: 'boolean',
100
+ default: false,
101
+ },
102
+ },
103
+ additionalProperties: false,
104
+ },
105
+ ],
106
+ messages: {
107
+ preferDestructuring: 'Use destructuring instead of accessing the property directly.',
108
+ },
109
+ },
110
+ defaultOptions,
111
+ create(context) {
112
+ const options = {
113
+ object: defaultOptions[0].object,
114
+ enforceForRenamedProperties: defaultOptions[0].enforceForRenamedProperties,
115
+ ...context.options[0],
116
+ };
117
+ /**
118
+ * Check if destructuring should be used for this node
119
+ */
120
+ function shouldUseDestructuring(node, leftNode) {
121
+ // Skip if this is a class instance or static class member
122
+ if (isClassInstance(node, context) ||
123
+ isStaticClassMember(node, context)) {
124
+ return false;
125
+ }
126
+ // Check object destructuring
127
+ if (options.object) {
128
+ if (options.enforceForRenamedProperties) {
129
+ return true;
130
+ }
131
+ // Only suggest destructuring when property name matches variable name
132
+ if (leftNode.type === utils_1.AST_NODE_TYPES.Identifier) {
133
+ return isMatchingPropertyName(node.property, leftNode.name);
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ return {
139
+ VariableDeclarator(node) {
140
+ // Skip if variable is declared without assignment or if init is not a MemberExpression
141
+ if (!node.init)
142
+ return;
143
+ if (node.init.type !== utils_1.AST_NODE_TYPES.MemberExpression)
144
+ return;
145
+ if (shouldUseDestructuring(node.init, node.id)) {
146
+ const sourceCode = context.getSourceCode();
147
+ const objectText = sourceCode.getText(node.init.object);
148
+ const propertyText = getPropertyText(node.init.property, node.init.computed, sourceCode);
149
+ context.report({
150
+ node,
151
+ messageId: 'preferDestructuring',
152
+ fix(fixer) {
153
+ // Get the variable declaration kind (const, let, var)
154
+ const parentNode = node.parent;
155
+ if (!parentNode ||
156
+ parentNode.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
157
+ return null;
158
+ }
159
+ const kind = parentNode.kind;
160
+ // Handle renamed properties
161
+ if (options.enforceForRenamedProperties &&
162
+ node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
163
+ node.init &&
164
+ node.init.type === utils_1.AST_NODE_TYPES.MemberExpression &&
165
+ !isMatchingPropertyName(node.init.property, node.id.name)) {
166
+ return fixer.replaceText(parentNode, `${kind} { ${propertyText}: ${node.id.name} } = ${objectText};`);
167
+ }
168
+ return fixer.replaceText(parentNode, `${kind} { ${propertyText} } = ${objectText};`);
169
+ },
170
+ });
171
+ }
172
+ },
173
+ AssignmentExpression(node) {
174
+ if (node.operator === '=' &&
175
+ node.right.type === utils_1.AST_NODE_TYPES.MemberExpression) {
176
+ if (shouldUseDestructuring(node.right, node.left)) {
177
+ const sourceCode = context.getSourceCode();
178
+ const objectText = sourceCode.getText(node.right.object);
179
+ const propertyText = getPropertyText(node.right.property, node.right.computed, sourceCode);
180
+ context.report({
181
+ node,
182
+ messageId: 'preferDestructuring',
183
+ fix(fixer) {
184
+ // Handle renamed properties
185
+ if (options.enforceForRenamedProperties &&
186
+ node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
187
+ node.right.type === utils_1.AST_NODE_TYPES.MemberExpression &&
188
+ !isMatchingPropertyName(node.right.property, node.left.name)) {
189
+ return fixer.replaceText(node, `({ ${propertyText}: ${node.left.name} } = ${objectText})`);
190
+ }
191
+ return fixer.replaceText(node, `({ ${propertyText} } = ${objectText})`);
192
+ },
193
+ });
194
+ }
195
+ }
196
+ },
197
+ };
198
+ },
199
+ });
200
+ //# sourceMappingURL=prefer-destructuring-no-class.js.map
@@ -7,7 +7,7 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
7
7
  meta: {
8
8
  type: 'suggestion',
9
9
  docs: {
10
- description: 'Enforce using useMemo for inline object literals passed as props to JSX components',
10
+ description: 'Enforce using useMemo for inline object/array literals passed as props to JSX components to prevent unnecessary re-renders. When object/array literals are defined inline in JSX, they create new references on every render, causing child components to re-render even if the values haven\'t changed. Wrap them in useMemo to maintain referential equality.',
11
11
  recommended: 'error',
12
12
  },
13
13
  messages: {
@@ -27,6 +27,40 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
27
27
  },
28
28
  defaultOptions: [],
29
29
  create(context) {
30
+ function checkMethodName(node) {
31
+ // Skip getters and setters
32
+ if (node.kind === 'get' || node.kind === 'set') {
33
+ return;
34
+ }
35
+ const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : '';
36
+ if (!methodName)
37
+ return;
38
+ // Skip if method starts with 'is' (boolean check methods are okay)
39
+ if (methodName.startsWith('is'))
40
+ return;
41
+ // Extract first word from PascalCase/camelCase
42
+ let firstWord = methodName;
43
+ for (let i = 1; i < methodName.length; i++) {
44
+ if (methodName[i] >= 'A' && methodName[i] <= 'Z') {
45
+ firstWord = methodName.substring(0, i);
46
+ break;
47
+ }
48
+ }
49
+ // Check for disallowed prefixes
50
+ for (const prefix of DISALLOWED_PREFIXES) {
51
+ if (firstWord.toLowerCase() === prefix.toLowerCase()) {
52
+ context.report({
53
+ node: node.key,
54
+ messageId: 'avoidGenericPrefix',
55
+ data: {
56
+ prefix,
57
+ alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
58
+ },
59
+ });
60
+ break;
61
+ }
62
+ }
63
+ }
30
64
  function checkFunctionName(node) {
31
65
  // Skip anonymous functions
32
66
  if (!node.id && node.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
@@ -45,13 +79,17 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
45
79
  // Skip if function starts with 'is' (boolean check functions are okay)
46
80
  if (functionName.startsWith('is'))
47
81
  return;
48
- // Skip class getters
49
- if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition && node.parent.kind === 'get') {
50
- return;
82
+ // Extract first word from PascalCase/camelCase
83
+ let firstWord = functionName;
84
+ for (let i = 1; i < functionName.length; i++) {
85
+ if (functionName[i] >= 'A' && functionName[i] <= 'Z') {
86
+ firstWord = functionName.substring(0, i);
87
+ break;
88
+ }
51
89
  }
52
90
  // Check for disallowed prefixes
53
91
  for (const prefix of DISALLOWED_PREFIXES) {
54
- if (functionName.toLowerCase().startsWith(prefix.toLowerCase())) {
92
+ if (firstWord.toLowerCase() === prefix.toLowerCase()) {
55
93
  context.report({
56
94
  node: node.id || node,
57
95
  messageId: 'avoidGenericPrefix',
@@ -68,6 +106,7 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
68
106
  FunctionDeclaration: checkFunctionName,
69
107
  FunctionExpression: checkFunctionName,
70
108
  ArrowFunctionExpression: checkFunctionName,
109
+ MethodDefinition: checkMethodName,
71
110
  };
72
111
  },
73
112
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -71,7 +71,7 @@
71
71
  "dotenv-cli": "5.0.0",
72
72
  "eslint": "8.57.0",
73
73
  "eslint-config-prettier": "8.5.0",
74
- "eslint-doc-generator": "1.0.0",
74
+ "eslint-doc-generator": "1.7.1",
75
75
  "eslint-import-resolver-typescript": "3.5.5",
76
76
  "eslint-plugin-eslint-plugin": "5.0.0",
77
77
  "eslint-plugin-import": "2.26.0",