@blumintinc/eslint-plugin-blumint 1.2.1 β†’ 1.3.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/README.md CHANGED
@@ -54,42 +54,45 @@ Or use the recommended config:
54
54
  πŸ’Ό Configurations enabled in.\
55
55
  ⚠️ Configurations set to warn in.\
56
56
  βœ… Set in the `recommended` configuration.\
57
- πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
58
-
59
- | NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ |
60
- | :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
61
- | [array-methods-this-context](docs/rules/array-methods-this-context.md) | Prevent misuse of Array methods in OOP | | βœ… | |
62
- | [avoid-utils-directory](docs/rules/avoid-utils-directory.md) | Enforce using util/ instead of utils/ directory | βœ… | | πŸ”§ |
63
- | [class-methods-read-top-to-bottom](docs/rules/class-methods-read-top-to-bottom.md) | Ensures classes read linearly from top to bottom. | | βœ… | πŸ”§ |
64
- | [consistent-callback-naming](docs/rules/consistent-callback-naming.md) | Enforce consistent naming conventions for callback props and functions | βœ… | | πŸ”§ |
65
- | [dynamic-https-errors](docs/rules/dynamic-https-errors.md) | Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id. | | βœ… | |
66
- | [enforce-callable-types](docs/rules/enforce-callable-types.md) | Enforce Params and Response type exports in callable functions | βœ… | | |
67
- | [enforce-callback-memo](docs/rules/enforce-callback-memo.md) | Enforce useCallback or useMemo for inline functions in JSX props | βœ… | | |
68
- | [enforce-dynamic-firebase-imports](docs/rules/enforce-dynamic-firebase-imports.md) | Enforce dynamic importing for modules within the firebaseCloud directory | βœ… | | |
69
- | [enforce-firestore-path-utils](docs/rules/enforce-firestore-path-utils.md) | Enforce usage of utility functions for Firestore paths | βœ… | | |
70
- | [enforce-identifiable-firestore-type](docs/rules/enforce-identifiable-firestore-type.md) | Enforce that Firestore type definitions extend Identifiable and match their folder name | βœ… | | |
71
- | [enforce-safe-stringify](docs/rules/enforce-safe-stringify.md) | Enforce using safe-stable-stringify instead of JSON.stringify | βœ… | | πŸ”§ |
72
- | [export-if-in-doubt](docs/rules/export-if-in-doubt.md) | All top-level const definitions, type definitions, and functions should be exported | | | |
73
- | [extract-global-constants](docs/rules/extract-global-constants.md) | Extract static constants and functions to the global scope when possible | | βœ… | |
74
- | [generic-starts-with-t](docs/rules/generic-starts-with-t.md) | Enforce TypeScript generic types to start with T | | βœ… | |
75
- | [global-const-style](docs/rules/global-const-style.md) | Enforce UPPER_SNAKE_CASE and as const for global static constants | βœ… | | πŸ”§ |
76
- | [no-async-array-filter](docs/rules/no-async-array-filter.md) | Disallow async callbacks for Array.filter | βœ… | | |
77
- | [no-async-foreach](docs/rules/no-async-foreach.md) | Disallow Array.forEach with an async callback function | βœ… | | |
78
- | [no-conditional-literals-in-jsx](docs/rules/no-conditional-literals-in-jsx.md) | Disallow use of conditional literals in JSX code | βœ… | | |
79
- | [no-entire-object-hook-deps](docs/rules/no-entire-object-hook-deps.md) | Avoid using entire objects in React hook dependency arrays when only specific fields are used | βœ… | | πŸ”§ |
80
- | [no-filter-without-return](docs/rules/no-filter-without-return.md) | Disallow Array.filter callbacks without an explicit return (if part of a block statement) | βœ… | | |
81
- | [no-jsx-whitespace-literal](docs/rules/no-jsx-whitespace-literal.md) | Disallow the use of {" "} elements in JSX code | βœ… | | |
82
- | [no-misused-switch-case](docs/rules/no-misused-switch-case.md) | Prevent misuse of logical OR in switch case statements | βœ… | | |
83
- | [no-unpinned-dependencies](docs/rules/no-unpinned-dependencies.md) | Enforces pinned dependencies | βœ… | | πŸ”§ |
84
- | [no-unused-props](docs/rules/no-unused-props.md) | Detect unused props in React component type definitions | βœ… | | πŸ”§ |
85
- | [no-useless-fragment](docs/rules/no-useless-fragment.md) | Prevent unnecessary use of React fragments | | βœ… | πŸ”§ |
86
- | [prefer-fragment-shorthand](docs/rules/prefer-fragment-shorthand.md) | Prefer <> shorthand for <React.Fragment> | | βœ… | πŸ”§ |
87
- | [prefer-type-over-interface](docs/rules/prefer-type-over-interface.md) | Prefer using type alias over interface | | βœ… | πŸ”§ |
88
- | [require-dynamic-firebase-imports](docs/rules/require-dynamic-firebase-imports.md) | Enforce dynamic imports for Firebase dependencies | βœ… | | πŸ”§ |
89
- | [require-https-error](docs/rules/require-https-error.md) | Enforce using proprietary HttpsError instead of throw new Error or firebase-admin HttpsError in functions/src | βœ… | | |
90
- | [require-image-overlayed](docs/rules/require-image-overlayed.md) | Enforce using ImageOverlayed component instead of next/image or img tags | βœ… | | πŸ”§ |
91
- | [require-memo](docs/rules/require-memo.md) | React components must be memoized | βœ… | | πŸ”§ |
92
- | [require-usememo-object-literals](docs/rules/require-usememo-object-literals.md) | Enforce using useMemo for inline object literals passed as props to JSX components | βœ… | | |
93
- | [use-custom-router](docs/rules/use-custom-router.md) | Enforce using src/hooks/routing/useRouter instead of next/router | βœ… | | πŸ”§ |
57
+ πŸ”§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
58
+ πŸ’­ Requires type information.
59
+
60
+ | NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | πŸ’­ |
61
+ | :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | :- |
62
+ | [array-methods-this-context](docs/rules/array-methods-this-context.md) | Prevent misuse of Array methods in OOP | | βœ… | | |
63
+ | [avoid-utils-directory](docs/rules/avoid-utils-directory.md) | Enforce using util/ instead of utils/ directory | βœ… | | πŸ”§ | |
64
+ | [class-methods-read-top-to-bottom](docs/rules/class-methods-read-top-to-bottom.md) | Ensures classes read linearly from top to bottom. | | βœ… | πŸ”§ | |
65
+ | [consistent-callback-naming](docs/rules/consistent-callback-naming.md) | Enforce consistent naming conventions for callback props and functions | βœ… | | πŸ”§ | |
66
+ | [dynamic-https-errors](docs/rules/dynamic-https-errors.md) | Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id. | | βœ… | | |
67
+ | [enforce-callable-types](docs/rules/enforce-callable-types.md) | Enforce Params and Response type exports in callable functions | βœ… | | | |
68
+ | [enforce-callback-memo](docs/rules/enforce-callback-memo.md) | Enforce useCallback or useMemo for inline functions in JSX props | βœ… | | | |
69
+ | [enforce-dynamic-firebase-imports](docs/rules/enforce-dynamic-firebase-imports.md) | Enforce dynamic importing for modules within the firebaseCloud directory | βœ… | | | |
70
+ | [enforce-firestore-doc-ref-generic](docs/rules/enforce-firestore-doc-ref-generic.md) | Enforce generic argument for Firestore DocumentReference | βœ… | | | πŸ’­ |
71
+ | [enforce-firestore-path-utils](docs/rules/enforce-firestore-path-utils.md) | Enforce usage of utility functions for Firestore paths | βœ… | | | |
72
+ | [enforce-identifiable-firestore-type](docs/rules/enforce-identifiable-firestore-type.md) | Enforce that Firestore type definitions extend Identifiable and match their folder name | βœ… | | | |
73
+ | [enforce-safe-stringify](docs/rules/enforce-safe-stringify.md) | Enforce using safe-stable-stringify instead of JSON.stringify | βœ… | | πŸ”§ | |
74
+ | [export-if-in-doubt](docs/rules/export-if-in-doubt.md) | All top-level const definitions, type definitions, and functions should be exported | | | | |
75
+ | [extract-global-constants](docs/rules/extract-global-constants.md) | Extract static constants and functions to the global scope when possible | | βœ… | | |
76
+ | [generic-starts-with-t](docs/rules/generic-starts-with-t.md) | Enforce TypeScript generic types to start with T | | βœ… | | |
77
+ | [global-const-style](docs/rules/global-const-style.md) | Enforce UPPER_SNAKE_CASE and as const for global static constants | βœ… | | πŸ”§ | |
78
+ | [no-async-array-filter](docs/rules/no-async-array-filter.md) | Disallow async callbacks for Array.filter | βœ… | | | |
79
+ | [no-async-foreach](docs/rules/no-async-foreach.md) | Disallow Array.forEach with an async callback function | βœ… | | | |
80
+ | [no-compositing-layer-props](docs/rules/no-compositing-layer-props.md) | Warn when using CSS properties that trigger compositing layers | | βœ… | | |
81
+ | [no-conditional-literals-in-jsx](docs/rules/no-conditional-literals-in-jsx.md) | Disallow use of conditional literals in JSX code | βœ… | | | |
82
+ | [no-entire-object-hook-deps](docs/rules/no-entire-object-hook-deps.md) | Avoid using entire objects in React hook dependency arrays when only specific fields are used | βœ… | | πŸ”§ | |
83
+ | [no-filter-without-return](docs/rules/no-filter-without-return.md) | Disallow Array.filter callbacks without an explicit return (if part of a block statement) | βœ… | | | |
84
+ | [no-jsx-whitespace-literal](docs/rules/no-jsx-whitespace-literal.md) | Disallow the use of {" "} elements in JSX code | βœ… | | | |
85
+ | [no-misused-switch-case](docs/rules/no-misused-switch-case.md) | Prevent misuse of logical OR in switch case statements | βœ… | | | |
86
+ | [no-unpinned-dependencies](docs/rules/no-unpinned-dependencies.md) | Enforces pinned dependencies | βœ… | | πŸ”§ | |
87
+ | [no-unused-props](docs/rules/no-unused-props.md) | Detect unused props in React component type definitions | βœ… | | πŸ”§ | |
88
+ | [no-useless-fragment](docs/rules/no-useless-fragment.md) | Prevent unnecessary use of React fragments | | βœ… | πŸ”§ | |
89
+ | [prefer-fragment-shorthand](docs/rules/prefer-fragment-shorthand.md) | Prefer <> shorthand for <React.Fragment> | | βœ… | πŸ”§ | |
90
+ | [prefer-type-over-interface](docs/rules/prefer-type-over-interface.md) | Prefer using type alias over interface | | βœ… | πŸ”§ | |
91
+ | [require-dynamic-firebase-imports](docs/rules/require-dynamic-firebase-imports.md) | Enforce dynamic imports for Firebase dependencies | βœ… | | πŸ”§ | |
92
+ | [require-https-error](docs/rules/require-https-error.md) | Enforce using proprietary HttpsError instead of throw new Error or firebase-admin HttpsError in functions/src | βœ… | | | |
93
+ | [require-image-overlayed](docs/rules/require-image-overlayed.md) | Enforce using ImageOverlayed component instead of next/image or img tags | βœ… | | πŸ”§ | |
94
+ | [require-memo](docs/rules/require-memo.md) | React components must be memoized | βœ… | | πŸ”§ | |
95
+ | [require-usememo-object-literals](docs/rules/require-usememo-object-literals.md) | Enforce using useMemo for inline object literals passed as props to JSX components | βœ… | | | |
96
+ | [use-custom-router](docs/rules/use-custom-router.md) | Enforce using src/hooks/routing/useRouter instead of next/router | βœ… | | πŸ”§ | |
94
97
 
95
98
  <!-- end auto-generated rules list -->
package/lib/index.js CHANGED
@@ -30,7 +30,7 @@ const no_jsx_whitespace_literal_1 = require("./rules/no-jsx-whitespace-literal")
30
30
  const require_dynamic_firebase_imports_1 = __importDefault(require("./rules/require-dynamic-firebase-imports"));
31
31
  const require_https_error_1 = __importDefault(require("./rules/require-https-error"));
32
32
  const use_custom_router_1 = require("./rules/use-custom-router");
33
- const require_image_overlayed_1 = __importDefault(require("./rules/require-image-overlayed"));
33
+ const require_image_optimized_1 = __importDefault(require("./rules/require-image-optimized"));
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");
@@ -38,10 +38,12 @@ const no_entire_object_hook_deps_1 = require("./rules/no-entire-object-hook-deps
38
38
  const enforce_firestore_path_utils_1 = require("./rules/enforce-firestore-path-utils");
39
39
  const no_compositing_layer_props_1 = require("./rules/no-compositing-layer-props");
40
40
  const enforce_firestore_doc_ref_generic_1 = require("./rules/enforce-firestore-doc-ref-generic");
41
+ const semantic_function_prefixes_1 = require("./rules/semantic-function-prefixes");
42
+ const enforce_mock_firestore_1 = require("./rules/enforce-mock-firestore");
41
43
  module.exports = {
42
44
  meta: {
43
45
  name: '@blumintinc/eslint-plugin-blumint',
44
- version: '1.2.1',
46
+ version: '1.3.1',
45
47
  },
46
48
  parseOptions: {
47
49
  ecmaVersion: 2020,
@@ -53,17 +55,17 @@ module.exports = {
53
55
  '@blumintinc/blumint/avoid-utils-directory': 'error',
54
56
  '@blumintinc/blumint/enforce-firestore-path-utils': 'error',
55
57
  '@blumintinc/blumint/no-jsx-whitespace-literal': 'error',
56
- '@blumintinc/blumint/array-methods-this-context': 'warn',
57
- '@blumintinc/blumint/class-methods-read-top-to-bottom': 'warn',
58
+ '@blumintinc/blumint/array-methods-this-context': 'error',
59
+ '@blumintinc/blumint/class-methods-read-top-to-bottom': 'error',
58
60
  '@blumintinc/blumint/consistent-callback-naming': 'error',
59
- '@blumintinc/blumint/dynamic-https-errors': 'warn',
61
+ '@blumintinc/blumint/dynamic-https-errors': 'error',
60
62
  '@blumintinc/blumint/enforce-identifiable-firestore-type': 'error',
61
63
  '@blumintinc/blumint/enforce-callback-memo': 'error',
62
64
  '@blumintinc/blumint/enforce-callable-types': 'error',
63
65
  '@blumintinc/blumint/enforce-dynamic-firebase-imports': 'error',
64
66
  // '@blumintinc/blumint/export-if-in-doubt': 'warn',
65
- '@blumintinc/blumint/extract-global-constants': 'warn',
66
- '@blumintinc/blumint/generic-starts-with-t': 'warn',
67
+ '@blumintinc/blumint/extract-global-constants': 'error',
68
+ '@blumintinc/blumint/generic-starts-with-t': 'error',
67
69
  '@blumintinc/blumint/global-const-style': 'error',
68
70
  '@blumintinc/blumint/no-async-array-filter': 'error',
69
71
  '@blumintinc/blumint/no-async-foreach': 'error',
@@ -72,19 +74,21 @@ module.exports = {
72
74
  '@blumintinc/blumint/no-misused-switch-case': 'error',
73
75
  '@blumintinc/blumint/no-unpinned-dependencies': 'error',
74
76
  '@blumintinc/blumint/no-unused-props': 'error',
75
- '@blumintinc/blumint/no-useless-fragment': 'warn',
76
- '@blumintinc/blumint/prefer-fragment-shorthand': 'warn',
77
- '@blumintinc/blumint/prefer-type-over-interface': 'warn',
77
+ //'@blumintinc/blumint/no-useless-fragment': 'error',
78
+ '@blumintinc/blumint/prefer-fragment-shorthand': 'error',
79
+ '@blumintinc/blumint/prefer-type-over-interface': 'error',
78
80
  '@blumintinc/blumint/require-memo': 'error',
79
81
  '@blumintinc/blumint/require-dynamic-firebase-imports': 'error',
80
82
  '@blumintinc/blumint/require-https-error': 'error',
81
83
  '@blumintinc/blumint/use-custom-router': 'error',
82
- '@blumintinc/blumint/require-image-overlayed': 'error',
84
+ '@blumintinc/blumint/require-image-optimized': 'error',
83
85
  '@blumintinc/blumint/require-usememo-object-literals': 'error',
84
86
  '@blumintinc/blumint/enforce-safe-stringify': 'error',
85
87
  '@blumintinc/blumint/no-entire-object-hook-deps': 'error',
86
- '@blumintinc/blumint/no-compositing-layer-props': 'warn',
88
+ '@blumintinc/blumint/no-compositing-layer-props': 'error',
87
89
  '@blumintinc/blumint/enforce-firestore-doc-ref-generic': 'error',
90
+ '@blumintinc/blumint/semantic-function-prefixes': 'error',
91
+ '@blumintinc/blumint/enforce-mock-firestore': 'error',
88
92
  },
89
93
  },
90
94
  },
@@ -116,7 +120,7 @@ module.exports = {
116
120
  'require-dynamic-firebase-imports': require_dynamic_firebase_imports_1.default,
117
121
  'require-https-error': require_https_error_1.default,
118
122
  'use-custom-router': use_custom_router_1.useCustomRouter,
119
- 'require-image-overlayed': require_image_overlayed_1.default,
123
+ 'require-image-optimized': require_image_optimized_1.default,
120
124
  'require-usememo-object-literals': require_usememo_object_literals_1.requireUseMemoObjectLiterals,
121
125
  'enforce-safe-stringify': enforce_safe_stringify_1.enforceStableStringify,
122
126
  'avoid-utils-directory': avoid_utils_directory_1.avoidUtilsDirectory,
@@ -124,6 +128,8 @@ module.exports = {
124
128
  'enforce-firestore-path-utils': enforce_firestore_path_utils_1.enforceFirestorePathUtils,
125
129
  'no-compositing-layer-props': no_compositing_layer_props_1.noCompositingLayerProps,
126
130
  'enforce-firestore-doc-ref-generic': enforce_firestore_doc_ref_generic_1.enforceFirestoreDocRefGeneric,
131
+ 'semantic-function-prefixes': semantic_function_prefixes_1.semanticFunctionPrefixes,
132
+ 'enforce-mock-firestore': enforce_mock_firestore_1.enforceFirestoreMock,
127
133
  },
128
134
  };
129
135
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview Enforce generic argument for Firestore DocumentReference
2
+ * @fileoverview Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup
3
3
  * @author BluMint
4
4
  */
5
5
  type MessageIds = 'missingGeneric' | 'invalidGeneric';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @fileoverview Enforce generic argument for Firestore DocumentReference
3
+ * @fileoverview Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup
4
4
  * @author BluMint
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -18,14 +18,14 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
18
18
  meta: {
19
19
  type: 'problem',
20
20
  docs: {
21
- description: 'Enforce generic argument for Firestore DocumentReference',
21
+ description: 'Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup',
22
22
  recommended: 'error',
23
23
  requiresTypeChecking: true,
24
24
  },
25
25
  schema: [],
26
26
  messages: {
27
- missingGeneric: 'DocumentReference must specify a generic type argument',
28
- invalidGeneric: 'DocumentReference must not use "any" or "{}" as generic type argument',
27
+ missingGeneric: '{{ type }} must specify a generic type argument',
28
+ invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument',
29
29
  },
30
30
  },
31
31
  defaultOptions: [],
@@ -109,12 +109,16 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
109
109
  return {
110
110
  TSTypeReference(node) {
111
111
  if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
112
- node.typeName.name === 'DocumentReference') {
112
+ (node.typeName.name === 'DocumentReference' ||
113
+ node.typeName.name === 'CollectionReference' ||
114
+ node.typeName.name === 'CollectionGroup')) {
115
+ const typeName = node.typeName.name;
113
116
  // Check if generic type argument is missing
114
117
  if (!node.typeParameters || node.typeParameters.params.length === 0) {
115
118
  context.report({
116
119
  node,
117
120
  messageId: 'missingGeneric',
121
+ data: { type: typeName }
118
122
  });
119
123
  return;
120
124
  }
@@ -124,6 +128,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
124
128
  context.report({
125
129
  node,
126
130
  messageId: 'invalidGeneric',
131
+ data: { type: typeName }
127
132
  });
128
133
  }
129
134
  }
@@ -0,0 +1 @@
1
+ export declare const enforceFirestoreSetMerge: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferSetMerge", [], 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.enforceFirestoreSetMerge = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
7
+ name: 'enforce-firestore-set-merge',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce using set() with { merge: true } instead of update() for Firestore operations',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ preferSetMerge: 'Use set() with { merge: true } instead of update() for more predictable Firestore operations',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ const updateAliases = new Set();
23
+ function isFirestoreUpdateCall(node) {
24
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
25
+ const property = node.callee.property;
26
+ return property.type === utils_1.AST_NODE_TYPES.Identifier && property.name === 'update';
27
+ }
28
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
29
+ return updateAliases.has(node.callee.name);
30
+ }
31
+ return false;
32
+ }
33
+ function convertUpdateToSetMerge(node, sourceCode) {
34
+ const args = node.arguments;
35
+ if (args.length === 0)
36
+ return '';
37
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
38
+ const object = sourceCode.getText(node.callee.object);
39
+ if (object.includes('transaction')) {
40
+ const docRef = sourceCode.getText(args[0]);
41
+ const data = sourceCode.getText(args[1]);
42
+ return `${object}.set(${docRef}, ${data}, { merge: true })`;
43
+ }
44
+ const data = sourceCode.getText(args[0]);
45
+ return `${object}.set(${data}, { merge: true })`;
46
+ }
47
+ // For updateDoc from firebase/firestore
48
+ const docRef = sourceCode.getText(args[0]);
49
+ const data = args.length > 1 ? sourceCode.getText(args[1]) : '{}';
50
+ return `setDoc(${docRef}, ${data}, { merge: true })`;
51
+ }
52
+ return {
53
+ ImportDeclaration(node) {
54
+ if (node.source.value === 'firebase/firestore' || node.source.value === 'firebase-admin') {
55
+ node.specifiers.forEach(specifier => {
56
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
57
+ if (specifier.imported.name === 'updateDoc') {
58
+ updateAliases.add(specifier.local.name);
59
+ }
60
+ }
61
+ });
62
+ }
63
+ },
64
+ ImportExpression(node) {
65
+ if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
66
+ (node.source.value === 'firebase/firestore' || node.source.value === 'firebase-admin')) {
67
+ // Dynamic imports are handled in VariableDeclarator
68
+ }
69
+ },
70
+ VariableDeclarator(node) {
71
+ if (node.init?.type === utils_1.AST_NODE_TYPES.AwaitExpression &&
72
+ node.init.argument.type === utils_1.AST_NODE_TYPES.ImportExpression) {
73
+ const importSource = node.init.argument.source;
74
+ if (importSource.type === utils_1.AST_NODE_TYPES.Literal &&
75
+ (importSource.value === 'firebase/firestore' || importSource.value === 'firebase-admin')) {
76
+ // Handle destructured imports
77
+ if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
78
+ node.id.properties.forEach(prop => {
79
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
80
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
81
+ prop.key.name === 'updateDoc') {
82
+ if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
83
+ updateAliases.add(prop.value.name);
84
+ }
85
+ }
86
+ });
87
+ }
88
+ }
89
+ }
90
+ },
91
+ CallExpression(node) {
92
+ if (isFirestoreUpdateCall(node)) {
93
+ context.report({
94
+ node,
95
+ messageId: 'preferSetMerge',
96
+ fix(fixer) {
97
+ const newText = convertUpdateToSetMerge(node, context.getSourceCode());
98
+ return fixer.replaceText(node, newText);
99
+ },
100
+ });
101
+ }
102
+ },
103
+ };
104
+ },
105
+ });
106
+ //# sourceMappingURL=enforce-firestore-set-merge.js.map
@@ -0,0 +1,3 @@
1
+ type MessageIds = 'noManualFirestoreMock' | 'noMockFirebase';
2
+ export declare const enforceFirestoreMock: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceFirestoreMock = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const FIRESTORE_PATHS = [
7
+ 'functions/src/config/firebaseAdmin',
8
+ 'firebase-admin',
9
+ 'firebase-admin/firestore',
10
+ ];
11
+ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
12
+ name: 'enforce-mock-firestore',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description: 'Enforce using mockFirestore over manual Firestore mocking',
17
+ recommended: 'error',
18
+ },
19
+ schema: [],
20
+ messages: {
21
+ noManualFirestoreMock: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of manually mocking Firestore',
22
+ noMockFirebase: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of mockFirebase',
23
+ },
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ return {
28
+ // Detect jest.mock() calls for firebaseAdmin
29
+ CallExpression(node) {
30
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
31
+ node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
32
+ node.callee.object.name === 'jest' &&
33
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
34
+ node.callee.property.name === 'mock' &&
35
+ node.arguments.length > 0 &&
36
+ node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
37
+ typeof node.arguments[0].value === 'string' &&
38
+ FIRESTORE_PATHS.some((path) => node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
39
+ typeof node.arguments[0].value === 'string' &&
40
+ node.arguments[0].value?.includes(path))) {
41
+ // Check if the mock includes Firestore-related properties
42
+ const mockFn = node.arguments[1];
43
+ if (mockFn &&
44
+ mockFn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
45
+ const returnStmt = mockFn.body;
46
+ if (returnStmt.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
47
+ returnStmt.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
48
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
49
+ (prop.key.name === 'db' ||
50
+ prop.key.name === 'firestore' ||
51
+ prop.key.name === 'getFirestore'))) {
52
+ context.report({
53
+ node,
54
+ messageId: 'noManualFirestoreMock',
55
+ });
56
+ }
57
+ }
58
+ }
59
+ },
60
+ // Detect imports of mockFirebase
61
+ ImportDeclaration(node) {
62
+ if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
63
+ node.source.value === 'firestore-jest-mock' &&
64
+ node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
65
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
66
+ specifier.imported.name === 'mockFirebase')) {
67
+ context.report({
68
+ node,
69
+ messageId: 'noMockFirebase',
70
+ });
71
+ }
72
+ },
73
+ };
74
+ },
75
+ });
76
+ //# sourceMappingURL=enforce-mock-firestore.js.map
@@ -0,0 +1 @@
1
+ export declare const enforceVerbNounNaming: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"functionVerbPhrase", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.enforceVerbNounNaming = void 0;
7
+ const utils_1 = require("@typescript-eslint/utils");
8
+ const createRule_1 = require("../utils/createRule");
9
+ const compromise_1 = __importDefault(require("compromise"));
10
+ const PREPOSITIONS = ['to', 'from', 'with', 'by', 'at', 'of'];
11
+ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
12
+ name: 'enforce-verb-noun-naming',
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: {
16
+ description: 'Enforce verb phrases for functions and methods',
17
+ recommended: 'error',
18
+ },
19
+ schema: [],
20
+ messages: {
21
+ functionVerbPhrase: 'Function names should start with a verb phrase (e.g., fetchData, processRequest)',
22
+ },
23
+ },
24
+ defaultOptions: [],
25
+ create(context) {
26
+ function extractFirstWord(name) {
27
+ const firstChar = name.charAt(0);
28
+ const rest = name.slice(1);
29
+ const words = rest.split(/(?=[A-Z])/);
30
+ return firstChar + words[0];
31
+ }
32
+ function toSentence(name) {
33
+ return name.split(/(?=[A-Z])/).join(' ');
34
+ }
35
+ function getPossibleTags(sentence) {
36
+ const doc = (0, compromise_1.default)(sentence);
37
+ const terms = doc.terms().json();
38
+ if (terms.length === 0 || !terms[0].terms || !terms[0].terms[0].tags)
39
+ return [];
40
+ const tags = terms[0].terms[0].tags;
41
+ return tags;
42
+ }
43
+ function isVerbPhrase(name) {
44
+ const firstWord = extractFirstWord(name);
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ if (PREPOSITIONS.includes(firstWord.toLowerCase())) {
47
+ return true;
48
+ }
49
+ const tags = getPossibleTags(toSentence(name));
50
+ const isVerb = tags.includes('Verb');
51
+ const isPreposition = tags.includes('Preposition');
52
+ const isConjunction = tags.includes('Conjunction');
53
+ return isVerb || isPreposition || isConjunction;
54
+ }
55
+ function isJsxReturnFunction(node) {
56
+ if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
57
+ node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
58
+ node.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
59
+ return false;
60
+ }
61
+ // Check if function returns JSX
62
+ const sourceCode = context.getSourceCode();
63
+ const text = sourceCode.getText(node);
64
+ return text.includes('return <') || text.includes('=> <');
65
+ }
66
+ return {
67
+ FunctionDeclaration(node) {
68
+ if (!node.id)
69
+ return;
70
+ if (isJsxReturnFunction(node)) {
71
+ return;
72
+ }
73
+ if (!isVerbPhrase(node.id.name)) {
74
+ context.report({
75
+ node: node.id,
76
+ messageId: 'functionVerbPhrase',
77
+ });
78
+ }
79
+ },
80
+ VariableDeclarator(node) {
81
+ if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
82
+ return;
83
+ // Only check if it's a function
84
+ if (node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
85
+ node.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
86
+ if (isJsxReturnFunction(node.init)) {
87
+ return;
88
+ }
89
+ if (!isVerbPhrase(node.id.name)) {
90
+ context.report({
91
+ node: node.id,
92
+ messageId: 'functionVerbPhrase',
93
+ });
94
+ }
95
+ }
96
+ },
97
+ MethodDefinition(node) {
98
+ if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
99
+ return;
100
+ if (!isVerbPhrase(node.key.name)) {
101
+ context.report({
102
+ node: node.key,
103
+ messageId: 'functionVerbPhrase',
104
+ });
105
+ }
106
+ },
107
+ };
108
+ },
109
+ });
110
+ //# sourceMappingURL=enforce-verb-noun-naming.js.map
@@ -19,13 +19,42 @@ function isFunctionDefinition(node) {
19
19
  return (node?.type === 'FunctionExpression' ||
20
20
  node?.type === 'ArrowFunctionExpression');
21
21
  }
22
+ function isImmutableValue(node) {
23
+ if (!node)
24
+ return false;
25
+ switch (node.type) {
26
+ case 'Literal':
27
+ return true;
28
+ case 'TemplateLiteral':
29
+ return node.expressions.length === 0;
30
+ case 'UnaryExpression':
31
+ return isImmutableValue(node.argument);
32
+ case 'BinaryExpression':
33
+ if (node.left.type === 'PrivateIdentifier')
34
+ return false;
35
+ return isImmutableValue(node.left) && isImmutableValue(node.right);
36
+ default:
37
+ return false;
38
+ }
39
+ }
22
40
  function isMutableValue(node) {
23
41
  if (!node)
24
42
  return false;
25
- // Check for array literals and object expressions
26
- if (node.type === 'ArrayExpression' || node.type === 'ObjectExpression') {
43
+ // Check for object expressions (always mutable)
44
+ if (node.type === 'ObjectExpression') {
27
45
  return true;
28
46
  }
47
+ // Check array literals - mutable if empty or if they contain mutable values
48
+ if (node.type === 'ArrayExpression') {
49
+ // Empty arrays are mutable since they can be modified later
50
+ if (node.elements.length === 0)
51
+ return true;
52
+ // Arrays with spread elements are mutable
53
+ if (node.elements.some(element => !element || element.type === 'SpreadElement'))
54
+ return true;
55
+ // Arrays with non-immutable values are mutable
56
+ return node.elements.some(element => !isImmutableValue(element));
57
+ }
29
58
  // Check for new expressions (e.g., new Map(), new Set())
30
59
  if (node.type === 'NewExpression') {
31
60
  return true;
@@ -118,6 +118,10 @@ exports.default = (0, createRule_1.createRule)({
118
118
  return false;
119
119
  }
120
120
  // Check if it's a literal, array, or object that should have as const
121
+ // Skip regular expressions as they are already immutable
122
+ if (node.type === utils_1.AST_NODE_TYPES.Literal && 'regex' in node) {
123
+ return false;
124
+ }
121
125
  return (node.type === utils_1.AST_NODE_TYPES.Literal ||
122
126
  node.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
123
127
  node.type === utils_1.AST_NODE_TYPES.ObjectExpression);
@@ -42,22 +42,31 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
42
42
  function getObjectUsagesInHook(hookBody, objectName) {
43
43
  const usages = new Set();
44
44
  const visited = new Set();
45
+ let needsEntireObject = false;
45
46
  function buildAccessPath(node) {
46
47
  const parts = [];
47
48
  let current = node;
49
+ let hasOptionalChaining = false;
50
+ // Collect all parts from leaf to root
48
51
  while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
49
- if (current.computed) {
52
+ const memberExpr = current;
53
+ if (memberExpr.computed) {
50
54
  return null; // Skip computed properties
51
55
  }
52
- if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
56
+ if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
53
57
  return null;
54
58
  }
55
- parts.unshift(current.property.name);
56
- current = current.object;
59
+ parts.unshift(memberExpr.property.name);
60
+ if (memberExpr.optional) {
61
+ hasOptionalChaining = true;
62
+ }
63
+ current = memberExpr.object;
57
64
  }
58
- if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
59
- current.name === objectName) {
60
- return parts.join('.');
65
+ // Check if we reached the target identifier
66
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier && current.name === objectName) {
67
+ // Build the path with optional chaining
68
+ const path = objectName + (hasOptionalChaining ? '?' : '') + parts.map(part => '.' + part).join('');
69
+ return path;
61
70
  }
62
71
  return null;
63
72
  }
@@ -65,10 +74,27 @@ function getObjectUsagesInHook(hookBody, objectName) {
65
74
  if (visited.has(node))
66
75
  return;
67
76
  visited.add(node);
68
- if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
77
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
78
+ // Check if the object is directly passed as an argument
79
+ node.arguments.forEach((arg) => {
80
+ if (arg.type === utils_1.AST_NODE_TYPES.Identifier &&
81
+ arg.name === objectName) {
82
+ needsEntireObject = true;
83
+ }
84
+ });
85
+ }
86
+ else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
87
+ // If we find a spread operator with our target object, consider it as accessing all properties
88
+ if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
89
+ node.argument.name === objectName) {
90
+ needsEntireObject = true;
91
+ return;
92
+ }
93
+ }
94
+ else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
69
95
  const path = buildAccessPath(node);
70
96
  if (path) {
71
- usages.add(`${objectName}.${path}`);
97
+ usages.add(path);
72
98
  }
73
99
  }
74
100
  // Visit all child nodes
@@ -90,6 +116,10 @@ function getObjectUsagesInHook(hookBody, objectName) {
90
116
  }
91
117
  }
92
118
  visit(hookBody);
119
+ // If the entire object is needed, return an empty set to indicate valid usage
120
+ if (needsEntireObject) {
121
+ return new Set();
122
+ }
93
123
  // Filter out intermediate paths
94
124
  const paths = Array.from(usages);
95
125
  const filteredPaths = paths.filter((path) => !paths.some((otherPath) => otherPath !== path && otherPath.startsWith(path + '.')));
@@ -145,6 +175,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
145
175
  }
146
176
  const usages = getObjectUsagesInHook(callbackArg.body, objectName);
147
177
  // If we found specific field usages and the entire object is in deps
178
+ // Skip reporting if usages is empty (indicates spread operator usage)
148
179
  if (usages.size > 0) {
149
180
  const fields = Array.from(usages).join(', ');
150
181
  context.report({
@@ -0,0 +1,11 @@
1
+ type Options = [
2
+ {
3
+ allowRecursiveFunctions?: boolean;
4
+ allowOverloadedFunctions?: boolean;
5
+ allowInterfaceMethodSignatures?: boolean;
6
+ allowAbstractMethodSignatures?: boolean;
7
+ allowDtsFiles?: boolean;
8
+ }
9
+ ];
10
+ export declare const noExplicitReturnType: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noExplicitReturnType", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
11
+ export {};
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noExplicitReturnType = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const defaultOptions = {
7
+ allowRecursiveFunctions: true,
8
+ allowOverloadedFunctions: true,
9
+ allowInterfaceMethodSignatures: true,
10
+ allowAbstractMethodSignatures: true,
11
+ allowDtsFiles: true,
12
+ };
13
+ function isRecursiveFunction(node) {
14
+ const functionName = node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ? node.id?.name :
15
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression && node.id ? node.id.name :
16
+ undefined;
17
+ if (!functionName || !node.body)
18
+ return false;
19
+ let hasRecursiveCall = false;
20
+ function checkNode(node) {
21
+ if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
22
+ node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
23
+ node.callee.name === functionName) {
24
+ hasRecursiveCall = true;
25
+ return;
26
+ }
27
+ // Only traverse specific node types to avoid circular references
28
+ if (node.type === utils_1.AST_NODE_TYPES.BlockStatement) {
29
+ node.body.forEach(checkNode);
30
+ }
31
+ else if (node.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
32
+ checkNode(node.expression);
33
+ }
34
+ else if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
35
+ checkNode(node.callee);
36
+ node.arguments.forEach(checkNode);
37
+ }
38
+ else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
39
+ checkNode(node.left);
40
+ checkNode(node.right);
41
+ }
42
+ else if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement && node.argument) {
43
+ checkNode(node.argument);
44
+ }
45
+ else if (node.type === utils_1.AST_NODE_TYPES.IfStatement) {
46
+ checkNode(node.test);
47
+ checkNode(node.consequent);
48
+ if (node.alternate) {
49
+ checkNode(node.alternate);
50
+ }
51
+ }
52
+ }
53
+ checkNode(node.body);
54
+ return hasRecursiveCall;
55
+ }
56
+ function isOverloadedFunction(node) {
57
+ if (!node.parent)
58
+ return false;
59
+ if (node.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
60
+ const interfaceBody = node.parent;
61
+ if (interfaceBody.type !== utils_1.AST_NODE_TYPES.TSInterfaceBody)
62
+ return false;
63
+ const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : undefined;
64
+ if (!methodName)
65
+ return false;
66
+ return interfaceBody.body.filter(member => member.type === utils_1.AST_NODE_TYPES.TSMethodSignature &&
67
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
68
+ member.key.name === methodName).length > 1;
69
+ }
70
+ return false;
71
+ }
72
+ function isInterfaceOrAbstractMethodSignature(node) {
73
+ if (node.type === utils_1.AST_NODE_TYPES.TSMethodSignature)
74
+ return true;
75
+ if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
76
+ let current = node;
77
+ while (current) {
78
+ if (current.type === utils_1.AST_NODE_TYPES.ClassDeclaration && current.abstract) {
79
+ return true;
80
+ }
81
+ current = current.parent;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ exports.noExplicitReturnType = (0, createRule_1.createRule)({
87
+ name: 'no-explicit-return-type',
88
+ meta: {
89
+ type: 'suggestion',
90
+ docs: {
91
+ description: 'Disallow explicit return types on functions',
92
+ recommended: 'error',
93
+ },
94
+ fixable: 'code',
95
+ schema: [
96
+ {
97
+ type: 'object',
98
+ properties: {
99
+ allowRecursiveFunctions: { type: 'boolean' },
100
+ allowOverloadedFunctions: { type: 'boolean' },
101
+ allowInterfaceMethodSignatures: { type: 'boolean' },
102
+ allowAbstractMethodSignatures: { type: 'boolean' },
103
+ allowDtsFiles: { type: 'boolean' },
104
+ },
105
+ additionalProperties: false,
106
+ },
107
+ ],
108
+ messages: {
109
+ noExplicitReturnType: 'Explicit return type is not allowed. Let TypeScript infer it.',
110
+ },
111
+ },
112
+ defaultOptions: [defaultOptions],
113
+ create(context, [options]) {
114
+ const mergedOptions = { ...defaultOptions, ...options };
115
+ const filename = context.getFilename();
116
+ if (mergedOptions.allowDtsFiles && filename.endsWith('.d.ts')) {
117
+ return {};
118
+ }
119
+ function fixReturnType(fixer, node) {
120
+ const returnType = node.returnType || node.value?.returnType;
121
+ if (!returnType)
122
+ return null;
123
+ // Create a fix that removes the return type annotation
124
+ return fixer.remove(returnType);
125
+ }
126
+ return {
127
+ FunctionDeclaration(node) {
128
+ if (!node.returnType)
129
+ return;
130
+ if (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node)) {
131
+ return;
132
+ }
133
+ context.report({
134
+ node: node.returnType,
135
+ messageId: 'noExplicitReturnType',
136
+ fix: fixer => fixReturnType(fixer, node),
137
+ });
138
+ },
139
+ FunctionExpression(node) {
140
+ if (!node.returnType)
141
+ return;
142
+ if (mergedOptions.allowRecursiveFunctions && isRecursiveFunction(node)) {
143
+ return;
144
+ }
145
+ context.report({
146
+ node: node.returnType,
147
+ messageId: 'noExplicitReturnType',
148
+ fix: fixer => fixReturnType(fixer, node),
149
+ });
150
+ },
151
+ ArrowFunctionExpression(node) {
152
+ if (!node.returnType)
153
+ return;
154
+ context.report({
155
+ node: node.returnType,
156
+ messageId: 'noExplicitReturnType',
157
+ fix: fixer => fixReturnType(fixer, node),
158
+ });
159
+ },
160
+ TSMethodSignature(node) {
161
+ if (!node.returnType)
162
+ return;
163
+ if (mergedOptions.allowInterfaceMethodSignatures) {
164
+ return;
165
+ }
166
+ if (mergedOptions.allowOverloadedFunctions && isOverloadedFunction(node)) {
167
+ return;
168
+ }
169
+ context.report({
170
+ node: node.returnType,
171
+ messageId: 'noExplicitReturnType',
172
+ fix: fixer => fixReturnType(fixer, node),
173
+ });
174
+ },
175
+ MethodDefinition(node) {
176
+ if (!node.value.returnType)
177
+ return;
178
+ if (mergedOptions.allowAbstractMethodSignatures && isInterfaceOrAbstractMethodSignature(node)) {
179
+ return;
180
+ }
181
+ context.report({
182
+ node: node.value.returnType,
183
+ messageId: 'noExplicitReturnType',
184
+ fix: fixer => fixReturnType(fixer, node),
185
+ });
186
+ },
187
+ };
188
+ },
189
+ });
190
+ //# sourceMappingURL=no-explicit-return-type.js.map
@@ -22,23 +22,32 @@ module.exports = (0, createRule_1.createRule)({
22
22
  if (!filename.includes('functions/src')) {
23
23
  return {};
24
24
  }
25
- let hasFirebaseAdminImport = false;
26
25
  let httpsIdentifier = null;
26
+ let httpsErrorIdentifier = null;
27
27
  return {
28
28
  ImportDeclaration(node) {
29
29
  if (node.source.value === 'firebase-admin' ||
30
30
  node.source.value === 'firebase-admin/lib/https-error') {
31
- hasFirebaseAdminImport = true;
32
- // Track the local name of the https import
31
+ // Check for direct HttpsError import
32
+ const httpsErrorSpecifier = node.specifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
33
+ spec.imported.name === 'HttpsError');
34
+ // Check for https import that could be used for https.HttpsError
33
35
  const httpsSpecifier = node.specifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
34
36
  spec.imported.name === 'https');
37
+ if (httpsErrorSpecifier && 'local' in httpsErrorSpecifier) {
38
+ httpsErrorIdentifier = httpsErrorSpecifier.local.name;
39
+ context.report({
40
+ node,
41
+ messageId: 'useProprietaryHttpsError',
42
+ });
43
+ }
35
44
  if (httpsSpecifier && 'local' in httpsSpecifier) {
36
45
  httpsIdentifier = httpsSpecifier.local.name;
46
+ context.report({
47
+ node,
48
+ messageId: 'useProprietaryHttpsError',
49
+ });
37
50
  }
38
- context.report({
39
- node,
40
- messageId: 'useProprietaryHttpsError',
41
- });
42
51
  }
43
52
  },
44
53
  ThrowStatement(node) {
@@ -60,17 +69,14 @@ module.exports = (0, createRule_1.createRule)({
60
69
  return;
61
70
  }
62
71
  // Check for firebase-admin HttpsError usage
63
- if (!hasFirebaseAdminImport) {
64
- return;
65
- }
66
- const isHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
67
- callee.name === 'HttpsError';
68
72
  const isFirebaseHttpsError = callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
69
73
  callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
70
74
  callee.object.name === httpsIdentifier &&
71
75
  callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
72
76
  callee.property.name === 'HttpsError';
73
- if (isHttpsError || isFirebaseHttpsError) {
77
+ const isDirectHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
78
+ callee.name === httpsErrorIdentifier;
79
+ if (isFirebaseHttpsError || isDirectHttpsError) {
74
80
  context.report({
75
81
  node,
76
82
  messageId: 'useProprietaryHttpsError',
@@ -0,0 +1,7 @@
1
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useImageOptimized", {
2
+ componentPath: string;
3
+ }[], {
4
+ JSXElement(node: any): void;
5
+ ImportDeclaration(node: any): void;
6
+ }>;
7
+ export = _default;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ const createRule_1 = require("../utils/createRule");
3
+ module.exports = (0, createRule_1.createRule)({
4
+ name: 'require-image-optimized',
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Enforce using ImageOptimized component instead of next/image or img tags',
9
+ recommended: 'error',
10
+ requiresTypeChecking: false,
11
+ },
12
+ fixable: 'code',
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ componentPath: {
18
+ type: 'string',
19
+ description: 'The import path for the ImageOptimized component',
20
+ default: 'src/components/image/ImageOptimized',
21
+ },
22
+ },
23
+ additionalProperties: false,
24
+ },
25
+ ],
26
+ messages: {
27
+ useImageOptimized: 'Use ImageOptimized component from {{ componentPath }} instead of {{ component }}',
28
+ },
29
+ },
30
+ defaultOptions: [{ componentPath: 'src/components/image/ImageOptimized' }],
31
+ create(context) {
32
+ const options = context.options[0] || {
33
+ componentPath: 'src/components/image/ImageOptimized',
34
+ };
35
+ const sourceCode = context.getSourceCode();
36
+ return {
37
+ // Handle JSX img elements
38
+ JSXElement(node) {
39
+ if (node.openingElement.name.name === 'img') {
40
+ context.report({
41
+ node,
42
+ messageId: 'useImageOptimized',
43
+ data: {
44
+ componentPath: options.componentPath,
45
+ component: 'img tag',
46
+ },
47
+ fix(fixer) {
48
+ const attributes = node.openingElement.attributes
49
+ .map((attr) => sourceCode.getText(attr))
50
+ .join(' ');
51
+ return fixer.replaceText(node, `<ImageOptimized ${attributes} />`);
52
+ },
53
+ });
54
+ }
55
+ },
56
+ // Handle next/image imports and usage
57
+ ImportDeclaration(node) {
58
+ if (node.source.value === 'next/image' && node.specifiers.length > 0) {
59
+ const imageSpecifier = node.specifiers.find((spec) => (spec.type === 'ImportDefaultSpecifier' ||
60
+ spec.type === 'ImportSpecifier') &&
61
+ (spec.local.name === 'Image' || spec.imported?.name === 'Image'));
62
+ if (imageSpecifier) {
63
+ const localName = imageSpecifier.local.name;
64
+ // Report the import
65
+ context.report({
66
+ node,
67
+ messageId: 'useImageOptimized',
68
+ data: {
69
+ componentPath: options.componentPath,
70
+ component: 'next/image',
71
+ },
72
+ fix(fixer) {
73
+ return fixer.replaceText(node, `import ${localName} from '${options.componentPath}';`);
74
+ },
75
+ });
76
+ }
77
+ }
78
+ },
79
+ };
80
+ },
81
+ });
82
+ //# sourceMappingURL=require-image-optimized.js.map
@@ -0,0 +1 @@
1
+ export declare const semanticFunctionPrefixes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"avoidGenericPrefix", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.semanticFunctionPrefixes = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const DISALLOWED_PREFIXES = new Set(['get', 'update', 'check', 'manage', 'process', 'do']);
7
+ const SUGGESTED_ALTERNATIVES = {
8
+ get: ['fetch', 'retrieve', 'compute', 'derive'],
9
+ update: ['modify', 'set', 'apply'],
10
+ check: ['validate', 'assert', 'ensure'],
11
+ manage: ['control', 'coordinate', 'schedule'],
12
+ process: ['transform', 'sanitize', 'compute'],
13
+ do: ['execute', 'perform', 'apply'],
14
+ };
15
+ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
16
+ name: 'semantic-function-prefixes',
17
+ meta: {
18
+ type: 'suggestion',
19
+ docs: {
20
+ description: 'Enforce semantic function prefixes over generic ones like "get" and "update"',
21
+ recommended: 'error',
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ avoidGenericPrefix: 'Avoid using generic prefix "{{prefix}}". Consider using one of these alternatives: {{alternatives}}',
26
+ },
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ function checkFunctionName(node) {
31
+ // Skip anonymous functions
32
+ if (!node.id && node.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
33
+ return;
34
+ }
35
+ // Get function name from either the function declaration or variable declarator
36
+ let functionName = '';
37
+ if (node.id) {
38
+ functionName = node.id.name;
39
+ }
40
+ else if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
41
+ functionName = node.parent.id.name;
42
+ }
43
+ if (!functionName)
44
+ return;
45
+ // Skip if function starts with 'is' (boolean check functions are okay)
46
+ if (functionName.startsWith('is'))
47
+ return;
48
+ // Skip class getters
49
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition && node.parent.kind === 'get') {
50
+ return;
51
+ }
52
+ // Check for disallowed prefixes
53
+ for (const prefix of DISALLOWED_PREFIXES) {
54
+ if (functionName.toLowerCase().startsWith(prefix.toLowerCase())) {
55
+ context.report({
56
+ node: node.id || node,
57
+ messageId: 'avoidGenericPrefix',
58
+ data: {
59
+ prefix,
60
+ alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
61
+ },
62
+ });
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ return {
68
+ FunctionDeclaration: checkFunctionName,
69
+ FunctionExpression: checkFunctionName,
70
+ ArrowFunctionExpression: checkFunctionName,
71
+ };
72
+ },
73
+ });
74
+ //# sourceMappingURL=semantic-function-prefixes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -45,6 +45,7 @@
45
45
  "commitlint": "commitlint --edit"
46
46
  },
47
47
  "dependencies": {
48
+ "compromise": "14.14.4",
48
49
  "requireindex": "1.2.0"
49
50
  },
50
51
  "devDependencies": {