@blumintinc/eslint-plugin-blumint 1.1.4 → 1.1.6

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/lib/index.js CHANGED
@@ -34,10 +34,12 @@ const require_image_overlayed_1 = __importDefault(require("./rules/require-image
34
34
  const require_usememo_object_literals_1 = require("./rules/require-usememo-object-literals");
35
35
  const enforce_safe_stringify_1 = require("./rules/enforce-safe-stringify");
36
36
  const avoid_utils_directory_1 = require("./rules/avoid-utils-directory");
37
+ const no_entire_object_hook_deps_1 = require("./rules/no-entire-object-hook-deps");
38
+ const enforce_firestore_path_utils_1 = require("./rules/enforce-firestore-path-utils");
37
39
  module.exports = {
38
40
  meta: {
39
41
  name: '@blumintinc/eslint-plugin-blumint',
40
- version: '1.1.4',
42
+ version: '1.1.6',
41
43
  },
42
44
  parseOptions: {
43
45
  ecmaVersion: 2020,
@@ -47,6 +49,7 @@ module.exports = {
47
49
  plugins: ['@blumintinc/blumint'],
48
50
  rules: {
49
51
  '@blumintinc/blumint/avoid-utils-directory': 'error',
52
+ '@blumintinc/blumint/enforce-firestore-path-utils': 'error',
50
53
  '@blumintinc/blumint/no-jsx-whitespace-literal': 'error',
51
54
  '@blumintinc/blumint/array-methods-this-context': 'warn',
52
55
  '@blumintinc/blumint/class-methods-read-top-to-bottom': 'warn',
@@ -77,6 +80,7 @@ module.exports = {
77
80
  '@blumintinc/blumint/require-image-overlayed': 'error',
78
81
  '@blumintinc/blumint/require-usememo-object-literals': 'error',
79
82
  '@blumintinc/blumint/enforce-safe-stringify': 'error',
83
+ '@blumintinc/blumint/no-entire-object-hook-deps': 'error',
80
84
  },
81
85
  },
82
86
  },
@@ -112,6 +116,8 @@ module.exports = {
112
116
  'require-usememo-object-literals': require_usememo_object_literals_1.requireUseMemoObjectLiterals,
113
117
  'enforce-safe-stringify': enforce_safe_stringify_1.enforceStableStringify,
114
118
  'avoid-utils-directory': avoid_utils_directory_1.avoidUtilsDirectory,
119
+ 'no-entire-object-hook-deps': no_entire_object_hook_deps_1.noEntireObjectHookDeps,
120
+ 'enforce-firestore-path-utils': enforce_firestore_path_utils_1.enforceFirestorePathUtils,
115
121
  },
116
122
  };
117
123
  //# sourceMappingURL=index.js.map
@@ -56,19 +56,31 @@ module.exports = (0, createRule_1.createRule)({
56
56
  // Check if type is a React component type
57
57
  const isComponent = symbol.declarations?.some((decl) => {
58
58
  const declaration = decl;
59
- if (ts.isClassDeclaration(declaration) ||
60
- ts.isInterfaceDeclaration(declaration)) {
59
+ // Check for JSX element types
60
+ if (ts.isTypeAliasDeclaration(declaration)) {
61
+ const typeText = declaration.type.getText();
62
+ return typeText.includes('JSX.Element') || typeText.includes('ReactElement');
63
+ }
64
+ // Check for class/interface component patterns
65
+ if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
61
66
  const name = declaration.name?.text ?? '';
62
- return (
63
- // Check for common React component patterns
64
- name.includes('Component') ||
67
+ return (name.includes('Component') ||
65
68
  name.includes('Element') ||
66
69
  name.includes('FC') ||
67
70
  name.includes('FunctionComponent'));
68
71
  }
69
72
  return false;
70
73
  });
71
- return isComponent || false;
74
+ // Check if the type itself is a component or element type
75
+ const typeString = checker.typeToString(type);
76
+ const isComponentType = (typeString.includes('JSX.Element') ||
77
+ typeString.includes('ReactElement') ||
78
+ typeString.includes('Component') ||
79
+ typeString.includes('FC'));
80
+ return isComponent || isComponentType;
81
+ }
82
+ function isPascalCase(str) {
83
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str);
72
84
  }
73
85
  function isFunctionType(node) {
74
86
  const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
@@ -85,6 +97,38 @@ module.exports = (0, createRule_1.createRule)({
85
97
  if (propName?.match(/^on[A-Z]/)) {
86
98
  return;
87
99
  }
100
+ // Skip PascalCase props as they typically represent components or component-related props
101
+ if (propName && isPascalCase(propName)) {
102
+ return;
103
+ }
104
+ // Skip common non-callback props
105
+ const commonNonCallbackProps = new Set([
106
+ 'theme',
107
+ 'style',
108
+ 'className',
109
+ 'ref',
110
+ 'key',
111
+ 'component',
112
+ 'as',
113
+ 'sx',
114
+ 'css', // Emotion css prop
115
+ ]);
116
+ if (propName && commonNonCallbackProps.has(propName)) {
117
+ return;
118
+ }
119
+ // Skip props on components that commonly use function props that aren't callbacks
120
+ const parentName = node.parent?.name;
121
+ const componentName = parentName?.type === 'JSXIdentifier' ? parentName.name : undefined;
122
+ const componentsWithFunctionProps = new Set([
123
+ 'ThemeProvider',
124
+ 'Transition',
125
+ 'CSSTransition',
126
+ 'TransitionGroup',
127
+ 'SwitchTransition', // React Transition Group
128
+ ]);
129
+ if (componentName && componentsWithFunctionProps.has(componentName)) {
130
+ return;
131
+ }
88
132
  // Check if the value is a function type and not a React component
89
133
  if (isFunctionType(node.value.expression) &&
90
134
  propName &&
@@ -0,0 +1 @@
1
+ export declare const enforceFirestorePathUtils: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requirePathUtil", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceFirestorePathUtils = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const FIRESTORE_METHODS = new Set(['doc', 'collection']);
7
+ exports.enforceFirestorePathUtils = (0, createRule_1.createRule)({
8
+ name: 'enforce-firestore-path-utils',
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: {
12
+ description: 'Enforce usage of utility functions for Firestore paths',
13
+ recommended: 'error',
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ requirePathUtil: 'Use a utility function (e.g., toUserPath, toCollectionPath) for Firestore paths instead of string literals',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ function isFirestoreCall(node) {
23
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
24
+ return false;
25
+ }
26
+ const property = node.callee.property;
27
+ if (property.type !== utils_1.AST_NODE_TYPES.Identifier) {
28
+ return false;
29
+ }
30
+ return FIRESTORE_METHODS.has(property.name);
31
+ }
32
+ function isStringLiteralOrTemplate(node) {
33
+ return (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string' ||
34
+ node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
35
+ }
36
+ function isUtilityFunction(node) {
37
+ if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
38
+ return false;
39
+ }
40
+ const callee = node.callee;
41
+ if (callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
42
+ return false;
43
+ }
44
+ // Match functions starting with 'to' and ending with 'Path'
45
+ return /^to.*Path$/.test(callee.name);
46
+ }
47
+ return {
48
+ CallExpression(node) {
49
+ if (!isFirestoreCall(node)) {
50
+ return;
51
+ }
52
+ // Check first argument of doc() or collection() call
53
+ const pathArg = node.arguments[0];
54
+ if (!pathArg) {
55
+ return;
56
+ }
57
+ // Skip if it's already using a utility function
58
+ if (isUtilityFunction(pathArg)) {
59
+ return;
60
+ }
61
+ // Skip if it's a variable or other non-literal expression
62
+ if (!isStringLiteralOrTemplate(pathArg)) {
63
+ return;
64
+ }
65
+ // Skip test files
66
+ const filename = context.getFilename();
67
+ if (filename.includes('__tests__') || filename.includes('.test.') || filename.includes('.spec.')) {
68
+ return;
69
+ }
70
+ context.report({
71
+ node: pathArg,
72
+ messageId: 'requirePathUtil',
73
+ });
74
+ },
75
+ };
76
+ },
77
+ });
78
+ //# sourceMappingURL=enforce-firestore-path-utils.js.map
@@ -0,0 +1 @@
1
+ export declare const enforceRealtimedbPathUtils: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requirePathUtil", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceRealtimedbPathUtils = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const RTDB_METHODS = new Set(['ref', 'child']);
7
+ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
8
+ name: 'enforce-realtimedb-path-utils',
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: {
12
+ description: 'Enforce usage of utility functions for Realtime Database paths',
13
+ recommended: 'error',
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ requirePathUtil: 'Use a utility function (e.g., toUserPath, toItemPath) for Realtime Database paths instead of string literals',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ function isRTDBCall(node) {
23
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
24
+ return false;
25
+ }
26
+ const property = node.callee.property;
27
+ if (property.type !== utils_1.AST_NODE_TYPES.Identifier) {
28
+ return false;
29
+ }
30
+ // Check for both frontend and backend SDK patterns
31
+ if (!RTDB_METHODS.has(property.name)) {
32
+ return false;
33
+ }
34
+ // Check if it's a Firebase RTDB call by looking at the chain
35
+ let current = node.callee.object;
36
+ while (current) {
37
+ if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
38
+ if (current.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
39
+ const method = current.callee.property;
40
+ if (method.type === utils_1.AST_NODE_TYPES.Identifier &&
41
+ method.name === 'database') {
42
+ return true;
43
+ }
44
+ }
45
+ current = current.callee;
46
+ }
47
+ else if (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
48
+ // Handle chained calls like ref().child()
49
+ current = current.object;
50
+ }
51
+ else {
52
+ break;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ function isStringLiteralOrTemplate(node) {
58
+ return ((node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') ||
59
+ node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
60
+ }
61
+ function isUtilityFunction(node) {
62
+ if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
63
+ return false;
64
+ }
65
+ const callee = node.callee;
66
+ if (callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
67
+ return false;
68
+ }
69
+ // Match functions starting with 'to' and ending with 'Path'
70
+ return /^to.*Path$/.test(callee.name);
71
+ }
72
+ return {
73
+ CallExpression(node) {
74
+ if (!isRTDBCall(node)) {
75
+ return;
76
+ }
77
+ // Check first argument of ref() or child() call
78
+ const pathArg = node.arguments[0];
79
+ if (!pathArg) {
80
+ return;
81
+ }
82
+ // Skip if it's already using a utility function
83
+ if (isUtilityFunction(pathArg)) {
84
+ return;
85
+ }
86
+ // Skip if it's a variable or other non-literal expression
87
+ if (!isStringLiteralOrTemplate(pathArg)) {
88
+ return;
89
+ }
90
+ // Skip test files
91
+ const filename = context.getFilename();
92
+ if (filename.includes('__tests__') ||
93
+ filename.includes('.test.') ||
94
+ filename.includes('.spec.') ||
95
+ filename.includes('mocks')) {
96
+ return;
97
+ }
98
+ context.report({
99
+ node: pathArg,
100
+ messageId: 'requirePathUtil',
101
+ });
102
+ },
103
+ };
104
+ },
105
+ });
106
+ //# sourceMappingURL=enforce-realtimedb-path-utils.js.map
@@ -0,0 +1 @@
1
+ export declare const noEntireObjectHookDeps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"avoidEntireObject", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noEntireObjectHookDeps = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
7
+ function isHookCall(node) {
8
+ const callee = node.callee;
9
+ return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
10
+ HOOK_NAMES.has(callee.name));
11
+ }
12
+ function getObjectUsagesInHook(hookBody, objectName) {
13
+ const usages = new Set();
14
+ const visited = new Set();
15
+ function buildAccessPath(node) {
16
+ const parts = [];
17
+ let current = node;
18
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
19
+ if (current.computed) {
20
+ return null; // Skip computed properties
21
+ }
22
+ if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
23
+ return null;
24
+ }
25
+ parts.unshift(current.property.name);
26
+ current = current.object;
27
+ }
28
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier && current.name === objectName) {
29
+ parts.unshift(objectName);
30
+ return parts.join('.');
31
+ }
32
+ return null;
33
+ }
34
+ function visit(node) {
35
+ if (visited.has(node)) {
36
+ return;
37
+ }
38
+ visited.add(node);
39
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression && !node.computed) {
40
+ const accessPath = buildAccessPath(node);
41
+ if (accessPath) {
42
+ usages.add(accessPath);
43
+ }
44
+ }
45
+ // Visit children
46
+ switch (node.type) {
47
+ case utils_1.AST_NODE_TYPES.BlockStatement:
48
+ case utils_1.AST_NODE_TYPES.Program:
49
+ node.body.forEach(child => visit(child));
50
+ break;
51
+ case utils_1.AST_NODE_TYPES.ExpressionStatement:
52
+ visit(node.expression);
53
+ break;
54
+ case utils_1.AST_NODE_TYPES.CallExpression:
55
+ visit(node.callee);
56
+ node.arguments.forEach(arg => visit(arg));
57
+ break;
58
+ case utils_1.AST_NODE_TYPES.MemberExpression:
59
+ visit(node.object);
60
+ if (!node.computed) {
61
+ visit(node.property);
62
+ }
63
+ break;
64
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
65
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
66
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
67
+ visit(node.body);
68
+ }
69
+ else {
70
+ visit(node.body);
71
+ }
72
+ break;
73
+ case utils_1.AST_NODE_TYPES.ReturnStatement:
74
+ if (node.argument) {
75
+ visit(node.argument);
76
+ }
77
+ break;
78
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
79
+ visit(node.test);
80
+ visit(node.consequent);
81
+ visit(node.alternate);
82
+ break;
83
+ case utils_1.AST_NODE_TYPES.LogicalExpression:
84
+ case utils_1.AST_NODE_TYPES.BinaryExpression:
85
+ visit(node.left);
86
+ visit(node.right);
87
+ break;
88
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
89
+ node.expressions.forEach(expr => visit(expr));
90
+ break;
91
+ }
92
+ }
93
+ visit(hookBody);
94
+ // Filter out intermediate paths
95
+ const paths = Array.from(usages);
96
+ const filteredPaths = paths.filter(path => !paths.some(otherPath => otherPath !== path && otherPath.startsWith(path + '.')));
97
+ return new Set(filteredPaths);
98
+ }
99
+ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
100
+ name: 'no-entire-object-hook-deps',
101
+ meta: {
102
+ type: 'suggestion',
103
+ docs: {
104
+ description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used',
105
+ recommended: 'error',
106
+ },
107
+ fixable: 'code',
108
+ schema: [],
109
+ messages: {
110
+ avoidEntireObject: 'Avoid using entire object "{{objectName}}" in dependency array. Use specific fields: {{fields}}',
111
+ },
112
+ },
113
+ defaultOptions: [],
114
+ create(context) {
115
+ return {
116
+ CallExpression(node) {
117
+ if (!isHookCall(node)) {
118
+ return;
119
+ }
120
+ // Get the dependency array argument
121
+ const depsArg = node.arguments[node.arguments.length - 1];
122
+ if (!depsArg ||
123
+ depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
124
+ return;
125
+ }
126
+ // Get the hook callback function
127
+ const callbackArg = node.arguments[0];
128
+ if (!callbackArg ||
129
+ (callbackArg.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
130
+ callbackArg.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
131
+ return;
132
+ }
133
+ // Check each dependency in the array
134
+ depsArg.elements.forEach(element => {
135
+ if (element &&
136
+ element.type === utils_1.AST_NODE_TYPES.Identifier) {
137
+ const objectName = element.name;
138
+ const usages = getObjectUsagesInHook(callbackArg.body, objectName);
139
+ // If we found specific field usages and the entire object is in deps
140
+ if (usages.size > 0) {
141
+ const fields = Array.from(usages).join(', ');
142
+ context.report({
143
+ node: element,
144
+ messageId: 'avoidEntireObject',
145
+ data: {
146
+ objectName,
147
+ fields,
148
+ },
149
+ fix(fixer) {
150
+ // Only provide fix if we have specific fields to suggest
151
+ if (usages.size > 0) {
152
+ return fixer.replaceText(element, Array.from(usages).join(', '));
153
+ }
154
+ return null;
155
+ },
156
+ });
157
+ }
158
+ }
159
+ });
160
+ },
161
+ };
162
+ },
163
+ });
164
+ //# sourceMappingURL=no-entire-object-hook-deps.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -36,23 +36,28 @@
36
36
  "docs": "./scripts/make-docs.sh && npm run update:eslint-docs",
37
37
  "update:eslint-docs": "eslint-doc-generator",
38
38
  "build": "tsc",
39
- "prepare": "npm run build",
39
+ "prepare": "husky install && npm run build",
40
40
  "version": "git add -A src",
41
41
  "postversion": "git push && git push --tags",
42
42
  "remove-hooks": "del-cli ./.husky/",
43
43
  "release:dry-run": "semantic-release --dry-run --no-ci",
44
- "test:release": "node scripts/test-release.js"
44
+ "test:release": "node scripts/test-release.js",
45
+ "commitlint": "commitlint --edit"
45
46
  },
46
47
  "dependencies": {
47
48
  "requireindex": "1.2.0"
48
49
  },
49
50
  "devDependencies": {
51
+ "@commitlint/cli": "19.6.1",
52
+ "@commitlint/config-conventional": "19.6.0",
50
53
  "@semantic-release/changelog": "6.0.1",
51
54
  "@semantic-release/commit-analyzer": "9.0.2",
55
+ "@semantic-release/exec": "6.0.3",
52
56
  "@semantic-release/git": "10.0.1",
53
57
  "@semantic-release/npm": "9.0.1",
54
58
  "@semantic-release/release-notes-generator": "10.0.3",
55
59
  "@types/eslint": "8.37.0",
60
+ "@types/jest": "29.5.14",
56
61
  "@types/node": "18.7.13",
57
62
  "@typescript-eslint/eslint-plugin": "5.34.0",
58
63
  "@typescript-eslint/parser": "5.48.0",
@@ -69,11 +74,12 @@
69
74
  "eslint-import-resolver-typescript": "3.5.5",
70
75
  "eslint-plugin-eslint-plugin": "5.0.0",
71
76
  "eslint-plugin-import": "2.26.0",
72
- "eslint-plugin-node": "11.1.0",
73
77
  "eslint-plugin-jsdoc": "44.0.0",
78
+ "eslint-plugin-node": "11.1.0",
74
79
  "eslint-plugin-prettier": "4.2.1",
75
80
  "eslint-plugin-security": "1.5.0",
76
81
  "fs": "0.0.1-security",
82
+ "husky": "9.1.7",
77
83
  "jest": "29.3.1",
78
84
  "jest-junit": "14.0.0",
79
85
  "jsonc-eslint-parser": "2.3.0",
@@ -85,9 +91,7 @@
85
91
  "semantic-release": "19.0.3",
86
92
  "ts-jest": "29.0.5",
87
93
  "ts-node": "10.9.1",
88
- "typescript": "4.9.5",
89
- "@types/jest": "^29.3.1",
90
- "@semantic-release/exec": "^6.0.3"
94
+ "typescript": "4.9.5"
91
95
  },
92
96
  "peerDependencies": {
93
97
  "eslint": ">=7"