@blumintinc/eslint-plugin-blumint 1.3.2 → 1.5.0
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 +41 -40
- package/lib/index.js +37 -1
- package/lib/rules/array-methods-this-context.js +2 -2
- package/lib/rules/enforce-callback-memo.js +3 -3
- package/lib/rules/enforce-dynamic-firebase-imports.js +2 -2
- package/lib/rules/enforce-exported-function-types.js +88 -85
- package/lib/rules/enforce-firestore-doc-ref-generic.js +7 -2
- package/lib/rules/enforce-firestore-path-utils.js +2 -2
- package/lib/rules/enforce-firestore-set-merge.js +45 -8
- package/lib/rules/enforce-identifiable-firestore-type.js +2 -2
- package/lib/rules/enforce-memoize-async.js +2 -2
- package/lib/rules/enforce-mock-firestore.js +3 -3
- package/lib/rules/enforce-realtimedb-path-utils.js +1 -1
- package/lib/rules/enforce-safe-stringify.js +2 -2
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-verb-noun-naming.js +41 -7
- package/lib/rules/no-async-array-filter.js +2 -2
- package/lib/rules/no-async-foreach.js +1 -1
- package/lib/rules/no-class-instance-destructuring.d.ts +1 -0
- package/lib/rules/no-class-instance-destructuring.js +95 -0
- package/lib/rules/no-compositing-layer-props.js +1 -1
- package/lib/rules/no-conditional-literals-in-jsx.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +1 -1
- package/lib/rules/no-explicit-return-type.d.ts +3 -1
- package/lib/rules/no-explicit-return-type.js +30 -17
- package/lib/rules/no-filter-without-return.js +1 -1
- package/lib/rules/no-misused-switch-case.js +2 -2
- package/lib/rules/no-redundant-param-types.d.ts +2 -0
- package/lib/rules/no-redundant-param-types.js +129 -0
- package/lib/rules/no-unused-props.js +1 -1
- package/lib/rules/no-useless-fragment.js +1 -1
- package/lib/rules/prefer-destructuring-no-class.d.ts +8 -0
- package/lib/rules/prefer-destructuring-no-class.js +200 -0
- package/lib/rules/require-usememo-object-literals.js +1 -1
- package/lib/rules/semantic-function-prefixes.js +43 -4
- package/package.json +2 -2
|
@@ -8,24 +8,58 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'suggestion',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce using set() with { merge: true } instead of update() for Firestore operations',
|
|
11
|
+
description: 'Enforce using set() with { merge: true } instead of update() for Firestore operations to ensure consistent behavior. The update() method fails if the document does not exist, while set() with { merge: true } creates the document if needed and safely merges fields, making it more reliable and predictable.',
|
|
12
12
|
recommended: 'error',
|
|
13
|
+
requiresTypeChecking: false,
|
|
14
|
+
extendsBaseRule: false,
|
|
13
15
|
},
|
|
14
16
|
fixable: 'code',
|
|
15
17
|
schema: [],
|
|
16
18
|
messages: {
|
|
17
|
-
preferSetMerge: 'Use set() with { merge: true } instead of update() for more predictable Firestore operations',
|
|
19
|
+
preferSetMerge: 'Use set() with { merge: true } instead of update() for more predictable Firestore operations. Instead of `docRef.update({ field: value })`, use `docRef.set({ field: value }, { merge: true })`. This ensures consistent behavior when the document does not exist.',
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
22
|
defaultOptions: [],
|
|
21
23
|
create(context) {
|
|
22
24
|
const updateAliases = new Set();
|
|
23
25
|
function isFirestoreUpdateCall(node) {
|
|
26
|
+
// Check if it's a set() call with merge: true
|
|
24
27
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
25
28
|
const property = node.callee.property;
|
|
26
|
-
|
|
29
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
30
|
+
// If it's a set() call, check if it has merge: true
|
|
31
|
+
if (property.name === 'set') {
|
|
32
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
33
|
+
if (lastArg?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
34
|
+
const hasMergeTrue = lastArg.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
35
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
36
|
+
prop.key.name === 'merge' &&
|
|
37
|
+
prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
38
|
+
prop.value.value === true);
|
|
39
|
+
if (hasMergeTrue) {
|
|
40
|
+
return false; // Already using set with merge: true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Only flag update() calls
|
|
45
|
+
return property.name === 'update';
|
|
46
|
+
}
|
|
27
47
|
}
|
|
28
48
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
49
|
+
// Check if it's a setDoc() call with merge: true
|
|
50
|
+
if (node.callee.name === 'setDoc') {
|
|
51
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
52
|
+
if (lastArg?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
53
|
+
const hasMergeTrue = lastArg.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
54
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
55
|
+
prop.key.name === 'merge' &&
|
|
56
|
+
prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
57
|
+
prop.value.value === true);
|
|
58
|
+
if (hasMergeTrue) {
|
|
59
|
+
return false; // Already using setDoc with merge: true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
29
63
|
return updateAliases.has(node.callee.name);
|
|
30
64
|
}
|
|
31
65
|
return false;
|
|
@@ -51,8 +85,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
51
85
|
}
|
|
52
86
|
return {
|
|
53
87
|
ImportDeclaration(node) {
|
|
54
|
-
if (node.source.value === 'firebase/firestore' ||
|
|
55
|
-
node.
|
|
88
|
+
if (node.source.value === 'firebase/firestore' ||
|
|
89
|
+
node.source.value === 'firebase-admin') {
|
|
90
|
+
node.specifiers.forEach((specifier) => {
|
|
56
91
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
57
92
|
if (specifier.imported.name === 'updateDoc') {
|
|
58
93
|
updateAliases.add(specifier.local.name);
|
|
@@ -63,7 +98,8 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
63
98
|
},
|
|
64
99
|
ImportExpression(node) {
|
|
65
100
|
if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
66
|
-
(node.source.value === 'firebase/firestore' ||
|
|
101
|
+
(node.source.value === 'firebase/firestore' ||
|
|
102
|
+
node.source.value === 'firebase-admin')) {
|
|
67
103
|
// Dynamic imports are handled in VariableDeclarator
|
|
68
104
|
}
|
|
69
105
|
},
|
|
@@ -72,10 +108,11 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
72
108
|
node.init.argument.type === utils_1.AST_NODE_TYPES.ImportExpression) {
|
|
73
109
|
const importSource = node.init.argument.source;
|
|
74
110
|
if (importSource.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
75
|
-
(importSource.value === 'firebase/firestore' ||
|
|
111
|
+
(importSource.value === 'firebase/firestore' ||
|
|
112
|
+
importSource.value === 'firebase-admin')) {
|
|
76
113
|
// Handle destructured imports
|
|
77
114
|
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
78
|
-
node.id.properties.forEach(prop => {
|
|
115
|
+
node.id.properties.forEach((prop) => {
|
|
79
116
|
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
80
117
|
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
118
|
prop.key.name === 'updateDoc') {
|
|
@@ -17,8 +17,8 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
|
|
|
17
17
|
},
|
|
18
18
|
schema: [],
|
|
19
19
|
messages: {
|
|
20
|
-
missingType: 'Expected exported type "{{ typeName }}" in index.ts under folder "{{ folderName }}"',
|
|
21
|
-
notExtendingIdentifiable: 'Type "{{ typeName }}" must extend "Identifiable"
|
|
20
|
+
missingType: 'Expected exported type "{{ typeName }}" in index.ts under folder "{{ folderName }}". Create a type that matches the folder name: `export type {{ typeName }} = { /* fields */ }`.',
|
|
21
|
+
notExtendingIdentifiable: 'Type "{{ typeName }}" must extend "Identifiable" to ensure all Firestore documents have an ID field. Add `extends Identifiable` or include `id: string`: `export type {{ typeName }} = { id: string; /* other fields */ }`.',
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
defaultOptions: [],
|
|
@@ -8,13 +8,13 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'suggestion',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters',
|
|
11
|
+
description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. This improves performance by reusing previous results when the same parameters are provided, particularly useful for data fetching methods.',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize()',
|
|
17
|
+
requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize() to cache results and improve performance. Instead of `async getData(id?: string)`, use `@Memoize()\nasync getData(id?: string)`. Import Memoize from "typescript-memoize".',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -13,13 +13,13 @@ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
-
description: 'Enforce using mockFirestore
|
|
16
|
+
description: 'Enforce using the standardized mockFirestore utility instead of manual Firestore mocking or third-party mocks. This ensures consistent test behavior across the codebase, reduces boilerplate, and provides type-safe mocking of Firestore operations.',
|
|
17
17
|
recommended: 'error',
|
|
18
18
|
},
|
|
19
19
|
schema: [],
|
|
20
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',
|
|
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"`.',
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
defaultOptions: [],
|
|
@@ -14,7 +14,7 @@ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requirePathUtil: 'Use a utility function
|
|
17
|
+
requirePathUtil: 'Use a utility function for Realtime Database paths to ensure type safety and maintainability. Instead of `ref("users/" + userId)`, create and use a utility function: `const toUserPath = (id: string) => `users/${id}`; ref(toUserPath(userId))`.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -8,13 +8,13 @@ exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'problem',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce using safe-stable-stringify instead of JSON.stringify',
|
|
11
|
+
description: 'Enforce using safe-stable-stringify instead of JSON.stringify to handle circular references and ensure deterministic output. JSON.stringify can throw errors on circular references and produce inconsistent output for objects with the same properties in different orders. safe-stable-stringify handles these cases safely.',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
useStableStringify: 'Use safe-stable-stringify instead of JSON.stringify for safer serialization',
|
|
17
|
+
useStableStringify: 'Use safe-stable-stringify instead of JSON.stringify for safer serialization. Replace `JSON.stringify(obj)` with `stringify(obj)`. First import it: `import stringify from "safe-stable-stringify"`. This handles circular references and provides deterministic output.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -17,7 +17,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
17
17
|
meta: {
|
|
18
18
|
type: 'problem',
|
|
19
19
|
docs: {
|
|
20
|
-
description: 'Enforce serializable parameters in Firebase Callable/HTTPS Cloud Functions',
|
|
20
|
+
description: 'Enforce serializable parameters in Firebase Callable/HTTPS Cloud Functions to prevent runtime errors. Firebase Functions can only pass JSON-serializable data, so using non-serializable types like Date, DocumentReference, or Map will cause failures. Use primitive types, plain objects, and arrays instead, converting complex types to their serializable representations (e.g., Date to ISO string).',
|
|
21
21
|
recommended: 'error',
|
|
22
22
|
},
|
|
23
23
|
schema: [
|
|
@@ -38,8 +38,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
messages: {
|
|
41
|
-
nonSerializableParam: 'Parameter type "{{ type }}" is not serializable',
|
|
42
|
-
nonSerializableProperty: 'Property "{{ prop }}" has non-serializable type "{{ type }}"',
|
|
41
|
+
nonSerializableParam: 'Parameter type "{{ type }}" is not serializable in Firebase Cloud Functions. Use JSON-serializable types like string, number, boolean, arrays, or plain objects. Instead of `Date`, use ISO strings: `new Date().toISOString()`.',
|
|
42
|
+
nonSerializableProperty: 'Property "{{ prop }}" has non-serializable type "{{ type }}". Use JSON-serializable types. For example, instead of `{ timestamp: Date }`, use `{ timestamp: string }` with ISO format.',
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
defaultOptions: [
|
|
@@ -8,6 +8,22 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
8
8
|
const createRule_1 = require("../utils/createRule");
|
|
9
9
|
const compromise_1 = __importDefault(require("compromise"));
|
|
10
10
|
const PREPOSITIONS = ['to', 'from', 'with', 'by', 'at', 'of'];
|
|
11
|
+
// Common short verbs that should be allowed
|
|
12
|
+
const COMMON_VERBS = new Set([
|
|
13
|
+
'sync',
|
|
14
|
+
'fix',
|
|
15
|
+
'set',
|
|
16
|
+
'log',
|
|
17
|
+
'get',
|
|
18
|
+
'put',
|
|
19
|
+
'add',
|
|
20
|
+
'map',
|
|
21
|
+
'run',
|
|
22
|
+
'use',
|
|
23
|
+
'has',
|
|
24
|
+
'is',
|
|
25
|
+
'do',
|
|
26
|
+
]);
|
|
11
27
|
exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
12
28
|
name: 'enforce-verb-noun-naming',
|
|
13
29
|
meta: {
|
|
@@ -15,6 +31,8 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
15
31
|
docs: {
|
|
16
32
|
description: 'Enforce verb phrases for functions and methods',
|
|
17
33
|
recommended: 'error',
|
|
34
|
+
requiresTypeChecking: false,
|
|
35
|
+
extendsBaseRule: false,
|
|
18
36
|
},
|
|
19
37
|
schema: [],
|
|
20
38
|
messages: {
|
|
@@ -30,7 +48,8 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
30
48
|
return firstChar + words[0];
|
|
31
49
|
}
|
|
32
50
|
function toSentence(name) {
|
|
33
|
-
|
|
51
|
+
// Add "I" prefix to create a proper sentence for better verb detection
|
|
52
|
+
return 'I ' + name.split(/(?=[A-Z])/).join(' ');
|
|
34
53
|
}
|
|
35
54
|
function getPossibleTags(sentence) {
|
|
36
55
|
const doc = (0, compromise_1.default)(sentence);
|
|
@@ -43,14 +62,23 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
43
62
|
function isVerbPhrase(name) {
|
|
44
63
|
const firstWord = extractFirstWord(name);
|
|
45
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
-
|
|
65
|
+
const firstWordLower = firstWord.toLowerCase();
|
|
66
|
+
// Check for prepositions and common verbs first
|
|
67
|
+
if (PREPOSITIONS.includes(firstWordLower) || COMMON_VERBS.has(firstWordLower)) {
|
|
47
68
|
return true;
|
|
48
69
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
// Try both with and without "I" prefix to catch more verb forms
|
|
71
|
+
const withPrefixTags = getPossibleTags(toSentence(name));
|
|
72
|
+
const withoutPrefixTags = getPossibleTags(firstWord);
|
|
73
|
+
// Check if either form is recognized as a verb
|
|
74
|
+
const isVerb = withPrefixTags.includes('Verb') || withoutPrefixTags.includes('Verb');
|
|
75
|
+
const isPreposition = withPrefixTags.includes('Preposition');
|
|
76
|
+
const isConjunction = withPrefixTags.includes('Conjunction');
|
|
77
|
+
// For non-prepositions/conjunctions, require verb form
|
|
78
|
+
if (isPreposition || isConjunction) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return isVerb;
|
|
54
82
|
}
|
|
55
83
|
function isJsxReturnFunction(node) {
|
|
56
84
|
if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
@@ -97,6 +125,12 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
97
125
|
MethodDefinition(node) {
|
|
98
126
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
99
127
|
return;
|
|
128
|
+
// Skip getters since they represent properties and should use noun phrases
|
|
129
|
+
if (node.kind === 'get')
|
|
130
|
+
return;
|
|
131
|
+
// Skip constructors since they are special class methods
|
|
132
|
+
if (node.kind === 'constructor')
|
|
133
|
+
return;
|
|
100
134
|
if (!isVerbPhrase(node.key.name)) {
|
|
101
135
|
context.report({
|
|
102
136
|
node: node.key,
|
|
@@ -27,12 +27,12 @@ exports.noAsyncArrayFilter = (0, createRule_1.createRule)({
|
|
|
27
27
|
meta: {
|
|
28
28
|
type: 'problem',
|
|
29
29
|
docs: {
|
|
30
|
-
description: 'Disallow async callbacks
|
|
30
|
+
description: 'Disallow async callbacks in Array.filter() as they lead to incorrect filtering. Since async functions return Promises which are always truthy, the filter will keep all elements regardless of the async check\'s result. Use Promise.all() with map() first, then filter based on the resolved results.',
|
|
31
31
|
recommended: 'error',
|
|
32
32
|
},
|
|
33
33
|
schema: [],
|
|
34
34
|
messages: {
|
|
35
|
-
unexpected: 'Async array filter is dangerous as a Promise object will always be truthy.
|
|
35
|
+
unexpected: 'Async array filter is dangerous as a Promise object will always be truthy. Instead of `array.filter(async x => await someCheck(x))`, first resolve the promises with `Promise.all()` or move the async logic elsewhere: `const results = await Promise.all(array.map(x => someCheck(x))); array.filter((_, i) => results[i])`.',
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
defaultOptions: [],
|
|
@@ -24,7 +24,7 @@ exports.noAsyncForEach = {
|
|
|
24
24
|
meta: {
|
|
25
25
|
type: 'problem',
|
|
26
26
|
docs: {
|
|
27
|
-
description: 'Disallow Array.forEach with an async callback function',
|
|
27
|
+
description: 'Disallow Array.forEach with an async callback function as it does not wait for promises to resolve. This can lead to race conditions and unexpected behavior. Use a standard for...of loop for sequential execution or Promise.all with map for concurrent execution.',
|
|
28
28
|
recommended: 'error',
|
|
29
29
|
},
|
|
30
30
|
messages: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noClassInstanceDestructuring: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noClassInstanceDestructuring", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noClassInstanceDestructuring = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'no-class-instance-destructuring',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Disallow destructuring of class instances to prevent loss of `this` context',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
},
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
noClassInstanceDestructuring: 'Avoid destructuring class instances as it can lead to loss of `this` context. Use direct property access instead.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
function isClassInstance(node) {
|
|
23
|
+
// Check for new expressions
|
|
24
|
+
if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// Check for identifiers that might be class instances
|
|
28
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
29
|
+
const variableDef = context
|
|
30
|
+
.getScope()
|
|
31
|
+
.variables.find((variableDef) => variableDef.name === node.name);
|
|
32
|
+
if (variableDef?.defs[0]?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
33
|
+
const init = variableDef.defs[0].node
|
|
34
|
+
.init;
|
|
35
|
+
return init?.type === utils_1.AST_NODE_TYPES.NewExpression;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
VariableDeclarator(node) {
|
|
42
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
43
|
+
node.init &&
|
|
44
|
+
isClassInstance(node.init)) {
|
|
45
|
+
const objectPattern = node.id;
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: 'noClassInstanceDestructuring',
|
|
49
|
+
fix(fixer) {
|
|
50
|
+
const sourceCode = context.getSourceCode();
|
|
51
|
+
const properties = objectPattern.properties;
|
|
52
|
+
// Skip if there's no init expression
|
|
53
|
+
if (!node.init)
|
|
54
|
+
return null;
|
|
55
|
+
// For single property, use simple replacement
|
|
56
|
+
if (properties.length === 1) {
|
|
57
|
+
const prop = properties[0];
|
|
58
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
59
|
+
const key = prop.key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
60
|
+
? prop.key.name
|
|
61
|
+
: sourceCode.getText(prop.key);
|
|
62
|
+
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
63
|
+
? prop.value.name
|
|
64
|
+
: sourceCode.getText(prop.value);
|
|
65
|
+
const initText = sourceCode.getText(node.init);
|
|
66
|
+
return fixer.replaceText(node, `${value} = ${initText}.${key}`);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// For multiple properties, create multiple declarations
|
|
71
|
+
const declarations = properties
|
|
72
|
+
.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
|
|
73
|
+
.map((prop) => {
|
|
74
|
+
const key = prop.key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
75
|
+
? prop.key.name
|
|
76
|
+
: sourceCode.getText(prop.key);
|
|
77
|
+
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
78
|
+
? prop.value.name
|
|
79
|
+
: sourceCode.getText(prop.value);
|
|
80
|
+
const initText = sourceCode.getText(node.init);
|
|
81
|
+
return `${value} = ${initText}.${key}`;
|
|
82
|
+
})
|
|
83
|
+
.join(';\nconst ');
|
|
84
|
+
// Only apply the fix if we have valid declarations
|
|
85
|
+
if (!declarations)
|
|
86
|
+
return null;
|
|
87
|
+
return fixer.replaceText(node, declarations);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=no-class-instance-destructuring.js.map
|
|
@@ -37,7 +37,7 @@ exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
|
37
37
|
meta: {
|
|
38
38
|
type: 'suggestion',
|
|
39
39
|
docs: {
|
|
40
|
-
description: 'Warn when using CSS properties that trigger compositing layers',
|
|
40
|
+
description: 'Warn when using CSS properties that trigger compositing layers, which can impact performance. Properties like transform, opacity, filter, and will-change create new GPU layers. While sometimes beneficial for animations, excessive layer creation can increase memory usage and hurt performance. Consider alternatives or explicitly document intentional layer promotion.',
|
|
41
41
|
recommended: 'error',
|
|
42
42
|
},
|
|
43
43
|
schema: [],
|
|
@@ -14,7 +14,7 @@ exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
|
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
unexpected: 'Conditional
|
|
17
|
+
unexpected: 'Conditional text literals must be wrapped in a container element when next to other text. Instead of `<div>text {condition && "more text"}</div>`, use `<div>text <span>{condition && "more text"}</span></div>` to prevent React hydration issues.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -136,7 +136,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
136
136
|
meta: {
|
|
137
137
|
type: 'suggestion',
|
|
138
138
|
docs: {
|
|
139
|
-
description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used. Requires TypeScript and `parserOptions.project` to be configured.',
|
|
139
|
+
description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used, as this can cause unnecessary re-renders. When a hook only uses obj.name but obj is in the deps array, any change to obj.age will trigger the hook. Use individual fields (obj.name) instead of the entire object. Requires TypeScript and `parserOptions.project` to be configured.',
|
|
140
140
|
recommended: 'error',
|
|
141
141
|
requiresTypeChecking: true,
|
|
142
142
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [
|
|
2
3
|
{
|
|
3
4
|
allowRecursiveFunctions?: boolean;
|
|
@@ -5,7 +6,8 @@ type Options = [
|
|
|
5
6
|
allowInterfaceMethodSignatures?: boolean;
|
|
6
7
|
allowAbstractMethodSignatures?: boolean;
|
|
7
8
|
allowDtsFiles?: boolean;
|
|
9
|
+
allowFirestoreFunctionFiles?: boolean;
|
|
8
10
|
}
|
|
9
11
|
];
|
|
10
|
-
export declare const noExplicitReturnType:
|
|
12
|
+
export declare const noExplicitReturnType: TSESLint.RuleModule<'noExplicitReturnType', Options>;
|
|
11
13
|
export {};
|
|
@@ -9,11 +9,14 @@ const defaultOptions = {
|
|
|
9
9
|
allowInterfaceMethodSignatures: true,
|
|
10
10
|
allowAbstractMethodSignatures: true,
|
|
11
11
|
allowDtsFiles: true,
|
|
12
|
+
allowFirestoreFunctionFiles: true,
|
|
12
13
|
};
|
|
13
14
|
function isRecursiveFunction(node) {
|
|
14
|
-
const functionName = node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const functionName = node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration
|
|
16
|
+
? node.id?.name
|
|
17
|
+
: node.type === utils_1.AST_NODE_TYPES.FunctionExpression && node.id
|
|
18
|
+
? node.id.name
|
|
19
|
+
: undefined;
|
|
17
20
|
if (!functionName || !node.body)
|
|
18
21
|
return false;
|
|
19
22
|
let hasRecursiveCall = false;
|
|
@@ -63,9 +66,9 @@ function isOverloadedFunction(node) {
|
|
|
63
66
|
const methodName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : undefined;
|
|
64
67
|
if (!methodName)
|
|
65
68
|
return false;
|
|
66
|
-
return interfaceBody.body.filter(member => member.type === utils_1.AST_NODE_TYPES.TSMethodSignature &&
|
|
69
|
+
return (interfaceBody.body.filter((member) => member.type === utils_1.AST_NODE_TYPES.TSMethodSignature &&
|
|
67
70
|
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
68
|
-
member.key.name === methodName).length > 1;
|
|
71
|
+
member.key.name === methodName).length > 1);
|
|
69
72
|
}
|
|
70
73
|
return false;
|
|
71
74
|
}
|
|
@@ -75,7 +78,8 @@ function isInterfaceOrAbstractMethodSignature(node) {
|
|
|
75
78
|
if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
76
79
|
let current = node;
|
|
77
80
|
while (current) {
|
|
78
|
-
if (current.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
81
|
+
if (current.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
82
|
+
current.abstract) {
|
|
79
83
|
return true;
|
|
80
84
|
}
|
|
81
85
|
current = current.parent;
|
|
@@ -88,8 +92,10 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
88
92
|
meta: {
|
|
89
93
|
type: 'suggestion',
|
|
90
94
|
docs: {
|
|
91
|
-
description: 'Disallow explicit return
|
|
95
|
+
description: 'Disallow explicit return type annotations on functions when TypeScript can infer them. This reduces code verbosity and maintenance burden while leveraging TypeScript\'s powerful type inference. Exceptions are made for recursive functions, overloaded functions, interface methods, and abstract methods where explicit types improve clarity.',
|
|
92
96
|
recommended: 'error',
|
|
97
|
+
requiresTypeChecking: false,
|
|
98
|
+
extendsBaseRule: false,
|
|
93
99
|
},
|
|
94
100
|
fixable: 'code',
|
|
95
101
|
schema: [
|
|
@@ -101,6 +107,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
101
107
|
allowInterfaceMethodSignatures: { type: 'boolean' },
|
|
102
108
|
allowAbstractMethodSignatures: { type: 'boolean' },
|
|
103
109
|
allowDtsFiles: { type: 'boolean' },
|
|
110
|
+
allowFirestoreFunctionFiles: { type: 'boolean' },
|
|
104
111
|
},
|
|
105
112
|
additionalProperties: false,
|
|
106
113
|
},
|
|
@@ -113,9 +120,11 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
113
120
|
create(context, [options]) {
|
|
114
121
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
115
122
|
const filename = context.getFilename();
|
|
116
|
-
if (mergedOptions.allowDtsFiles && filename.endsWith('.d.ts'))
|
|
123
|
+
if ((mergedOptions.allowDtsFiles && filename.endsWith('.d.ts')) ||
|
|
124
|
+
(mergedOptions.allowFirestoreFunctionFiles && filename.endsWith('.f.ts'))) {
|
|
117
125
|
return {};
|
|
118
126
|
}
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
128
|
function fixReturnType(fixer, node) {
|
|
120
129
|
const returnType = node.returnType || node.value?.returnType;
|
|
121
130
|
if (!returnType)
|
|
@@ -127,25 +136,27 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
127
136
|
FunctionDeclaration(node) {
|
|
128
137
|
if (!node.returnType)
|
|
129
138
|
return;
|
|
130
|
-
if (mergedOptions.allowRecursiveFunctions &&
|
|
139
|
+
if (mergedOptions.allowRecursiveFunctions &&
|
|
140
|
+
isRecursiveFunction(node)) {
|
|
131
141
|
return;
|
|
132
142
|
}
|
|
133
143
|
context.report({
|
|
134
144
|
node: node.returnType,
|
|
135
145
|
messageId: 'noExplicitReturnType',
|
|
136
|
-
fix: fixer => fixReturnType(fixer, node),
|
|
146
|
+
fix: (fixer) => fixReturnType(fixer, node),
|
|
137
147
|
});
|
|
138
148
|
},
|
|
139
149
|
FunctionExpression(node) {
|
|
140
150
|
if (!node.returnType)
|
|
141
151
|
return;
|
|
142
|
-
if (mergedOptions.allowRecursiveFunctions &&
|
|
152
|
+
if (mergedOptions.allowRecursiveFunctions &&
|
|
153
|
+
isRecursiveFunction(node)) {
|
|
143
154
|
return;
|
|
144
155
|
}
|
|
145
156
|
context.report({
|
|
146
157
|
node: node.returnType,
|
|
147
158
|
messageId: 'noExplicitReturnType',
|
|
148
|
-
fix: fixer => fixReturnType(fixer, node),
|
|
159
|
+
fix: (fixer) => fixReturnType(fixer, node),
|
|
149
160
|
});
|
|
150
161
|
},
|
|
151
162
|
ArrowFunctionExpression(node) {
|
|
@@ -154,7 +165,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
154
165
|
context.report({
|
|
155
166
|
node: node.returnType,
|
|
156
167
|
messageId: 'noExplicitReturnType',
|
|
157
|
-
fix: fixer => fixReturnType(fixer, node),
|
|
168
|
+
fix: (fixer) => fixReturnType(fixer, node),
|
|
158
169
|
});
|
|
159
170
|
},
|
|
160
171
|
TSMethodSignature(node) {
|
|
@@ -163,25 +174,27 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
163
174
|
if (mergedOptions.allowInterfaceMethodSignatures) {
|
|
164
175
|
return;
|
|
165
176
|
}
|
|
166
|
-
if (mergedOptions.allowOverloadedFunctions &&
|
|
177
|
+
if (mergedOptions.allowOverloadedFunctions &&
|
|
178
|
+
isOverloadedFunction(node)) {
|
|
167
179
|
return;
|
|
168
180
|
}
|
|
169
181
|
context.report({
|
|
170
182
|
node: node.returnType,
|
|
171
183
|
messageId: 'noExplicitReturnType',
|
|
172
|
-
fix: fixer => fixReturnType(fixer, node),
|
|
184
|
+
fix: (fixer) => fixReturnType(fixer, node),
|
|
173
185
|
});
|
|
174
186
|
},
|
|
175
187
|
MethodDefinition(node) {
|
|
176
188
|
if (!node.value.returnType)
|
|
177
189
|
return;
|
|
178
|
-
if (mergedOptions.allowAbstractMethodSignatures &&
|
|
190
|
+
if (mergedOptions.allowAbstractMethodSignatures &&
|
|
191
|
+
isInterfaceOrAbstractMethodSignature(node)) {
|
|
179
192
|
return;
|
|
180
193
|
}
|
|
181
194
|
context.report({
|
|
182
195
|
node: node.value.returnType,
|
|
183
196
|
messageId: 'noExplicitReturnType',
|
|
184
|
-
fix: fixer => fixReturnType(fixer, node),
|
|
197
|
+
fix: (fixer) => fixReturnType(fixer, node),
|
|
185
198
|
});
|
|
186
199
|
},
|
|
187
200
|
};
|
|
@@ -33,7 +33,7 @@ exports.noFilterWithoutReturn = (0, createRule_1.createRule)({
|
|
|
33
33
|
},
|
|
34
34
|
schema: [],
|
|
35
35
|
messages: {
|
|
36
|
-
unexpected: '
|
|
36
|
+
unexpected: 'Array.filter callbacks with block statements must contain a return statement. Instead of `array.filter(x => { doSomething(x); })`, use `array.filter(x => { doSomething(x); return someCondition; })` or use implicit return `array.filter(x => someCondition)`.',
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
defaultOptions: [],
|
|
@@ -7,12 +7,12 @@ exports.noMisusedSwitchCase = (0, createRule_1.createRule)({
|
|
|
7
7
|
meta: {
|
|
8
8
|
type: 'problem',
|
|
9
9
|
docs: {
|
|
10
|
-
description: 'Prevent misuse of logical OR in switch case statements',
|
|
10
|
+
description: 'Prevent misuse of logical OR (||) in switch case statements, which can lead to confusing and error-prone code. Instead of using OR operators in case expressions, use multiple case statements in sequence to handle multiple values. This improves code readability and follows the standard switch-case pattern.',
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
schema: [],
|
|
14
14
|
messages: {
|
|
15
|
-
noMisusedSwitchCase: 'Avoid using logical OR
|
|
15
|
+
noMisusedSwitchCase: 'Avoid using logical OR (||) in switch case statements. Instead of `case x || y:`, use cascading cases like `case x: case y:`.',
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
defaultOptions: [],
|