@blumintinc/eslint-plugin-blumint 1.2.0 → 1.3.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 (30) hide show
  1. package/README.md +40 -37
  2. package/lib/index.js +22 -13
  3. package/lib/rules/dynamic-https-errors.js +1 -1
  4. package/lib/rules/enforce-callback-memo.js +21 -3
  5. package/lib/rules/enforce-firestore-doc-ref-generic.d.ts +10 -0
  6. package/lib/rules/enforce-firestore-doc-ref-generic.js +134 -0
  7. package/lib/rules/enforce-firestore-set-merge.d.ts +1 -0
  8. package/lib/rules/enforce-firestore-set-merge.js +106 -0
  9. package/lib/rules/enforce-mock-firestore.d.ts +3 -0
  10. package/lib/rules/enforce-mock-firestore.js +76 -0
  11. package/lib/rules/enforce-verb-noun-naming.d.ts +1 -0
  12. package/lib/rules/enforce-verb-noun-naming.js +110 -0
  13. package/lib/rules/export-if-in-doubt.js +1 -1
  14. package/lib/rules/extract-global-constants.js +31 -2
  15. package/lib/rules/global-const-style.d.ts +2 -4
  16. package/lib/rules/global-const-style.js +48 -22
  17. package/lib/rules/no-compositing-layer-props.js +38 -4
  18. package/lib/rules/no-entire-object-hook-deps.js +68 -60
  19. package/lib/rules/no-explicit-return-type.d.ts +11 -0
  20. package/lib/rules/no-explicit-return-type.js +190 -0
  21. package/lib/rules/no-unused-props.js +33 -8
  22. package/lib/rules/no-useless-fragment.js +1 -1
  23. package/lib/rules/prefer-fragment-shorthand.js +1 -1
  24. package/lib/rules/prefer-type-over-interface.js +1 -1
  25. package/lib/rules/require-https-error.js +32 -23
  26. package/lib/rules/require-image-optimized.d.ts +7 -0
  27. package/lib/rules/require-image-optimized.js +82 -0
  28. package/lib/rules/semantic-function-prefixes.d.ts +1 -0
  29. package/lib/rules/semantic-function-prefixes.js +74 -0
  30. package/package.json +2 -1
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noExplicitReturnType = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const defaultOptions = {
7
+ allowRecursiveFunctions: true,
8
+ allowOverloadedFunctions: true,
9
+ allowInterfaceMethodSignatures: true,
10
+ allowAbstractMethodSignatures: true,
11
+ allowDtsFiles: true,
12
+ };
13
+ function isRecursiveFunction(node) {
14
+ const functionName = node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ? node.id?.name :
15
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression && node.id ? node.id.name :
16
+ undefined;
17
+ if (!functionName || !node.body)
18
+ return false;
19
+ let hasRecursiveCall = false;
20
+ function checkNode(node) {
21
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
22
+ node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
23
+ node.callee.name === functionName) {
24
+ hasRecursiveCall = true;
25
+ return;
26
+ }
27
+ // Only traverse specific node types to avoid circular references
28
+ if (node.type === utils_1.AST_NODE_TYPES.BlockStatement) {
29
+ node.body.forEach(checkNode);
30
+ }
31
+ else if (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
32
+ checkNode(node.expression);
33
+ }
34
+ else if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
35
+ checkNode(node.callee);
36
+ node.arguments.forEach(checkNode);
37
+ }
38
+ else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
39
+ checkNode(node.left);
40
+ checkNode(node.right);
41
+ }
42
+ else if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement && node.argument) {
43
+ checkNode(node.argument);
44
+ }
45
+ else if (node.type === utils_1.AST_NODE_TYPES.IfStatement) {
46
+ checkNode(node.test);
47
+ checkNode(node.consequent);
48
+ if (node.alternate) {
49
+ checkNode(node.alternate);
50
+ }
51
+ }
52
+ }
53
+ checkNode(node.body);
54
+ return hasRecursiveCall;
55
+ }
56
+ function isOverloadedFunction(node) {
57
+ if (!node.parent)
58
+ return false;
59
+ if (node.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
60
+ const interfaceBody = node.parent;
61
+ if (interfaceBody.type !== utils_1.AST_NODE_TYPES.TSInterfaceBody)
62
+ return false;
63
+ const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : undefined;
64
+ if (!methodName)
65
+ return false;
66
+ return interfaceBody.body.filter(member => member.type === utils_1.AST_NODE_TYPES.TSMethodSignature &&
67
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
68
+ member.key.name === methodName).length > 1;
69
+ }
70
+ return false;
71
+ }
72
+ function isInterfaceOrAbstractMethodSignature(node) {
73
+ if (node.type === utils_1.AST_NODE_TYPES.TSMethodSignature)
74
+ return true;
75
+ if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
76
+ let current = node;
77
+ while (current) {
78
+ if (current.type === utils_1.AST_NODE_TYPES.ClassDeclaration && current.abstract) {
79
+ return true;
80
+ }
81
+ current = current.parent;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ exports.noExplicitReturnType = (0, createRule_1.createRule)({
87
+ name: 'no-explicit-return-type',
88
+ meta: {
89
+ type: 'suggestion',
90
+ docs: {
91
+ description: 'Disallow explicit return types on functions',
92
+ recommended: 'error',
93
+ },
94
+ fixable: 'code',
95
+ schema: [
96
+ {
97
+ type: 'object',
98
+ properties: {
99
+ allowRecursiveFunctions: { type: 'boolean' },
100
+ allowOverloadedFunctions: { type: 'boolean' },
101
+ allowInterfaceMethodSignatures: { type: 'boolean' },
102
+ allowAbstractMethodSignatures: { type: 'boolean' },
103
+ allowDtsFiles: { type: 'boolean' },
104
+ },
105
+ additionalProperties: false,
106
+ },
107
+ ],
108
+ messages: {
109
+ noExplicitReturnType: 'Explicit return type is not allowed. Let TypeScript infer it.',
110
+ },
111
+ },
112
+ defaultOptions: [defaultOptions],
113
+ create(context, [options]) {
114
+ const mergedOptions = { ...defaultOptions, ...options };
115
+ const filename = context.getFilename();
116
+ if (mergedOptions.allowDtsFiles && filename.endsWith('.d.ts')) {
117
+ return {};
118
+ }
119
+ function fixReturnType(fixer, node) {
120
+ const returnType = node.returnType || node.value?.returnType;
121
+ if (!returnType)
122
+ return null;
123
+ // Create a fix that removes the return type annotation
124
+ return fixer.remove(returnType);
125
+ }
126
+ return {
127
+ FunctionDeclaration(node) {
128
+ if (!node.returnType)
129
+ return;
130
+ if (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node)) {
131
+ return;
132
+ }
133
+ context.report({
134
+ node: node.returnType,
135
+ messageId: 'noExplicitReturnType',
136
+ fix: fixer => fixReturnType(fixer, node),
137
+ });
138
+ },
139
+ FunctionExpression(node) {
140
+ if (!node.returnType)
141
+ return;
142
+ if (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node)) {
143
+ return;
144
+ }
145
+ context.report({
146
+ node: node.returnType,
147
+ messageId: 'noExplicitReturnType',
148
+ fix: fixer => fixReturnType(fixer, node),
149
+ });
150
+ },
151
+ ArrowFunctionExpression(node) {
152
+ if (!node.returnType)
153
+ return;
154
+ context.report({
155
+ node: node.returnType,
156
+ messageId: 'noExplicitReturnType',
157
+ fix: fixer => fixReturnType(fixer, node),
158
+ });
159
+ },
160
+ TSMethodSignature(node) {
161
+ if (!node.returnType)
162
+ return;
163
+ if (mergedOptions.allowInterfaceMethodSignatures) {
164
+ return;
165
+ }
166
+ if (mergedOptions.allowOverloadedFunctions && isOverloadedFunction(node)) {
167
+ return;
168
+ }
169
+ context.report({
170
+ node: node.returnType,
171
+ messageId: 'noExplicitReturnType',
172
+ fix: fixer => fixReturnType(fixer, node),
173
+ });
174
+ },
175
+ MethodDefinition(node) {
176
+ if (!node.value.returnType)
177
+ return;
178
+ if (mergedOptions.allowAbstractMethodSignatures && isInterfaceOrAbstractMethodSignature(node)) {
179
+ return;
180
+ }
181
+ context.report({
182
+ node: node.value.returnType,
183
+ messageId: 'noExplicitReturnType',
184
+ fix: fixer => fixReturnType(fixer, node),
185
+ });
186
+ },
187
+ };
188
+ },
189
+ });
190
+ //# sourceMappingURL=no-explicit-return-type.js.map
@@ -25,16 +25,28 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
25
25
  return {
26
26
  TSTypeAliasDeclaration(node) {
27
27
  if (node.id.name.endsWith('Props')) {
28
- if (node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
29
- const props = {};
30
- node.typeAnnotation.members.forEach((member) => {
31
- if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
32
- member.key.type === utils_1.AST_NODE_TYPES.Identifier) {
33
- props[member.key.name] = member.key;
28
+ const props = {};
29
+ function extractProps(typeNode) {
30
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
31
+ typeNode.members.forEach((member) => {
32
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
33
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier) {
34
+ props[member.key.name] = member.key;
35
+ }
36
+ });
37
+ }
38
+ else if (typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
39
+ typeNode.types.forEach(extractProps);
40
+ }
41
+ else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
42
+ // For referenced types like FormControlLabelProps, we need to track that these props should be forwarded
43
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
44
+ props[`...${typeNode.typeName.name}`] = typeNode.typeName;
34
45
  }
35
- });
36
- propsTypes.set(node.id.name, props);
46
+ }
37
47
  }
48
+ extractProps(node.typeAnnotation);
49
+ propsTypes.set(node.id.name, props);
38
50
  }
39
51
  },
40
52
  VariableDeclaration(node) {
@@ -56,6 +68,19 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
56
68
  prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
57
69
  used.add(prop.key.name);
58
70
  }
71
+ else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
72
+ prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
73
+ // Handle rest spread operator {...rest}
74
+ // When a rest operator is used, all remaining props are considered used
75
+ const propsType = propsTypes.get(typeName);
76
+ if (propsType) {
77
+ Object.keys(propsType).forEach((key) => {
78
+ if (key.startsWith('...')) {
79
+ used.add(key);
80
+ }
81
+ });
82
+ }
83
+ }
59
84
  });
60
85
  usedProps.set(typeName, used);
61
86
  }
@@ -35,7 +35,7 @@ exports.noUselessFragment = {
35
35
  type: 'suggestion',
36
36
  docs: {
37
37
  description: 'Prevent unnecessary use of React fragments',
38
- recommended: 'warn',
38
+ recommended: 'error',
39
39
  },
40
40
  messages: {
41
41
  noUselessFragment: 'React fragment is unnecessary when wrapping a single child',
@@ -28,7 +28,7 @@ exports.preferFragmentShorthand = {
28
28
  type: 'suggestion',
29
29
  docs: {
30
30
  description: 'Prefer <> shorthand for <React.Fragment>',
31
- recommended: 'warn',
31
+ recommended: 'error',
32
32
  },
33
33
  messages: {
34
34
  preferShorthand: 'Use <> shorthand for <React.Fragment>, unless a key is required for an iterator',
@@ -8,7 +8,7 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
8
8
  type: 'suggestion',
9
9
  docs: {
10
10
  description: 'Prefer using type alias over interface',
11
- recommended: 'warn',
11
+ recommended: 'error',
12
12
  },
13
13
  schema: [],
14
14
  messages: {
@@ -42,30 +42,39 @@ module.exports = (0, createRule_1.createRule)({
42
42
  }
43
43
  },
44
44
  ThrowStatement(node) {
45
+ if (!node.argument) {
46
+ return;
47
+ }
45
48
  const argument = node.argument;
46
- if (argument &&
47
- argument.type === utils_1.AST_NODE_TYPES.NewExpression &&
48
- argument.callee) {
49
- if (argument.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
50
- argument.callee.name === 'Error') {
51
- context.report({
52
- node,
53
- messageId: 'useHttpsError',
54
- });
55
- }
56
- else if (hasFirebaseAdminImport &&
57
- ((argument.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
58
- argument.callee.name === 'HttpsError') ||
59
- (argument.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
60
- argument.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
61
- argument.callee.object.name === httpsIdentifier &&
62
- argument.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
63
- argument.callee.property.name === 'HttpsError'))) {
64
- context.report({
65
- node,
66
- messageId: 'useProprietaryHttpsError',
67
- });
68
- }
49
+ if (argument.type !== utils_1.AST_NODE_TYPES.NewExpression) {
50
+ return;
51
+ }
52
+ const callee = argument.callee;
53
+ // Check for direct Error usage
54
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
55
+ callee.name === 'Error') {
56
+ context.report({
57
+ node,
58
+ messageId: 'useHttpsError',
59
+ });
60
+ return;
61
+ }
62
+ // Check for firebase-admin HttpsError usage
63
+ if (!hasFirebaseAdminImport) {
64
+ return;
65
+ }
66
+ const isHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
67
+ callee.name === 'HttpsError';
68
+ const isFirebaseHttpsError = callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
69
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
70
+ callee.object.name === httpsIdentifier &&
71
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
72
+ callee.property.name === 'HttpsError';
73
+ if (isHttpsError || isFirebaseHttpsError) {
74
+ context.report({
75
+ node,
76
+ messageId: 'useProprietaryHttpsError',
77
+ });
69
78
  }
70
79
  },
71
80
  };
@@ -0,0 +1,7 @@
1
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useImageOptimized", {
2
+ componentPath: string;
3
+ }[], {
4
+ JSXElement(node: any): void;
5
+ ImportDeclaration(node: any): void;
6
+ }>;
7
+ export = _default;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ const createRule_1 = require("../utils/createRule");
3
+ module.exports = (0, createRule_1.createRule)({
4
+ name: 'require-image-optimized',
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Enforce using ImageOptimized component instead of next/image or img tags',
9
+ recommended: 'error',
10
+ requiresTypeChecking: false,
11
+ },
12
+ fixable: 'code',
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ componentPath: {
18
+ type: 'string',
19
+ description: 'The import path for the ImageOptimized component',
20
+ default: 'src/components/image/ImageOptimized',
21
+ },
22
+ },
23
+ additionalProperties: false,
24
+ },
25
+ ],
26
+ messages: {
27
+ useImageOptimized: 'Use ImageOptimized component from {{ componentPath }} instead of {{ component }}',
28
+ },
29
+ },
30
+ defaultOptions: [{ componentPath: 'src/components/image/ImageOptimized' }],
31
+ create(context) {
32
+ const options = context.options[0] || {
33
+ componentPath: 'src/components/image/ImageOptimized',
34
+ };
35
+ const sourceCode = context.getSourceCode();
36
+ return {
37
+ // Handle JSX img elements
38
+ JSXElement(node) {
39
+ if (node.openingElement.name.name === 'img') {
40
+ context.report({
41
+ node,
42
+ messageId: 'useImageOptimized',
43
+ data: {
44
+ componentPath: options.componentPath,
45
+ component: 'img tag',
46
+ },
47
+ fix(fixer) {
48
+ const attributes = node.openingElement.attributes
49
+ .map((attr) => sourceCode.getText(attr))
50
+ .join(' ');
51
+ return fixer.replaceText(node, `<ImageOptimized ${attributes} />`);
52
+ },
53
+ });
54
+ }
55
+ },
56
+ // Handle next/image imports and usage
57
+ ImportDeclaration(node) {
58
+ if (node.source.value === 'next/image' && node.specifiers.length > 0) {
59
+ const imageSpecifier = node.specifiers.find((spec) => (spec.type === 'ImportDefaultSpecifier' ||
60
+ spec.type === 'ImportSpecifier') &&
61
+ (spec.local.name === 'Image' || spec.imported?.name === 'Image'));
62
+ if (imageSpecifier) {
63
+ const localName = imageSpecifier.local.name;
64
+ // Report the import
65
+ context.report({
66
+ node,
67
+ messageId: 'useImageOptimized',
68
+ data: {
69
+ componentPath: options.componentPath,
70
+ component: 'next/image',
71
+ },
72
+ fix(fixer) {
73
+ return fixer.replaceText(node, `import ${localName} from '${options.componentPath}';`);
74
+ },
75
+ });
76
+ }
77
+ }
78
+ },
79
+ };
80
+ },
81
+ });
82
+ //# sourceMappingURL=require-image-optimized.js.map
@@ -0,0 +1 @@
1
+ export declare const semanticFunctionPrefixes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"avoidGenericPrefix", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.semanticFunctionPrefixes = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const DISALLOWED_PREFIXES = new Set(['get', 'update', 'check', 'manage', 'process', 'do']);
7
+ const SUGGESTED_ALTERNATIVES = {
8
+ get: ['fetch', 'retrieve', 'compute', 'derive'],
9
+ update: ['modify', 'set', 'apply'],
10
+ check: ['validate', 'assert', 'ensure'],
11
+ manage: ['control', 'coordinate', 'schedule'],
12
+ process: ['transform', 'sanitize', 'compute'],
13
+ do: ['execute', 'perform', 'apply'],
14
+ };
15
+ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
16
+ name: 'semantic-function-prefixes',
17
+ meta: {
18
+ type: 'suggestion',
19
+ docs: {
20
+ description: 'Enforce semantic function prefixes over generic ones like "get" and "update"',
21
+ recommended: 'error',
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ avoidGenericPrefix: 'Avoid using generic prefix "{{prefix}}". Consider using one of these alternatives: {{alternatives}}',
26
+ },
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ function checkFunctionName(node) {
31
+ // Skip anonymous functions
32
+ if (!node.id && node.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
33
+ return;
34
+ }
35
+ // Get function name from either the function declaration or variable declarator
36
+ let functionName = '';
37
+ if (node.id) {
38
+ functionName = node.id.name;
39
+ }
40
+ else if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
41
+ functionName = node.parent.id.name;
42
+ }
43
+ if (!functionName)
44
+ return;
45
+ // Skip if function starts with 'is' (boolean check functions are okay)
46
+ if (functionName.startsWith('is'))
47
+ return;
48
+ // Skip class getters
49
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition && node.parent.kind === 'get') {
50
+ return;
51
+ }
52
+ // Check for disallowed prefixes
53
+ for (const prefix of DISALLOWED_PREFIXES) {
54
+ if (functionName.toLowerCase().startsWith(prefix.toLowerCase())) {
55
+ context.report({
56
+ node: node.id || node,
57
+ messageId: 'avoidGenericPrefix',
58
+ data: {
59
+ prefix,
60
+ alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
61
+ },
62
+ });
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ return {
68
+ FunctionDeclaration: checkFunctionName,
69
+ FunctionExpression: checkFunctionName,
70
+ ArrowFunctionExpression: checkFunctionName,
71
+ };
72
+ },
73
+ });
74
+ //# sourceMappingURL=semantic-function-prefixes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -45,6 +45,7 @@
45
45
  "commitlint": "commitlint --edit"
46
46
  },
47
47
  "dependencies": {
48
+ "compromise": "14.14.4",
48
49
  "requireindex": "1.2.0"
49
50
  },
50
51
  "devDependencies": {