@blumintinc/eslint-plugin-blumint 1.1.0 → 1.1.2

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
@@ -32,10 +32,11 @@ const require_https_error_1 = __importDefault(require("./rules/require-https-err
32
32
  const use_custom_router_1 = require("./rules/use-custom-router");
33
33
  const require_image_overlayed_1 = __importDefault(require("./rules/require-image-overlayed"));
34
34
  const require_usememo_object_literals_1 = require("./rules/require-usememo-object-literals");
35
+ const enforce_safe_stringify_1 = require("./rules/enforce-safe-stringify");
35
36
  module.exports = {
36
37
  meta: {
37
38
  name: '@blumintinc/eslint-plugin-blumint',
38
- version: '1.1.0',
39
+ version: '1.1.2',
39
40
  },
40
41
  parseOptions: {
41
42
  ecmaVersion: 2020,
@@ -73,6 +74,7 @@ module.exports = {
73
74
  '@blumintinc/blumint/use-custom-router': 'error',
74
75
  '@blumintinc/blumint/require-image-overlayed': 'error',
75
76
  '@blumintinc/blumint/require-usememo-object-literals': 'error',
77
+ '@blumintinc/blumint/enforce-safe-stringify': 'error',
76
78
  },
77
79
  },
78
80
  },
@@ -106,6 +108,7 @@ module.exports = {
106
108
  'use-custom-router': use_custom_router_1.useCustomRouter,
107
109
  'require-image-overlayed': require_image_overlayed_1.default,
108
110
  'require-usememo-object-literals': require_usememo_object_literals_1.requireUseMemoObjectLiterals,
111
+ 'enforce-safe-stringify': enforce_safe_stringify_1.enforceStableStringify,
109
112
  },
110
113
  };
111
114
  //# sourceMappingURL=index.js.map
@@ -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.match(/^on[A-Z]/)) {
38
+ if (propName?.match(/^on[A-Z]/)) {
30
39
  return;
31
40
  }
32
- // Check if it's a function prop but doesn't follow the 'on' prefix convention
33
- if (valueName &&
34
- typeof valueName === 'string' &&
35
- (valueName.startsWith('handle') || valueName.match(/^[a-z]+[A-Z]/))) {
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 property methods
65
- Property(node) {
66
- if (node.method &&
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 = node.key.name.slice(6).charAt(0).toLowerCase() +
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
  });
@@ -22,8 +22,10 @@ exports.enforceCallableTypes = (0, createRule_1.createRule)({
22
22
  defaultOptions: [],
23
23
  create(context) {
24
24
  const filename = context.getFilename();
25
- // Only apply to .f.ts files in the callable directory
26
- if (!filename.endsWith('.f.ts') || !filename.includes('/callable/')) {
25
+ // Only apply to .f.ts files in the callable directory, but ignore scripts directory
26
+ if (!filename.endsWith('.f.ts') ||
27
+ !filename.includes('/callable/') ||
28
+ filename.includes('/callable/scripts/')) {
27
29
  return {};
28
30
  }
29
31
  let hasParamsExport = false;
@@ -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
- // Check if type extends Identifiable directly or through a generic type
68
- const checkIdentifiable = (type) => {
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(checkIdentifiable);
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(checkIdentifiable);
81
+ return type.typeParameters.params.some(checkIdentifiableExtension);
80
82
  }
81
83
  return false;
82
84
  };
83
- typeHasIdentifiable = checkIdentifiable(node.typeAnnotation);
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
  };
@@ -0,0 +1,2 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const enforceStableStringify: TSESLint.RuleModule<"useStableStringify", [], TSESLint.RuleListener>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceStableStringify = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.enforceStableStringify = (0, createRule_1.createRule)({
7
+ name: 'enforce-safe-stringify',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Enforce using safe-stable-stringify instead of JSON.stringify',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ useStableStringify: 'Use safe-stable-stringify instead of JSON.stringify for safer serialization',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ let hasStringifyImport = false;
23
+ return {
24
+ ImportDeclaration(node) {
25
+ if (node.source.value === 'safe-stable-stringify' &&
26
+ node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
27
+ specifier.local.name === 'stringify')) {
28
+ hasStringifyImport = true;
29
+ }
30
+ },
31
+ MemberExpression(node) {
32
+ if (node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
33
+ node.object.name === 'JSON' &&
34
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
35
+ node.property.name === 'stringify') {
36
+ context.report({
37
+ node,
38
+ messageId: 'useStableStringify',
39
+ fix(fixer) {
40
+ const fixes = [];
41
+ // Add import if not present
42
+ if (!hasStringifyImport) {
43
+ const program = context.getSourceCode().ast;
44
+ const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
45
+ const importStatement = "import stringify from 'safe-stable-stringify';\n";
46
+ if (firstImport) {
47
+ fixes.push(fixer.insertTextBefore(firstImport, importStatement));
48
+ }
49
+ else {
50
+ fixes.push(fixer.insertTextBefore(program.body[0], importStatement));
51
+ }
52
+ hasStringifyImport = true;
53
+ }
54
+ // Replace JSON.stringify with stringify
55
+ fixes.push(fixer.replaceText(node, 'stringify'));
56
+ return fixes;
57
+ },
58
+ });
59
+ }
60
+ },
61
+ };
62
+ },
63
+ });
64
+ //# sourceMappingURL=enforce-safe-stringify.js.map
@@ -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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- const constName = node.declarations[0].id.name;
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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/functions to the global scope when possible',
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
- node.parent?.type !== utils_1.AST_NODE_TYPES.Program) {
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
- if (init.type !== utils_1.AST_NODE_TYPES.TSAsExpression ||
59
- init.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
60
- init.typeAnnotation.typeName.name !==
61
- 'const') {
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' ||
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",