@blumintinc/eslint-plugin-blumint 1.0.5 → 1.1.1

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 (38) hide show
  1. package/README.md +1 -0
  2. package/lib/index.js +44 -2
  3. package/lib/rules/consistent-callback-naming.d.ts +2 -0
  4. package/lib/rules/consistent-callback-naming.js +93 -0
  5. package/lib/rules/enforce-callable-types.d.ts +3 -0
  6. package/lib/rules/enforce-callable-types.js +97 -0
  7. package/lib/rules/enforce-callback-memo.d.ts +3 -0
  8. package/lib/rules/enforce-callback-memo.js +79 -0
  9. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +4 -0
  10. package/lib/rules/enforce-dynamic-firebase-imports.js +49 -0
  11. package/lib/rules/enforce-identifiable-firestore-type.d.ts +3 -0
  12. package/lib/rules/enforce-identifiable-firestore-type.js +130 -0
  13. package/lib/rules/enforce-serializable-params.d.ts +9 -0
  14. package/lib/rules/enforce-serializable-params.js +127 -0
  15. package/lib/rules/extract-global-constants.js +27 -6
  16. package/lib/rules/global-const-style.d.ts +5 -0
  17. package/lib/rules/global-const-style.js +110 -0
  18. package/lib/rules/no-jsx-whitespace-literal.d.ts +1 -0
  19. package/lib/rules/no-jsx-whitespace-literal.js +34 -0
  20. package/lib/rules/no-unused-props.d.ts +6 -0
  21. package/lib/rules/no-unused-props.js +91 -0
  22. package/lib/rules/require-dynamic-firebase-imports.d.ts +6 -0
  23. package/lib/rules/require-dynamic-firebase-imports.js +85 -0
  24. package/lib/rules/require-https-error.d.ts +6 -0
  25. package/lib/rules/require-https-error.js +74 -0
  26. package/lib/rules/require-image-overlayed.d.ts +7 -0
  27. package/lib/rules/require-image-overlayed.js +80 -0
  28. package/lib/rules/require-memo.js +3 -3
  29. package/lib/rules/require-usememo-object-literals.d.ts +4 -0
  30. package/lib/rules/require-usememo-object-literals.js +58 -0
  31. package/lib/rules/use-custom-link.d.ts +1 -0
  32. package/lib/rules/use-custom-link.js +49 -0
  33. package/lib/rules/use-custom-router.d.ts +1 -0
  34. package/lib/rules/use-custom-router.js +65 -0
  35. package/lib/utils/ASTHelpers.js +5 -0
  36. package/lib/utils/graph/ClassGraphBuilder.js +4 -0
  37. package/lib/utils/graph/ClassGraphSorterReadability.js +8 -3
  38. package/package.json +2 -2
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ const createRule_1 = require("../utils/createRule");
3
+ module.exports = (0, createRule_1.createRule)({
4
+ name: 'require-image-overlayed',
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Enforce using ImageOverlayed component instead of next/image or img tags',
9
+ recommended: 'error',
10
+ },
11
+ fixable: 'code',
12
+ schema: [
13
+ {
14
+ type: 'object',
15
+ properties: {
16
+ componentPath: {
17
+ type: 'string',
18
+ default: 'src/components/ImageOverlayed',
19
+ },
20
+ },
21
+ additionalProperties: false,
22
+ },
23
+ ],
24
+ messages: {
25
+ useImageOverlayed: 'Use ImageOverlayed component from {{ componentPath }} instead of {{ component }}',
26
+ },
27
+ },
28
+ defaultOptions: [{ componentPath: 'src/components/ImageOverlayed' }],
29
+ create(context) {
30
+ const options = context.options[0] || {
31
+ componentPath: 'src/components/ImageOverlayed',
32
+ };
33
+ const sourceCode = context.getSourceCode();
34
+ return {
35
+ // Handle JSX img elements
36
+ JSXElement(node) {
37
+ if (node.openingElement.name.name === 'img') {
38
+ context.report({
39
+ node,
40
+ messageId: 'useImageOverlayed',
41
+ data: {
42
+ componentPath: options.componentPath,
43
+ component: 'img tag',
44
+ },
45
+ fix(fixer) {
46
+ const attributes = node.openingElement.attributes
47
+ .map((attr) => sourceCode.getText(attr))
48
+ .join(' ');
49
+ return fixer.replaceText(node, `<ImageOverlayed ${attributes} />`);
50
+ },
51
+ });
52
+ }
53
+ },
54
+ // Handle next/image imports and usage
55
+ ImportDeclaration(node) {
56
+ if (node.source.value === 'next/image' && node.specifiers.length > 0) {
57
+ const imageSpecifier = node.specifiers.find((spec) => (spec.type === 'ImportDefaultSpecifier' ||
58
+ spec.type === 'ImportSpecifier') &&
59
+ (spec.local.name === 'Image' || spec.imported?.name === 'Image'));
60
+ if (imageSpecifier) {
61
+ const localName = imageSpecifier.local.name;
62
+ // Report the import
63
+ context.report({
64
+ node,
65
+ messageId: 'useImageOverlayed',
66
+ data: {
67
+ componentPath: options.componentPath,
68
+ component: 'next/image',
69
+ },
70
+ fix(fixer) {
71
+ return fixer.replaceText(node, `import ${localName} from '${options.componentPath}';`);
72
+ },
73
+ });
74
+ }
75
+ }
76
+ },
77
+ };
78
+ },
79
+ });
80
+ //# sourceMappingURL=require-image-overlayed.js.map
@@ -45,7 +45,7 @@ const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
45
45
  };
46
46
  function isMemoImport(importPath) {
47
47
  // Match both absolute and relative paths ending with util/memo
48
- return /(?:^|\/)util\/memo$/.test(importPath);
48
+ return /(?:^|\/|\\)util\/memo$/.test(importPath);
49
49
  }
50
50
  function checkFunction(context, node) {
51
51
  const fileName = context.getFilename();
@@ -137,7 +137,7 @@ function calculateImportPath(currentFilePath) {
137
137
  // Calculate relative path based on current file depth from src
138
138
  // Subtract 1 from depth to exclude the filename itself
139
139
  const depth = parts.length - (srcIndex + 1) - 1;
140
- return '../'.repeat(depth) + 'util/memo';
140
+ return depth > 0 ? '../'.repeat(depth) + 'util/memo' : './util/memo';
141
141
  }
142
142
  exports.requireMemo = {
143
143
  create: (context) => ({
@@ -158,7 +158,7 @@ exports.requireMemo = {
158
158
  recommended: 'error',
159
159
  },
160
160
  messages: {
161
- requireMemo: 'Component definition not wrapped in React.memo()',
161
+ requireMemo: 'Component definition not wrapped in memo()',
162
162
  },
163
163
  schema: [],
164
164
  fixable: 'code',
@@ -0,0 +1,4 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ export declare const requireUseMemoObjectLiterals: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireUseMemo", never[], {
3
+ JSXAttribute(node: TSESTree.JSXAttribute): void;
4
+ }>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireUseMemoObjectLiterals = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
6
+ name: 'require-usememo-object-literals',
7
+ meta: {
8
+ type: 'suggestion',
9
+ docs: {
10
+ description: 'Enforce using useMemo for inline object literals passed as props to JSX components',
11
+ recommended: 'error',
12
+ },
13
+ messages: {
14
+ requireUseMemo: 'Inline object/array literals in JSX props should be wrapped in useMemo to prevent unnecessary re-renders',
15
+ },
16
+ schema: [],
17
+ },
18
+ defaultOptions: [],
19
+ create(context) {
20
+ return {
21
+ JSXAttribute(node) {
22
+ // Skip if the value is not an expression
23
+ if (!node.value || node.value.type !== 'JSXExpressionContainer') {
24
+ return;
25
+ }
26
+ // Skip if the prop name is 'sx' or ends with 'Sx'
27
+ const propName = node.name.name;
28
+ if (typeof propName === 'string' && (propName === 'sx' || propName.endsWith('Sx'))) {
29
+ return;
30
+ }
31
+ const { expression } = node.value;
32
+ // Check if the expression is an object or array literal
33
+ if ((expression.type === 'ObjectExpression' ||
34
+ expression.type === 'ArrayExpression') &&
35
+ // Ensure we're in a function component context
36
+ context
37
+ .getAncestors()
38
+ .some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
39
+ ancestor.type === 'ArrowFunctionExpression' ||
40
+ ancestor.type === 'FunctionExpression')) {
41
+ // Check if the parent component name starts with an uppercase letter
42
+ // to ensure it's a React component
43
+ const jsxElement = node.parent;
44
+ const elementName = jsxElement.name.type === 'JSXIdentifier'
45
+ ? jsxElement.name.name
46
+ : '';
47
+ if (elementName && /^[A-Z]/.test(elementName)) {
48
+ context.report({
49
+ node: expression,
50
+ messageId: 'requireUseMemo',
51
+ });
52
+ }
53
+ }
54
+ },
55
+ };
56
+ },
57
+ });
58
+ //# sourceMappingURL=require-usememo-object-literals.js.map
@@ -0,0 +1 @@
1
+ export declare const useCustomLink: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useCustomLink", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCustomLink = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ exports.useCustomLink = (0, createRule_1.createRule)({
6
+ name: 'use-custom-link',
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description: 'Enforce using src/components/Link instead of next/link',
11
+ recommended: 'error',
12
+ },
13
+ fixable: 'code',
14
+ schema: [],
15
+ messages: {
16
+ useCustomLink: 'Import Link from src/components/Link instead of next/link',
17
+ },
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ return {
22
+ ImportDeclaration(node) {
23
+ if (node.source.value === 'next/link') {
24
+ const importSpecifiers = node.specifiers;
25
+ // Handle different import types (default, named, namespace)
26
+ const defaultSpecifier = importSpecifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
27
+ const defaultAsSpecifier = importSpecifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
28
+ specifier.imported.name === 'default');
29
+ if (defaultSpecifier || defaultAsSpecifier) {
30
+ context.report({
31
+ node,
32
+ messageId: 'useCustomLink',
33
+ fix(fixer) {
34
+ // Get the local name of the imported Link component
35
+ const localName = defaultSpecifier?.local?.name ||
36
+ defaultAsSpecifier?.local?.name ||
37
+ 'Link';
38
+ // Create the new import statement
39
+ const newImport = `import ${localName} from 'src/components/Link';`;
40
+ return fixer.replaceText(node, newImport);
41
+ },
42
+ });
43
+ }
44
+ }
45
+ },
46
+ };
47
+ },
48
+ });
49
+ //# sourceMappingURL=use-custom-link.js.map
@@ -0,0 +1 @@
1
+ export declare const useCustomRouter: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useCustomRouter", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCustomRouter = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.useCustomRouter = (0, createRule_1.createRule)({
7
+ name: 'use-custom-router',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce using src/hooks/routing/useRouter instead of next/router',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ useCustomRouter: 'Import useRouter from src/hooks/routing/useRouter instead of next/router',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ return {
23
+ ImportDeclaration(node) {
24
+ if (node.source.value === 'next/router') {
25
+ const specifiers = node.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
26
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
27
+ specifier.imported.name === 'useRouter');
28
+ if (specifiers.length > 0) {
29
+ context.report({
30
+ node,
31
+ messageId: 'useCustomRouter',
32
+ fix(fixer) {
33
+ // If there are other imports from next/router, keep them
34
+ const otherSpecifiers = node.specifiers.filter((specifier) => specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
35
+ (specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
36
+ specifier.imported.name !== 'useRouter'));
37
+ if (otherSpecifiers.length === 0) {
38
+ // If useRouter is the only import, replace the entire import
39
+ return fixer.replaceText(node, `import { ${specifiers
40
+ .map((s) => s.local.name !== s.imported.name
41
+ ? `useRouter as ${s.local.name}`
42
+ : 'useRouter')
43
+ .join(', ')} } from 'src/hooks/routing/useRouter';`);
44
+ }
45
+ else {
46
+ // Create a new import for useRouter and keep other imports
47
+ const useRouterImport = `import { ${specifiers
48
+ .map((s) => s.local.name !== s.imported.name
49
+ ? `useRouter as ${s.local.name}`
50
+ : 'useRouter')
51
+ .join(', ')} } from 'src/hooks/routing/useRouter';\n`;
52
+ const otherImports = `import { ${otherSpecifiers
53
+ .map((s) => s.local.name)
54
+ .join(', ')} } from 'next/router';`;
55
+ return fixer.replaceText(node, useRouterImport + otherImports);
56
+ }
57
+ },
58
+ });
59
+ }
60
+ }
61
+ },
62
+ };
63
+ },
64
+ });
65
+ //# sourceMappingURL=use-custom-router.js.map
@@ -75,6 +75,9 @@ class ASTHelpers {
75
75
  }
76
76
  return (this.declarationIncludesIdentifier(node.object) ||
77
77
  this.declarationIncludesIdentifier(node.property));
78
+ case 'ImportExpression':
79
+ // Dynamic imports should be considered as having dependencies
80
+ return true;
78
81
  case 'CallExpression':
79
82
  case 'NewExpression':
80
83
  // For function and constructor calls, we care about both the callee and the arguments.
@@ -84,6 +87,8 @@ class ASTHelpers {
84
87
  return (this.declarationIncludesIdentifier(node.test) ||
85
88
  this.declarationIncludesIdentifier(node.consequent) ||
86
89
  this.declarationIncludesIdentifier(node.alternate));
90
+ case 'TemplateLiteral':
91
+ return node.expressions.some((expr) => this.declarationIncludesIdentifier(expr));
87
92
  case 'TSAsExpression':
88
93
  return this.declarationIncludesIdentifier(node.expression);
89
94
  default:
@@ -65,6 +65,10 @@ class ClassGraphBuilder {
65
65
  addDependencies(node, methodName) {
66
66
  const newDependencies = ASTHelpers_1.ASTHelpers.classMethodDependenciesOf(node, this.graph, this.className).filter((name) => !!this.graph[name] && name !== methodName);
67
67
  if (this.graph[methodName]) {
68
+ // Ensure dependencies is initialized as an array
69
+ if (!Array.isArray(this.graph[methodName].dependencies)) {
70
+ this.graph[methodName].dependencies = [];
71
+ }
68
72
  this.graph[methodName].dependencies.push(...newDependencies);
69
73
  }
70
74
  }
@@ -66,13 +66,18 @@ class ClassGraphSorterReadability extends ClassGraphSorter_1.ClassGraphSorter {
66
66
  const visited = new Set();
67
67
  const dfsSortedNodes = [];
68
68
  const dfs = (node) => {
69
- if (visited.has(node.name) || !node) {
69
+ if (!node || visited.has(node.name)) {
70
70
  return;
71
71
  }
72
72
  visited.add(node.name);
73
73
  dfsSortedNodes.push(node);
74
- for (const dep of node.dependencies) {
75
- dfs(this.graph[String(dep)]);
74
+ // Ensure node.dependencies exists and is an array before iterating
75
+ const dependencies = Array.isArray(node.dependencies) ? node.dependencies : [];
76
+ for (const dep of dependencies) {
77
+ const depNode = this.graph[String(dep)];
78
+ if (depNode) {
79
+ dfs(depNode);
80
+ }
76
81
  }
77
82
  };
78
83
  searchNodes.forEach((node) => dfs(node));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -63,7 +63,7 @@
63
63
  "cz-conventional-changelog": "3.3.0",
64
64
  "del-cli": "4.0.1",
65
65
  "dotenv-cli": "5.0.0",
66
- "eslint": "8.11.0",
66
+ "eslint": "8.57.0",
67
67
  "eslint-config-prettier": "8.5.0",
68
68
  "eslint-doc-generator": "1.0.0",
69
69
  "eslint-import-resolver-typescript": "3.5.5",