eslint-plugin-absolute 0.1.6 → 0.2.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.
Files changed (49) hide show
  1. package/.absolutejs/eslint.cache.json +49 -0
  2. package/.absolutejs/prettier.cache.json +49 -0
  3. package/.absolutejs/tsconfig.tsbuildinfo +1 -0
  4. package/.claude/settings.local.json +10 -0
  5. package/dist/index.js +1787 -1457
  6. package/eslint.config.mjs +107 -0
  7. package/package.json +15 -12
  8. package/src/index.ts +45 -0
  9. package/src/rules/explicit-object-types.ts +75 -0
  10. package/src/rules/inline-style-limit.ts +88 -0
  11. package/src/rules/localize-react-props.ts +454 -0
  12. package/src/rules/max-depth-extended.ts +153 -0
  13. package/src/rules/{max-jsx-nesting.js → max-jsx-nesting.ts} +37 -38
  14. package/src/rules/min-var-length.ts +360 -0
  15. package/src/rules/no-button-navigation.ts +270 -0
  16. package/src/rules/no-explicit-return-types.ts +83 -0
  17. package/src/rules/no-inline-prop-types.ts +68 -0
  18. package/src/rules/no-multi-style-objects.ts +80 -0
  19. package/src/rules/no-nested-jsx-return.ts +205 -0
  20. package/src/rules/no-or-none-component.ts +63 -0
  21. package/src/rules/no-transition-cssproperties.ts +131 -0
  22. package/src/rules/no-unnecessary-div.ts +65 -0
  23. package/src/rules/no-unnecessary-key.ts +111 -0
  24. package/src/rules/no-useless-function.ts +56 -0
  25. package/src/rules/seperate-style-files.ts +79 -0
  26. package/src/rules/sort-exports.ts +424 -0
  27. package/src/rules/sort-keys-fixable.ts +647 -0
  28. package/src/rules/spring-naming-convention.ts +160 -0
  29. package/tsconfig.json +4 -1
  30. package/src/index.js +0 -45
  31. package/src/rules/explicit-object-types.js +0 -54
  32. package/src/rules/inline-style-limit.js +0 -77
  33. package/src/rules/localize-react-props.js +0 -418
  34. package/src/rules/max-depth-extended.js +0 -124
  35. package/src/rules/min-var-length.js +0 -300
  36. package/src/rules/no-button-navigation.js +0 -232
  37. package/src/rules/no-explicit-return-types.js +0 -64
  38. package/src/rules/no-inline-prop-types.js +0 -55
  39. package/src/rules/no-multi-style-objects.js +0 -70
  40. package/src/rules/no-nested-jsx-return.js +0 -154
  41. package/src/rules/no-or-none-component.js +0 -50
  42. package/src/rules/no-transition-cssproperties.js +0 -102
  43. package/src/rules/no-unnecessary-div.js +0 -40
  44. package/src/rules/no-unnecessary-key.js +0 -128
  45. package/src/rules/no-useless-function.js +0 -43
  46. package/src/rules/seperate-style-files.js +0 -62
  47. package/src/rules/sort-exports.js +0 -397
  48. package/src/rules/sort-keys-fixable.js +0 -459
  49. package/src/rules/spring-naming-convention.js +0 -111
@@ -0,0 +1,360 @@
1
+ import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
+
3
+ type MinVarLengthOption = {
4
+ minLength?: number;
5
+ allowedVars?: string[];
6
+ };
7
+
8
+ type Options = [MinVarLengthOption?];
9
+ type MessageIds = "variableNameTooShort";
10
+
11
+ /**
12
+ * Recursively extract identifier names from a pattern (for destructuring)
13
+ */
14
+ const extractIdentifiersFromPattern = (
15
+ pattern: TSESTree.Node | null,
16
+ identifiers: string[] = []
17
+ ) => {
18
+ if (!pattern) return identifiers;
19
+ switch (pattern.type) {
20
+ case "Identifier":
21
+ identifiers.push(pattern.name);
22
+ break;
23
+ case "ObjectPattern":
24
+ pattern.properties.forEach((prop) => {
25
+ if (prop.type === "Property") {
26
+ extractIdentifiersFromPattern(prop.value, identifiers);
27
+ } else if (prop.type === "RestElement") {
28
+ extractIdentifiersFromPattern(prop.argument, identifiers);
29
+ }
30
+ });
31
+ break;
32
+ case "ArrayPattern":
33
+ pattern.elements
34
+ .filter(
35
+ (element): element is TSESTree.DestructuringPattern =>
36
+ element !== null
37
+ )
38
+ .forEach((element) => {
39
+ extractIdentifiersFromPattern(element, identifiers);
40
+ });
41
+ break;
42
+ case "AssignmentPattern":
43
+ extractIdentifiersFromPattern(pattern.left, identifiers);
44
+ break;
45
+ default:
46
+ break;
47
+ }
48
+ return identifiers;
49
+ };
50
+
51
+ const getDeclaratorNames = (declarations: TSESTree.VariableDeclarator[]) =>
52
+ declarations
53
+ .filter(
54
+ (
55
+ decl
56
+ ): decl is TSESTree.VariableDeclarator & {
57
+ id: TSESTree.Identifier;
58
+ } => decl.id.type === "Identifier"
59
+ )
60
+ .map((decl) => decl.id.name);
61
+
62
+ const collectParamNames = (params: TSESTree.Parameter[]) => {
63
+ const names: string[] = [];
64
+ params.forEach((param) => {
65
+ extractIdentifiersFromPattern(param, names);
66
+ });
67
+ return names;
68
+ };
69
+
70
+ export const minVarLength: TSESLint.RuleModule<MessageIds, Options> = {
71
+ create(context) {
72
+ const { sourceCode } = context;
73
+ const [options] = context.options;
74
+ const configuredMinLength =
75
+ options && typeof options.minLength === "number"
76
+ ? options.minLength
77
+ : 1;
78
+ const configuredAllowedVars =
79
+ options && Array.isArray(options.allowedVars)
80
+ ? options.allowedVars
81
+ : [];
82
+
83
+ const minLength = configuredMinLength;
84
+ const allowedVars = configuredAllowedVars;
85
+
86
+ // Helper: walk up the node.parent chain to get ancestors.
87
+ const getAncestors = (node: TSESTree.Node) => {
88
+ const ancestors: TSESTree.Node[] = [];
89
+ let current: TSESTree.Node | null | undefined = node.parent;
90
+ while (current) {
91
+ ancestors.push(current);
92
+ current = current.parent;
93
+ }
94
+ return ancestors;
95
+ };
96
+
97
+ // Helper: retrieve the scope for a given node using the scopeManager.
98
+ const getScope = (node: TSESTree.Node) => {
99
+ const { scopeManager } = sourceCode;
100
+ if (!scopeManager) {
101
+ return null;
102
+ }
103
+ const acquired = scopeManager.acquire(node);
104
+ if (acquired) {
105
+ return acquired;
106
+ }
107
+ return scopeManager.globalScope ?? null;
108
+ };
109
+
110
+ // Fallback: get declared variable names in the nearest BlockStatement.
111
+ const getVariablesInNearestBlock = (node: TSESTree.Node) => {
112
+ let current: TSESTree.Node | null | undefined = node.parent;
113
+ while (current && current.type !== "BlockStatement") {
114
+ current = current.parent;
115
+ }
116
+ if (
117
+ !current ||
118
+ current.type !== "BlockStatement" ||
119
+ !Array.isArray(current.body)
120
+ ) {
121
+ return [];
122
+ }
123
+
124
+ const varDeclarations = current.body.filter(
125
+ (stmt): stmt is TSESTree.VariableDeclaration =>
126
+ stmt.type === "VariableDeclaration"
127
+ );
128
+ return varDeclarations.flatMap((stmt) =>
129
+ getDeclaratorNames(stmt.declarations)
130
+ );
131
+ };
132
+
133
+ const isLongerMatchInScope = (shortName: string, varName: string) =>
134
+ varName.length >= minLength &&
135
+ varName.length > shortName.length &&
136
+ varName.startsWith(shortName);
137
+
138
+ const checkScopeVariables = (
139
+ shortName: string,
140
+ node: TSESTree.Identifier
141
+ ) => {
142
+ const startingScope = getScope(node);
143
+ let outer =
144
+ startingScope && startingScope.upper
145
+ ? startingScope.upper
146
+ : null;
147
+ while (outer) {
148
+ if (
149
+ outer.variables.some((variable) =>
150
+ isLongerMatchInScope(shortName, variable.name)
151
+ )
152
+ ) {
153
+ return true;
154
+ }
155
+ outer = outer.upper;
156
+ }
157
+ return false;
158
+ };
159
+
160
+ const checkBlockVariables = (
161
+ shortName: string,
162
+ node: TSESTree.Identifier
163
+ ) => {
164
+ const blockVars = getVariablesInNearestBlock(node);
165
+ return blockVars.some((varName) =>
166
+ isLongerMatchInScope(shortName, varName)
167
+ );
168
+ };
169
+
170
+ const checkAncestorDeclarators = (
171
+ shortName: string,
172
+ node: TSESTree.Identifier
173
+ ) => {
174
+ const ancestors = getAncestors(node);
175
+ return ancestors.some(
176
+ (anc) =>
177
+ anc.type === "VariableDeclarator" &&
178
+ anc.id &&
179
+ anc.id.type === "Identifier" &&
180
+ isLongerMatchInScope(shortName, anc.id.name)
181
+ );
182
+ };
183
+
184
+ const checkFunctionAncestor = (
185
+ shortName: string,
186
+ anc:
187
+ | TSESTree.FunctionDeclaration
188
+ | TSESTree.FunctionExpression
189
+ | TSESTree.ArrowFunctionExpression
190
+ ) => {
191
+ const names = collectParamNames(anc.params);
192
+ return names.some((paramName) =>
193
+ isLongerMatchInScope(shortName, paramName)
194
+ );
195
+ };
196
+
197
+ const checkCatchAncestor = (
198
+ shortName: string,
199
+ anc: TSESTree.CatchClause
200
+ ) => {
201
+ if (!anc.param) {
202
+ return false;
203
+ }
204
+ const names = extractIdentifiersFromPattern(anc.param, []);
205
+ return names.some((paramName) =>
206
+ isLongerMatchInScope(shortName, paramName)
207
+ );
208
+ };
209
+
210
+ const checkAncestorParams = (
211
+ shortName: string,
212
+ node: TSESTree.Identifier
213
+ ) => {
214
+ const ancestors = getAncestors(node);
215
+ return ancestors.some((anc) => {
216
+ if (
217
+ anc.type === "FunctionDeclaration" ||
218
+ anc.type === "FunctionExpression" ||
219
+ anc.type === "ArrowFunctionExpression"
220
+ ) {
221
+ return checkFunctionAncestor(shortName, anc);
222
+ }
223
+ if (anc.type === "CatchClause") {
224
+ return checkCatchAncestor(shortName, anc);
225
+ }
226
+ return false;
227
+ });
228
+ };
229
+
230
+ /**
231
+ * Checks if there is an outer variable whose name is longer than the current short name
232
+ * and starts with the same characters.
233
+ */
234
+ const hasOuterCorrespondingIdentifier = (
235
+ shortName: string,
236
+ node: TSESTree.Identifier
237
+ ) =>
238
+ checkScopeVariables(shortName, node) ||
239
+ checkBlockVariables(shortName, node) ||
240
+ checkAncestorDeclarators(shortName, node) ||
241
+ checkAncestorParams(shortName, node);
242
+
243
+ /**
244
+ * Checks an Identifier node. If its name is shorter than minLength (and not in the allowed list)
245
+ * and no outer variable with a longer name starting with the short name is found, it reports an error.
246
+ */
247
+ const checkIdentifier = (node: TSESTree.Identifier) => {
248
+ const { name } = node;
249
+ if (name.length >= minLength) {
250
+ return;
251
+ }
252
+
253
+ // If the name is in the allowed list, skip.
254
+ if (allowedVars.includes(name)) {
255
+ return;
256
+ }
257
+ if (!hasOuterCorrespondingIdentifier(name, node)) {
258
+ context.report({
259
+ data: { minLength, name },
260
+ messageId: "variableNameTooShort",
261
+ node
262
+ });
263
+ }
264
+ };
265
+
266
+ /**
267
+ * Recursively checks a pattern node for identifiers.
268
+ */
269
+ const checkPattern = (pattern: TSESTree.Node | null) => {
270
+ if (!pattern) return;
271
+ switch (pattern.type) {
272
+ case "Identifier":
273
+ checkIdentifier(pattern);
274
+ break;
275
+ case "ObjectPattern":
276
+ pattern.properties.forEach((prop) => {
277
+ if (prop.type === "Property") {
278
+ checkPattern(prop.value);
279
+ } else if (prop.type === "RestElement") {
280
+ checkPattern(prop.argument);
281
+ }
282
+ });
283
+ break;
284
+ case "ArrayPattern":
285
+ pattern.elements
286
+ .filter(
287
+ (
288
+ element
289
+ ): element is TSESTree.DestructuringPattern =>
290
+ element !== null
291
+ )
292
+ .forEach((element) => {
293
+ checkPattern(element);
294
+ });
295
+ break;
296
+ case "AssignmentPattern":
297
+ checkPattern(pattern.left);
298
+ break;
299
+ default:
300
+ break;
301
+ }
302
+ };
303
+
304
+ return {
305
+ CatchClause(node: TSESTree.CatchClause) {
306
+ if (node.param) {
307
+ checkPattern(node.param);
308
+ }
309
+ },
310
+ "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
311
+ node:
312
+ | TSESTree.FunctionDeclaration
313
+ | TSESTree.FunctionExpression
314
+ | TSESTree.ArrowFunctionExpression
315
+ ) {
316
+ node.params.forEach((param) => {
317
+ checkPattern(param);
318
+ });
319
+ },
320
+ VariableDeclarator(node: TSESTree.VariableDeclarator) {
321
+ if (node.id) {
322
+ checkPattern(node.id);
323
+ }
324
+ }
325
+ };
326
+ },
327
+ defaultOptions: [{}],
328
+ meta: {
329
+ docs: {
330
+ description:
331
+ "Disallow variable names shorter than the configured minimum length unless an outer variable with a longer name starting with the same characters exists. You can exempt specific variable names using the allowedVars option."
332
+ },
333
+ messages: {
334
+ variableNameTooShort:
335
+ "Variable '{{name}}' is too short. Minimum allowed length is {{minLength}} characters unless an outer variable with a longer name starting with '{{name}}' exists."
336
+ },
337
+ schema: [
338
+ {
339
+ additionalProperties: false,
340
+ properties: {
341
+ allowedVars: {
342
+ default: [],
343
+ items: {
344
+ minLength: 1,
345
+ type: "string"
346
+ // Note: The maxLength for each string should be at most the configured minLength.
347
+ },
348
+ type: "array"
349
+ },
350
+ minLength: {
351
+ default: 1,
352
+ type: "number"
353
+ }
354
+ },
355
+ type: "object"
356
+ }
357
+ ],
358
+ type: "problem"
359
+ }
360
+ };
@@ -0,0 +1,270 @@
1
+ import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
+
3
+ type Options = [];
4
+ type MessageIds = "noButtonNavigation";
5
+
6
+ type HandlerState = {
7
+ attribute: TSESTree.JSXAttribute;
8
+ reason: string | null;
9
+ sawReplaceCall: boolean;
10
+ sawAllowedLocationRead: boolean;
11
+ };
12
+
13
+ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
14
+ create(context) {
15
+ const handlerStack: HandlerState[] = [];
16
+
17
+ const getCurrentHandler = () => {
18
+ const state = handlerStack[handlerStack.length - 1];
19
+ if (!state) {
20
+ return null;
21
+ }
22
+ return state;
23
+ };
24
+
25
+ const isOnClickButtonHandler = (
26
+ node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression
27
+ ) => {
28
+ const { parent } = node;
29
+ if (!parent || parent.type !== "JSXExpressionContainer") {
30
+ return null;
31
+ }
32
+ const attributeCandidate = parent.parent;
33
+ if (
34
+ !attributeCandidate ||
35
+ attributeCandidate.type !== "JSXAttribute"
36
+ ) {
37
+ return null;
38
+ }
39
+ const attr = attributeCandidate;
40
+ if (
41
+ !attr.name ||
42
+ attr.name.type !== "JSXIdentifier" ||
43
+ attr.name.name !== "onClick"
44
+ ) {
45
+ return null;
46
+ }
47
+ const openingElementCandidate = attr.parent;
48
+ if (
49
+ !openingElementCandidate ||
50
+ openingElementCandidate.type !== "JSXOpeningElement"
51
+ ) {
52
+ return null;
53
+ }
54
+ const tagNameNode = openingElementCandidate.name;
55
+ if (
56
+ tagNameNode.type !== "JSXIdentifier" ||
57
+ tagNameNode.name !== "button"
58
+ ) {
59
+ return null;
60
+ }
61
+ return attr;
62
+ };
63
+
64
+ const isWindowLocationMember = (member: TSESTree.MemberExpression) => {
65
+ const { object } = member;
66
+ if (object.type !== "MemberExpression") {
67
+ return false;
68
+ }
69
+ const outerObject = object.object;
70
+ const outerProperty = object.property;
71
+ return (
72
+ outerObject.type === "Identifier" &&
73
+ outerObject.name === "window" &&
74
+ outerProperty.type === "Identifier" &&
75
+ outerProperty.name === "location"
76
+ );
77
+ };
78
+
79
+ const isWindowHistoryMember = (member: TSESTree.MemberExpression) => {
80
+ const { object } = member;
81
+ if (object.type !== "MemberExpression") {
82
+ return false;
83
+ }
84
+ const outerObject = object.object;
85
+ const outerProperty = object.property;
86
+ return (
87
+ outerObject.type === "Identifier" &&
88
+ outerObject.name === "window" &&
89
+ outerProperty.type === "Identifier" &&
90
+ outerProperty.name === "history"
91
+ );
92
+ };
93
+
94
+ const reportHandlerExit = (state: HandlerState) => {
95
+ const { reason, sawReplaceCall, sawAllowedLocationRead } = state;
96
+
97
+ if (reason) {
98
+ context.report({
99
+ data: { reason },
100
+ messageId: "noButtonNavigation",
101
+ node: state.attribute
102
+ });
103
+ return;
104
+ }
105
+
106
+ if (sawReplaceCall && !sawAllowedLocationRead) {
107
+ context.report({
108
+ data: {
109
+ reason: "history.replaceState/pushState without reading window.location"
110
+ },
111
+ messageId: "noButtonNavigation",
112
+ node: state.attribute
113
+ });
114
+ }
115
+ };
116
+
117
+ return {
118
+ ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {
119
+ const attr = isOnClickButtonHandler(node);
120
+ if (!attr) {
121
+ return;
122
+ }
123
+ handlerStack.push({
124
+ attribute: attr,
125
+ reason: null,
126
+ sawAllowedLocationRead: false,
127
+ sawReplaceCall: false
128
+ });
129
+ },
130
+ "ArrowFunctionExpression:exit"(
131
+ node: TSESTree.ArrowFunctionExpression
132
+ ) {
133
+ const attr = isOnClickButtonHandler(node);
134
+ if (!attr) {
135
+ return;
136
+ }
137
+ const state = handlerStack.pop();
138
+ if (!state) {
139
+ return;
140
+ }
141
+
142
+ reportHandlerExit(state);
143
+ },
144
+ AssignmentExpression(node: TSESTree.AssignmentExpression) {
145
+ const state = getCurrentHandler();
146
+ if (!state) {
147
+ return;
148
+ }
149
+ if (node.left.type !== "MemberExpression") {
150
+ return;
151
+ }
152
+ const { left } = node;
153
+
154
+ // window.location = ...
155
+ if (
156
+ left.object.type === "Identifier" &&
157
+ left.object.name === "window" &&
158
+ left.property.type === "Identifier" &&
159
+ left.property.name === "location" &&
160
+ !state.reason
161
+ ) {
162
+ state.reason = "assignment to window.location";
163
+ return;
164
+ }
165
+
166
+ // window.location.href = ... OR window.location.pathname = ...
167
+ if (isWindowLocationMember(left) && !state.reason) {
168
+ state.reason = "assignment to window.location sub-property";
169
+ }
170
+ },
171
+ CallExpression(node: TSESTree.CallExpression) {
172
+ const state = getCurrentHandler();
173
+ if (!state) {
174
+ return;
175
+ }
176
+ const { callee } = node;
177
+
178
+ if (callee.type !== "MemberExpression") {
179
+ return;
180
+ }
181
+
182
+ // 3) window.location.replace(...)
183
+ if (
184
+ isWindowLocationMember(callee) &&
185
+ callee.property.type === "Identifier" &&
186
+ callee.property.name === "replace" &&
187
+ !state.reason
188
+ ) {
189
+ state.reason = "window.location.replace";
190
+ return;
191
+ }
192
+
193
+ // 4) window.history.pushState(...) or replaceState(...)
194
+ if (
195
+ isWindowHistoryMember(callee) &&
196
+ callee.property.type === "Identifier" &&
197
+ (callee.property.name === "pushState" ||
198
+ callee.property.name === "replaceState")
199
+ ) {
200
+ state.sawReplaceCall = true;
201
+ }
202
+ },
203
+ FunctionExpression(node: TSESTree.FunctionExpression) {
204
+ const attr = isOnClickButtonHandler(node);
205
+ if (!attr) {
206
+ return;
207
+ }
208
+ handlerStack.push({
209
+ attribute: attr,
210
+ reason: null,
211
+ sawAllowedLocationRead: false,
212
+ sawReplaceCall: false
213
+ });
214
+ },
215
+ "FunctionExpression:exit"(node: TSESTree.FunctionExpression) {
216
+ const attr = isOnClickButtonHandler(node);
217
+ if (!attr) {
218
+ return;
219
+ }
220
+ const state = handlerStack.pop();
221
+ if (!state) {
222
+ return;
223
+ }
224
+
225
+ reportHandlerExit(state);
226
+ },
227
+ MemberExpression(node: TSESTree.MemberExpression) {
228
+ const state = getCurrentHandler();
229
+ if (!state) {
230
+ return;
231
+ }
232
+
233
+ // 1) window.open(...)
234
+ if (
235
+ node.object.type === "Identifier" &&
236
+ node.object.name === "window" &&
237
+ node.property.type === "Identifier" &&
238
+ node.property.name === "open" &&
239
+ !state.reason
240
+ ) {
241
+ state.reason = "window.open";
242
+ }
243
+
244
+ // 5) Reading window.location.search, .pathname, or .hash
245
+ if (
246
+ isWindowLocationMember(node) &&
247
+ node.property.type === "Identifier" &&
248
+ (node.property.name === "search" ||
249
+ node.property.name === "pathname" ||
250
+ node.property.name === "hash")
251
+ ) {
252
+ state.sawAllowedLocationRead = true;
253
+ }
254
+ }
255
+ };
256
+ },
257
+ defaultOptions: [],
258
+ meta: {
259
+ docs: {
260
+ description:
261
+ "Enforce using anchor tags for navigation instead of buttons whose onClick handlers change the path. Allow only query/hash updates via window.location.search or history.replaceState(window.location.pathname + …)."
262
+ },
263
+ messages: {
264
+ noButtonNavigation:
265
+ "Use an anchor tag for navigation instead of a button whose onClick handler changes the path. Detected: {{reason}}. Only query/hash updates (reading window.location.search, .pathname, or .hash) are allowed."
266
+ },
267
+ schema: [],
268
+ type: "suggestion"
269
+ }
270
+ };
@@ -0,0 +1,83 @@
1
+ import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
+
3
+ type Options = [];
4
+ type MessageIds = "noExplicitReturnType";
5
+
6
+ type AnyFunctionNode =
7
+ | TSESTree.FunctionDeclaration
8
+ | TSESTree.FunctionExpression
9
+ | TSESTree.ArrowFunctionExpression;
10
+
11
+ export const noExplicitReturnTypes: TSESLint.RuleModule<MessageIds, Options> = {
12
+ create(context) {
13
+ const hasSingleObjectReturn = (body: TSESTree.BlockStatement) => {
14
+ const returnStatements = body.body.filter(
15
+ (stmt) => stmt.type === "ReturnStatement"
16
+ );
17
+
18
+ if (returnStatements.length !== 1) {
19
+ return false;
20
+ }
21
+
22
+ const [returnStmt] = returnStatements;
23
+ return returnStmt?.argument?.type === "ObjectExpression";
24
+ };
25
+
26
+ return {
27
+ "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
28
+ node: AnyFunctionNode
29
+ ) {
30
+ const { returnType } = node;
31
+ if (!returnType) {
32
+ return;
33
+ }
34
+
35
+ // Allow type predicate annotations for type guards.
36
+ const { typeAnnotation } = returnType;
37
+ if (
38
+ typeAnnotation &&
39
+ typeAnnotation.type === "TSTypePredicate"
40
+ ) {
41
+ return;
42
+ }
43
+
44
+ // Allow if it's an arrow function that directly returns an object literal.
45
+ if (
46
+ node.type === "ArrowFunctionExpression" &&
47
+ node.expression === true &&
48
+ node.body.type === "ObjectExpression"
49
+ ) {
50
+ return;
51
+ }
52
+
53
+ // Allow if the function has a block body with a single return statement that returns an object literal.
54
+ if (
55
+ node.body &&
56
+ node.body.type === "BlockStatement" &&
57
+ hasSingleObjectReturn(node.body)
58
+ ) {
59
+ return;
60
+ }
61
+
62
+ // Otherwise, report an error.
63
+ context.report({
64
+ messageId: "noExplicitReturnType",
65
+ node: returnType
66
+ });
67
+ }
68
+ };
69
+ },
70
+ defaultOptions: [],
71
+ meta: {
72
+ docs: {
73
+ description:
74
+ "Disallow explicit return type annotations on functions, except when using type predicates for type guards or inline object literal returns (e.g., style objects)."
75
+ },
76
+ messages: {
77
+ noExplicitReturnType:
78
+ "Explicit return types are disallowed; rely on TypeScript's inference instead."
79
+ },
80
+ schema: [],
81
+ type: "suggestion"
82
+ }
83
+ };