@blumintinc/eslint-plugin-blumint 1.7.1 → 1.7.3

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
@@ -69,10 +69,11 @@ const enforce_centralized_mock_firestore_1 = require("./rules/enforce-centralize
69
69
  const require_hooks_default_params_1 = require("./rules/require-hooks-default-params");
70
70
  const prefer_destructuring_no_class_1 = require("./rules/prefer-destructuring-no-class");
71
71
  const enforce_render_hits_memoization_1 = require("./rules/enforce-render-hits-memoization");
72
+ const prefer_fragment_component_1 = require("./rules/prefer-fragment-component");
72
73
  module.exports = {
73
74
  meta: {
74
75
  name: '@blumintinc/eslint-plugin-blumint',
75
- version: '1.7.1',
76
+ version: '1.7.3',
76
77
  },
77
78
  parseOptions: {
78
79
  ecmaVersion: 2020,
@@ -104,7 +105,7 @@ module.exports = {
104
105
  '@blumintinc/blumint/no-unpinned-dependencies': 'error',
105
106
  '@blumintinc/blumint/no-unused-props': 'error',
106
107
  //'@blumintinc/blumint/no-useless-fragment': 'error',
107
- '@blumintinc/blumint/prefer-fragment-shorthand': 'error',
108
+ //'@blumintinc/blumint/prefer-fragment-shorthand': 'error',
108
109
  '@blumintinc/blumint/prefer-type-over-interface': 'error',
109
110
  '@blumintinc/blumint/require-memo': 'error',
110
111
  '@blumintinc/blumint/require-dynamic-firebase-imports': 'error',
@@ -147,6 +148,7 @@ module.exports = {
147
148
  '@blumintinc/blumint/require-hooks-default-params': 'error',
148
149
  '@blumintinc/blumint/prefer-destructuring-no-class': 'error',
149
150
  '@blumintinc/blumint/enforce-render-hits-memoization': 'error',
151
+ '@blumintinc/blumint/prefer-fragment-component': 'error',
150
152
  },
151
153
  },
152
154
  },
@@ -217,6 +219,7 @@ module.exports = {
217
219
  'require-hooks-default-params': require_hooks_default_params_1.requireHooksDefaultParams,
218
220
  'prefer-destructuring-no-class': prefer_destructuring_no_class_1.preferDestructuringNoClass,
219
221
  'enforce-render-hits-memoization': enforce_render_hits_memoization_1.enforceRenderHitsMemoization,
222
+ 'prefer-fragment-component': prefer_fragment_component_1.preferFragmentComponent,
220
223
  },
221
224
  };
222
225
  //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceCentralizedMockFirestore = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
- const MOCK_FIRESTORE_PATH = '../../../../../__mocks__/functions/src/config/mockFirestore';
6
+ const MOCK_FIRESTORE_PATH = '../../../../../__test-utils__/mockFirestore';
7
7
  exports.enforceCentralizedMockFirestore = (0, createRule_1.createRule)({
8
8
  name: 'enforce-centralized-mock-firestore',
9
9
  meta: {
@@ -18,8 +18,8 @@ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
18
18
  },
19
19
  schema: [],
20
20
  messages: {
21
- noManualFirestoreMock: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of manually mocking Firestore. Replace `jest.mock("firebase-admin", () => ({ firestore: () => ({ /* mock */ }) }))` with `import { mockFirestore } from "__mocks__/functions/src/config/mockFirestore"; jest.mock("firebase-admin", () => mockFirestore)`.',
22
- noMockFirebase: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of mockFirebase. Replace `import { mockFirebase } from "firestore-jest-mock"` with `import { mockFirestore } from "__mocks__/functions/src/config/mockFirestore"`.',
21
+ noManualFirestoreMock: 'Use mockFirestore from __test-utils__/mockFirestore instead of manually mocking Firestore. Replace `jest.mock("firebase-admin", () => ({ firestore: () => ({ /* mock */ }) }))` with `import { mockFirestore } from "__test-utils__/mockFirestore"; jest.mock("firebase-admin", () => mockFirestore)`.',
22
+ noMockFirebase: 'Use mockFirestore from __test-utils__/mockFirestore instead of mockFirebase. Replace `import { mockFirebase } from "firestore-jest-mock"` with `import { mockFirestore } from "__test-utils__/mockFirestore"`.',
23
23
  },
24
24
  },
25
25
  defaultOptions: [],
@@ -35,7 +35,27 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
35
35
  const name = node.name;
36
36
  return /^[A-Z]/.test(name);
37
37
  };
38
+ const isMemoizedVariable = (node) => {
39
+ if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
40
+ return false;
41
+ // Get the variable declaration for this identifier
42
+ const variable = context.getScope().variables.find(v => v.name === node.name);
43
+ if (!variable)
44
+ return false;
45
+ // Check if the variable is initialized with a memoized call
46
+ const def = variable.defs[0];
47
+ if (!def || !def.node)
48
+ return false;
49
+ if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && def.node.init) {
50
+ return isMemoizedCall(def.node.init);
51
+ }
52
+ return false;
53
+ };
38
54
  const isInsideMemoizedCall = (node) => {
55
+ // Check if the node is a reference to a memoized variable
56
+ if (isMemoizedVariable(node))
57
+ return true;
58
+ // Check if the node is inside a memoization hook call
39
59
  let current = node;
40
60
  while (current?.parent) {
41
61
  if (isMemoizedCall(current.parent))
@@ -10,6 +10,7 @@ const compromise_1 = __importDefault(require("compromise"));
10
10
  const PREPOSITIONS = new Set(['to', 'from', 'with', 'by', 'at', 'of']);
11
11
  // Create a Set from the verbs list for O(1) lookup
12
12
  const VERBS_SET = new Set([
13
+ 'clean',
13
14
  'abbreviate',
14
15
  'abort',
15
16
  'abstract',
@@ -32,7 +32,7 @@ exports.noFirestoreJestMock = (0, createRule_1.createRule)({
32
32
  messageId: 'noFirestoreJestMock',
33
33
  fix(fixer) {
34
34
  // Replace with mockFirestore import
35
- return fixer.replaceText(node, "import { mockFirestore } from '../../../../../__mocks__/functions/src/config/mockFirestore';");
35
+ return fixer.replaceText(node, "import { mockFirestore } from '../../../../../__test-utils__/mockFirestore';");
36
36
  },
37
37
  });
38
38
  }
@@ -19,7 +19,7 @@ exports.noMockFirebaseAdmin = (0, createRule_1.createRule)({
19
19
  },
20
20
  schema: [],
21
21
  messages: {
22
- noMockFirebaseAdmin: 'Do not mock firebaseAdmin directly. Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead.',
22
+ noMockFirebaseAdmin: 'Do not mock firebaseAdmin directly. Use mockFirestore from __test-utils__/mockFirestore instead.',
23
23
  },
24
24
  },
25
25
  defaultOptions: [],
@@ -36,7 +36,53 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
36
36
  });
37
37
  }
38
38
  else if (typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
39
- typeNode.types.forEach(extractProps);
39
+ typeNode.types.forEach((type) => {
40
+ if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
41
+ const typeName = type.typeName;
42
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
43
+ if (typeName.name === 'Pick' && type.typeParameters) {
44
+ // Handle Pick utility type in intersection
45
+ const [baseType, pickedProps] = type.typeParameters.params;
46
+ if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
47
+ baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
48
+ // Extract the picked properties from the union type
49
+ if (pickedProps.type === utils_1.AST_NODE_TYPES.TSUnionType) {
50
+ pickedProps.types.forEach((t) => {
51
+ if (t.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
52
+ t.literal.type === utils_1.AST_NODE_TYPES.Literal &&
53
+ typeof t.literal.value === 'string') {
54
+ // Add each picked property as a regular prop
55
+ props[t.literal.value] = t.literal;
56
+ }
57
+ });
58
+ }
59
+ else if (pickedProps.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
60
+ pickedProps.literal.type === utils_1.AST_NODE_TYPES.Literal &&
61
+ typeof pickedProps.literal.value === 'string') {
62
+ // Single property pick
63
+ props[pickedProps.literal.value] = pickedProps.literal;
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ // For referenced types in intersections, we need to find their type declaration
69
+ const scope = context.getScope();
70
+ const variable = scope.variables.find(v => v.name === typeName.name);
71
+ if (variable && variable.defs[0]?.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
72
+ extractProps(variable.defs[0].node.typeAnnotation);
73
+ }
74
+ else {
75
+ // If we can't find the type declaration, it's likely an imported type
76
+ // Mark it as a forwarded prop
77
+ props[`...${typeName.name}`] = typeName;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ else {
83
+ extractProps(type);
84
+ }
85
+ });
40
86
  }
41
87
  else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
42
88
  if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
@@ -0,0 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferFragment' | 'addFragmentImport';
3
+ export declare const preferFragmentComponent: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
+ export {};
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferFragmentComponent = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.preferFragmentComponent = (0, createRule_1.createRule)({
7
+ name: 'prefer-fragment-component',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce using Fragment imported from react over shorthand fragments and React.Fragment',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ preferFragment: 'Use Fragment imported from react instead of {{type}}',
18
+ addFragmentImport: 'Add Fragment import from react',
19
+ },
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ const sourceCode = context.getSourceCode();
24
+ let hasFragmentImport = false;
25
+ let reactImportNode = null;
26
+ function getReactImportNode() {
27
+ const program = sourceCode.ast;
28
+ for (const node of program.body) {
29
+ if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
30
+ node.source.value === 'react') {
31
+ return node;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ function checkFragmentImport(node) {
37
+ if (node.source.value === 'react') {
38
+ reactImportNode = node;
39
+ for (const specifier of node.specifiers) {
40
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
41
+ specifier.imported.name === 'Fragment') {
42
+ hasFragmentImport = true;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ function addFragmentImport(fixer) {
49
+ if (reactImportNode) {
50
+ // Add Fragment to existing react import
51
+ const lastSpecifier = reactImportNode.specifiers[reactImportNode.specifiers.length - 1];
52
+ return fixer.insertTextAfter(lastSpecifier, ', Fragment');
53
+ }
54
+ // Add new react import with Fragment
55
+ return fixer.insertTextBefore(sourceCode.ast.body[0], 'import { Fragment } from \'react\';\n');
56
+ }
57
+ return {
58
+ ImportDeclaration: checkFragmentImport,
59
+ JSXFragment(node) {
60
+ context.report({
61
+ node,
62
+ messageId: 'preferFragment',
63
+ data: { type: 'shorthand fragment (<>)' },
64
+ *fix(fixer) {
65
+ if (!hasFragmentImport) {
66
+ yield addFragmentImport(fixer);
67
+ }
68
+ yield fixer.replaceText(node.openingFragment, '<Fragment>');
69
+ yield fixer.replaceText(node.closingFragment, '</Fragment>');
70
+ },
71
+ });
72
+ },
73
+ JSXIdentifier(node) {
74
+ if (node.name === 'Fragment' &&
75
+ node.parent?.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
76
+ node.parent.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
77
+ node.parent.object.name === 'React') {
78
+ const memberExpr = node.parent;
79
+ context.report({
80
+ node: memberExpr,
81
+ messageId: 'preferFragment',
82
+ data: { type: 'React.Fragment' },
83
+ *fix(fixer) {
84
+ if (!hasFragmentImport) {
85
+ yield addFragmentImport(fixer);
86
+ }
87
+ yield fixer.replaceText(memberExpr, 'Fragment');
88
+ },
89
+ });
90
+ }
91
+ },
92
+ 'Program:exit'() {
93
+ // If we found any violations but no Fragment import, we need to add it
94
+ if (!hasFragmentImport && !reactImportNode) {
95
+ reactImportNode = getReactImportNode();
96
+ }
97
+ },
98
+ };
99
+ },
100
+ });
101
+ //# sourceMappingURL=prefer-fragment-component.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",