@blumintinc/eslint-plugin-blumint 1.1.0 → 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.
- package/lib/index.js +1 -1
- package/lib/rules/consistent-callback-naming.js +26 -17
- package/lib/rules/enforce-identifiable-firestore-type.js +46 -5
- package/lib/rules/extract-global-constants.js +27 -6
- package/lib/rules/global-const-style.js +39 -6
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-usememo-object-literals.js +5 -0
- package/lib/utils/ASTHelpers.js +3 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -11,28 +11,37 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
11
11
|
fixable: 'code',
|
|
12
12
|
schema: [],
|
|
13
13
|
messages: {
|
|
14
|
-
callbackPropPrefix: 'Callback props must be prefixed with "on" (e.g., onClick, onChange)',
|
|
14
|
+
callbackPropPrefix: 'Callback props (function type props) must be prefixed with "on" (e.g., onClick, onChange)',
|
|
15
15
|
callbackFunctionPrefix: 'Callback functions should not use "handle" prefix, use descriptive verb phrases instead',
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
defaultOptions: [],
|
|
19
19
|
create(context) {
|
|
20
|
+
const parserServices = context.parserServices;
|
|
21
|
+
// Check if we have access to TypeScript services
|
|
22
|
+
if (!parserServices?.program || !parserServices?.esTreeNodeToTSNodeMap) {
|
|
23
|
+
throw new Error('You have to enable the `project` setting in parser options to use this rule');
|
|
24
|
+
}
|
|
25
|
+
const checker = parserServices.program.getTypeChecker();
|
|
26
|
+
function isFunctionType(node) {
|
|
27
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
28
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
29
|
+
return type.getCallSignatures().length > 0;
|
|
30
|
+
}
|
|
20
31
|
return {
|
|
21
32
|
// Check JSX attributes for callback props
|
|
22
33
|
JSXAttribute(node) {
|
|
23
|
-
if (node.value &&
|
|
24
|
-
node.value.type === 'JSXExpressionContainer' &&
|
|
34
|
+
if (node.value?.type === 'JSXExpressionContainer' &&
|
|
25
35
|
node.value.expression.type === 'Identifier') {
|
|
26
|
-
const propName = node.name.name;
|
|
27
|
-
const valueName = node.value.expression.name;
|
|
36
|
+
const propName = node.name.type === 'JSXIdentifier' ? node.name.name : undefined;
|
|
28
37
|
// Skip React's built-in event handlers
|
|
29
|
-
if (propName
|
|
38
|
+
if (propName?.match(/^on[A-Z]/)) {
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
32
|
-
// Check if
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// Check if the value is a function type
|
|
42
|
+
if (isFunctionType(node.value.expression) &&
|
|
43
|
+
propName &&
|
|
44
|
+
!propName.startsWith('on')) {
|
|
36
45
|
context.report({
|
|
37
46
|
node,
|
|
38
47
|
messageId: 'callbackPropPrefix',
|
|
@@ -47,8 +56,8 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
47
56
|
},
|
|
48
57
|
// Check function declarations and variable declarations for callback functions
|
|
49
58
|
'FunctionDeclaration, VariableDeclarator'(node) {
|
|
50
|
-
const functionName = node.id?.name;
|
|
51
|
-
if (functionName && functionName.startsWith('handle')) {
|
|
59
|
+
const functionName = node.id?.type === 'Identifier' ? node.id.name : undefined;
|
|
60
|
+
if (functionName && functionName.startsWith('handle') && node.id) {
|
|
52
61
|
context.report({
|
|
53
62
|
node,
|
|
54
63
|
messageId: 'callbackFunctionPrefix',
|
|
@@ -61,18 +70,18 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
},
|
|
64
|
-
// Check object
|
|
65
|
-
Property(node) {
|
|
66
|
-
if (node.
|
|
73
|
+
// Check class methods and object methods
|
|
74
|
+
'MethodDefinition, Property'(node) {
|
|
75
|
+
if (node.key.type === 'Identifier' &&
|
|
67
76
|
node.key.name &&
|
|
68
77
|
node.key.name.startsWith('handle')) {
|
|
78
|
+
const name = node.key.name;
|
|
69
79
|
context.report({
|
|
70
80
|
node: node.key,
|
|
71
81
|
messageId: 'callbackFunctionPrefix',
|
|
72
82
|
fix(fixer) {
|
|
73
83
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
74
|
-
const newName =
|
|
75
|
-
node.key.name.slice(7);
|
|
84
|
+
const newName = name.slice(6).charAt(0).toLowerCase() + name.slice(7);
|
|
76
85
|
return fixer.replaceText(node.key, newName);
|
|
77
86
|
},
|
|
78
87
|
});
|
|
@@ -64,23 +64,64 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
|
|
|
64
64
|
if (node.id.name === folderName) {
|
|
65
65
|
hasExpectedType = true;
|
|
66
66
|
// Check if type extends Identifiable
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const checkIdentifiableExtension = (type) => {
|
|
68
|
+
// Check for direct Identifiable extension
|
|
69
69
|
if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
70
70
|
type.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
71
71
|
type.typeName.name === 'Identifiable') {
|
|
72
72
|
return true;
|
|
73
73
|
}
|
|
74
|
+
// Check intersection types
|
|
74
75
|
if (type.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
75
|
-
return type.types.some(
|
|
76
|
+
return type.types.some(checkIdentifiableExtension);
|
|
76
77
|
}
|
|
78
|
+
// Check generic type parameters
|
|
77
79
|
if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
78
80
|
type.typeParameters?.params) {
|
|
79
|
-
return type.typeParameters.params.some(
|
|
81
|
+
return type.typeParameters.params.some(checkIdentifiableExtension);
|
|
80
82
|
}
|
|
81
83
|
return false;
|
|
82
84
|
};
|
|
83
|
-
|
|
85
|
+
// Check if type has id: string field
|
|
86
|
+
const checkIdField = (type) => {
|
|
87
|
+
// Check for id: string field in type literal
|
|
88
|
+
if (type.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
89
|
+
return type.members.some((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
90
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
91
|
+
member.key.name === 'id' &&
|
|
92
|
+
member.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSStringKeyword);
|
|
93
|
+
}
|
|
94
|
+
// Check intersection types
|
|
95
|
+
if (type.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
96
|
+
return type.types.some(checkIdField);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
// Check if type is wrapped in a utility type
|
|
101
|
+
const isUtilityType = (type) => {
|
|
102
|
+
return (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
103
|
+
type.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
104
|
+
type.typeName.name === 'Resolve');
|
|
105
|
+
};
|
|
106
|
+
// Recursively check the type and its parameters
|
|
107
|
+
const checkType = (type) => {
|
|
108
|
+
// Check if type extends Identifiable
|
|
109
|
+
if (checkIdentifiableExtension(type)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// Check if type has id: string field (only for utility types)
|
|
113
|
+
if (isUtilityType(type) && checkIdField(type.typeParameters.params[0])) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Check if type is wrapped in a utility type
|
|
117
|
+
if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
118
|
+
type.typeParameters?.params?.[0]) {
|
|
119
|
+
return checkType(type.typeParameters.params[0]);
|
|
120
|
+
}
|
|
121
|
+
// For direct type definitions, require extending Identifiable
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
typeHasIdentifiable = checkType(node.typeAnnotation);
|
|
84
125
|
}
|
|
85
126
|
},
|
|
86
127
|
};
|
|
@@ -3,6 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.extractGlobalConstants = void 0;
|
|
4
4
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
function isInsideFunction(node) {
|
|
7
|
+
let current = node;
|
|
8
|
+
while (current) {
|
|
9
|
+
if (current.type === 'FunctionDeclaration' ||
|
|
10
|
+
current.type === 'FunctionExpression' ||
|
|
11
|
+
current.type === 'ArrowFunctionExpression') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function isFunctionDefinition(node) {
|
|
19
|
+
return (node?.type === 'FunctionExpression' ||
|
|
20
|
+
node?.type === 'ArrowFunctionExpression');
|
|
21
|
+
}
|
|
6
22
|
exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
7
23
|
create(context) {
|
|
8
24
|
return {
|
|
@@ -10,13 +26,19 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
10
26
|
if (node.kind !== 'const') {
|
|
11
27
|
return;
|
|
12
28
|
}
|
|
29
|
+
// Skip if any of the declarations are function definitions
|
|
30
|
+
if (node.declarations.some((d) => isFunctionDefinition(d.init))) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
13
33
|
const scope = context.getScope();
|
|
14
34
|
const hasDependencies = node.declarations.some((declaration) => declaration.init &&
|
|
15
35
|
ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(declaration.init));
|
|
36
|
+
// Only check function/block scoped constants without dependencies
|
|
16
37
|
if (!hasDependencies &&
|
|
17
|
-
(scope.type === 'function' || scope.type === 'block')
|
|
18
|
-
|
|
19
|
-
const constName = node.declarations[0].id
|
|
38
|
+
(scope.type === 'function' || scope.type === 'block') &&
|
|
39
|
+
isInsideFunction(node)) {
|
|
40
|
+
const constName = node.declarations[0].id
|
|
41
|
+
.name;
|
|
20
42
|
context.report({
|
|
21
43
|
node,
|
|
22
44
|
messageId: 'extractGlobalConstants',
|
|
@@ -34,8 +56,7 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
34
56
|
const scope = context.getScope();
|
|
35
57
|
const hasDependencies = ASTHelpers_1.ASTHelpers.blockIncludesIdentifier(node.body);
|
|
36
58
|
if (!hasDependencies && scope.type === 'function') {
|
|
37
|
-
|
|
38
|
-
const funcName = node.id.name;
|
|
59
|
+
const funcName = node.id?.name;
|
|
39
60
|
context.report({
|
|
40
61
|
node,
|
|
41
62
|
messageId: 'extractGlobalConstants',
|
|
@@ -52,7 +73,7 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
52
73
|
meta: {
|
|
53
74
|
type: 'suggestion',
|
|
54
75
|
docs: {
|
|
55
|
-
description: 'Extract constants
|
|
76
|
+
description: 'Extract static constants and functions to the global scope when possible',
|
|
56
77
|
recommended: 'error',
|
|
57
78
|
},
|
|
58
79
|
schema: [],
|
|
@@ -23,8 +23,27 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
23
23
|
return {
|
|
24
24
|
VariableDeclaration(node) {
|
|
25
25
|
// Only check top-level const declarations
|
|
26
|
-
if (node.kind !== 'const'
|
|
27
|
-
|
|
26
|
+
if (node.kind !== 'const') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Skip if not at program level
|
|
30
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.Program) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Skip if any declaration is a function component or arrow function
|
|
34
|
+
const shouldSkip = node.declarations.some(declaration => {
|
|
35
|
+
if (declaration.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const name = declaration.id.name;
|
|
39
|
+
const init = declaration.init;
|
|
40
|
+
return (
|
|
41
|
+
// Skip function components (uppercase name + arrow function)
|
|
42
|
+
(/^[A-Z]/.test(name) && init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) ||
|
|
43
|
+
// Skip any arrow function
|
|
44
|
+
init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
45
|
+
});
|
|
46
|
+
if (shouldSkip) {
|
|
28
47
|
return;
|
|
29
48
|
}
|
|
30
49
|
node.declarations.forEach((declaration) => {
|
|
@@ -55,10 +74,24 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
55
74
|
});
|
|
56
75
|
}
|
|
57
76
|
// Check for as const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
const isAsConstExpression = (node) => {
|
|
78
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
79
|
+
return (node.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
80
|
+
node.typeAnnotation?.typeName?.name === 'const');
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
const shouldHaveAsConst = (node) => {
|
|
85
|
+
// Skip if it's already an as const expression
|
|
86
|
+
if (isAsConstExpression(node)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// Check if it's a literal, array, or object that should have as const
|
|
90
|
+
return (node.type === utils_1.AST_NODE_TYPES.Literal ||
|
|
91
|
+
node.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
92
|
+
node.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
93
|
+
};
|
|
94
|
+
if (shouldHaveAsConst(init)) {
|
|
62
95
|
context.report({
|
|
63
96
|
node: init,
|
|
64
97
|
messageId: 'asConst',
|
|
@@ -67,7 +67,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
67
67
|
ImportDeclaration(node) {
|
|
68
68
|
const importSource = node.source.value;
|
|
69
69
|
if (typeof importSource === 'string' &&
|
|
70
|
-
isFirebaseImport(importSource)
|
|
70
|
+
isFirebaseImport(importSource) &&
|
|
71
|
+
!node.importKind?.includes('type')) {
|
|
71
72
|
context.report({
|
|
72
73
|
node,
|
|
73
74
|
messageId: 'requireDynamicImport',
|
|
@@ -23,6 +23,11 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
23
23
|
if (!node.value || node.value.type !== 'JSXExpressionContainer') {
|
|
24
24
|
return;
|
|
25
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
|
+
}
|
|
26
31
|
const { expression } = node.value;
|
|
27
32
|
// Check if the expression is an object or array literal
|
|
28
33
|
if ((expression.type === 'ObjectExpression' ||
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -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.
|