@blumintinc/eslint-plugin-blumint 1.0.4 → 1.1.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 (46) hide show
  1. package/README.md +1 -0
  2. package/lib/index.js +44 -2
  3. package/lib/rules/class-methods-read-top-to-bottom.js +1 -0
  4. package/lib/rules/consistent-callback-naming.d.ts +2 -0
  5. package/lib/rules/consistent-callback-naming.js +84 -0
  6. package/lib/rules/dynamic-https-errors.js +0 -1
  7. package/lib/rules/enforce-callable-types.d.ts +3 -0
  8. package/lib/rules/enforce-callable-types.js +97 -0
  9. package/lib/rules/enforce-callback-memo.d.ts +3 -0
  10. package/lib/rules/enforce-callback-memo.js +79 -0
  11. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +4 -0
  12. package/lib/rules/enforce-dynamic-firebase-imports.js +49 -0
  13. package/lib/rules/enforce-identifiable-firestore-type.d.ts +3 -0
  14. package/lib/rules/enforce-identifiable-firestore-type.js +89 -0
  15. package/lib/rules/enforce-serializable-params.d.ts +9 -0
  16. package/lib/rules/enforce-serializable-params.js +127 -0
  17. package/lib/rules/export-if-in-doubt.js +0 -1
  18. package/lib/rules/extract-global-constants.js +2 -0
  19. package/lib/rules/global-const-style.d.ts +5 -0
  20. package/lib/rules/global-const-style.js +77 -0
  21. package/lib/rules/no-conditional-literals-in-jsx.js +0 -1
  22. package/lib/rules/no-jsx-whitespace-literal.d.ts +1 -0
  23. package/lib/rules/no-jsx-whitespace-literal.js +34 -0
  24. package/lib/rules/no-unpinned-dependencies.js +1 -0
  25. package/lib/rules/no-unused-props.d.ts +6 -0
  26. package/lib/rules/no-unused-props.js +91 -0
  27. package/lib/rules/no-useless-fragment.js +2 -0
  28. package/lib/rules/prefer-fragment-shorthand.js +1 -0
  29. package/lib/rules/prefer-type-over-interface.js +3 -1
  30. package/lib/rules/require-dynamic-firebase-imports.d.ts +6 -0
  31. package/lib/rules/require-dynamic-firebase-imports.js +84 -0
  32. package/lib/rules/require-https-error.d.ts +6 -0
  33. package/lib/rules/require-https-error.js +74 -0
  34. package/lib/rules/require-image-overlayed.d.ts +7 -0
  35. package/lib/rules/require-image-overlayed.js +80 -0
  36. package/lib/rules/require-memo.js +5 -3
  37. package/lib/rules/require-usememo-object-literals.d.ts +4 -0
  38. package/lib/rules/require-usememo-object-literals.js +53 -0
  39. package/lib/rules/use-custom-link.d.ts +1 -0
  40. package/lib/rules/use-custom-link.js +49 -0
  41. package/lib/rules/use-custom-router.d.ts +1 -0
  42. package/lib/rules/use-custom-router.js +65 -0
  43. package/lib/utils/ASTHelpers.js +2 -0
  44. package/lib/utils/graph/ClassGraphBuilder.js +4 -1
  45. package/lib/utils/graph/ClassGraphSorterReadability.js +8 -3
  46. package/package.json +3 -4
@@ -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
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requireMemo = void 0;
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
4
6
  const utils_1 = require("@typescript-eslint/utils");
5
7
  const ASTHelpers_1 = require("../utils/ASTHelpers");
6
8
  const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
@@ -43,7 +45,7 @@ const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
43
45
  };
44
46
  function isMemoImport(importPath) {
45
47
  // Match both absolute and relative paths ending with util/memo
46
- return /(?:^|\/)util\/memo$/.test(importPath);
48
+ return /(?:^|\/|\\)util\/memo$/.test(importPath);
47
49
  }
48
50
  function checkFunction(context, node) {
49
51
  const fileName = context.getFilename();
@@ -135,7 +137,7 @@ function calculateImportPath(currentFilePath) {
135
137
  // Calculate relative path based on current file depth from src
136
138
  // Subtract 1 from depth to exclude the filename itself
137
139
  const depth = parts.length - (srcIndex + 1) - 1;
138
- return '../'.repeat(depth) + 'util/memo';
140
+ return depth > 0 ? '../'.repeat(depth) + 'util/memo' : './util/memo';
139
141
  }
140
142
  exports.requireMemo = {
141
143
  create: (context) => ({
@@ -156,7 +158,7 @@ exports.requireMemo = {
156
158
  recommended: 'error',
157
159
  },
158
160
  messages: {
159
- requireMemo: 'Component definition not wrapped in React.memo()',
161
+ requireMemo: 'Component definition not wrapped in memo()',
160
162
  },
161
163
  schema: [],
162
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,53 @@
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
+ const { expression } = node.value;
27
+ // Check if the expression is an object or array literal
28
+ if ((expression.type === 'ObjectExpression' ||
29
+ expression.type === 'ArrayExpression') &&
30
+ // Ensure we're in a function component context
31
+ context
32
+ .getAncestors()
33
+ .some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
34
+ ancestor.type === 'ArrowFunctionExpression' ||
35
+ ancestor.type === 'FunctionExpression')) {
36
+ // Check if the parent component name starts with an uppercase letter
37
+ // to ensure it's a React component
38
+ const jsxElement = node.parent;
39
+ const elementName = jsxElement.name.type === 'JSXIdentifier'
40
+ ? jsxElement.name.name
41
+ : '';
42
+ if (elementName && /^[A-Z]/.test(elementName)) {
43
+ context.report({
44
+ node: expression,
45
+ messageId: 'requireUseMemo',
46
+ });
47
+ }
48
+ }
49
+ },
50
+ };
51
+ },
52
+ });
53
+ //# 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
@@ -84,6 +84,8 @@ class ASTHelpers {
84
84
  return (this.declarationIncludesIdentifier(node.test) ||
85
85
  this.declarationIncludesIdentifier(node.consequent) ||
86
86
  this.declarationIncludesIdentifier(node.alternate));
87
+ case 'TemplateLiteral':
88
+ return node.expressions.some((expr) => this.declarationIncludesIdentifier(expr));
87
89
  case 'TSAsExpression':
88
90
  return this.declarationIncludesIdentifier(node.expression);
89
91
  default:
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClassGraphBuilder = void 0;
4
- /* eslint-disable security/detect-object-injection */
5
4
  const utils_1 = require("@typescript-eslint/utils");
6
5
  const ClassGraphSorterReadability_1 = require("./ClassGraphSorterReadability");
7
6
  const ASTHelpers_1 = require("../ASTHelpers");
@@ -66,6 +65,10 @@ class ClassGraphBuilder {
66
65
  addDependencies(node, methodName) {
67
66
  const newDependencies = ASTHelpers_1.ASTHelpers.classMethodDependenciesOf(node, this.graph, this.className).filter((name) => !!this.graph[name] && name !== methodName);
68
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
+ }
69
72
  this.graph[methodName].dependencies.push(...newDependencies);
70
73
  }
71
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.4",
3
+ "version": "1.1.0",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -29,7 +29,6 @@
29
29
  "lint": "npm-run-all \"lint:*\"",
30
30
  "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
31
31
  "lint:js": "eslint ./src",
32
- "lint:md": "remark .",
33
32
  "lint:shell": "shellcheck .devcontainer/git-flow-completion.bash",
34
33
  "lint:fix": "tsc && eslint ./**/* --quiet --fix",
35
34
  "test": "jest --passWithNoTests --reporters=default --reporters=jest-junit",
@@ -64,7 +63,7 @@
64
63
  "cz-conventional-changelog": "3.3.0",
65
64
  "del-cli": "4.0.1",
66
65
  "dotenv-cli": "5.0.0",
67
- "eslint": "8.11.0",
66
+ "eslint": "8.57.0",
68
67
  "eslint-config-prettier": "8.5.0",
69
68
  "eslint-doc-generator": "1.0.0",
70
69
  "eslint-import-resolver-typescript": "3.5.5",
@@ -94,7 +93,7 @@
94
93
  "eslint": ">=7"
95
94
  },
96
95
  "engines": {
97
- "node": "^20.0.0"
96
+ "node": "^22.0.0"
98
97
  },
99
98
  "config": {
100
99
  "commitizen": {