eslint-plugin-absolute 0.2.7 → 0.2.8
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/package.json +6 -1
- package/.absolutejs/eslint.cache.json +0 -49
- package/.absolutejs/prettier.cache.json +0 -49
- package/.absolutejs/tsconfig.tsbuildinfo +0 -1
- package/.claude/settings.local.json +0 -10
- package/.codex +0 -0
- package/.prettierignore +0 -4
- package/.prettierrc.json +0 -8
- package/eslint.config.mjs +0 -107
- package/src/index.ts +0 -45
- package/src/rules/explicit-object-types.ts +0 -75
- package/src/rules/inline-style-limit.ts +0 -88
- package/src/rules/localize-react-props.ts +0 -454
- package/src/rules/max-depth-extended.ts +0 -153
- package/src/rules/max-jsx-nesting.ts +0 -59
- package/src/rules/min-var-length.ts +0 -360
- package/src/rules/no-button-navigation.ts +0 -270
- package/src/rules/no-explicit-return-types.ts +0 -83
- package/src/rules/no-inline-prop-types.ts +0 -68
- package/src/rules/no-multi-style-objects.ts +0 -80
- package/src/rules/no-nested-jsx-return.ts +0 -205
- package/src/rules/no-or-none-component.ts +0 -63
- package/src/rules/no-transition-cssproperties.ts +0 -131
- package/src/rules/no-unnecessary-div.ts +0 -65
- package/src/rules/no-unnecessary-key.ts +0 -111
- package/src/rules/no-useless-function.ts +0 -56
- package/src/rules/seperate-style-files.ts +0 -79
- package/src/rules/sort-exports.ts +0 -581
- package/src/rules/sort-keys-fixable.ts +0 -1265
- package/src/rules/spring-naming-convention.ts +0 -160
- package/tsconfig.json +0 -17
|
@@ -1,270 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
type Options = [];
|
|
4
|
-
type MessageIds = "noInlinePropTypes";
|
|
5
|
-
|
|
6
|
-
export const noInlinePropTypes: TSESLint.RuleModule<MessageIds, Options> = {
|
|
7
|
-
create(context) {
|
|
8
|
-
/**
|
|
9
|
-
* Checks the node representing a parameter to determine if it is an ObjectPattern with an inline type literal.
|
|
10
|
-
* @param {ASTNode} param The parameter node from the function declaration/expression.
|
|
11
|
-
*/
|
|
12
|
-
const checkParameter = (param: TSESTree.Parameter) => {
|
|
13
|
-
// Ensure we are dealing with a destructured object pattern with a type annotation.
|
|
14
|
-
if (
|
|
15
|
-
param.type !== "ObjectPattern" ||
|
|
16
|
-
!param.typeAnnotation ||
|
|
17
|
-
param.typeAnnotation.type !== "TSTypeAnnotation"
|
|
18
|
-
) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// The actual type annotation node (for example, { mode: string } yields a TSTypeLiteral).
|
|
23
|
-
const annotation = param.typeAnnotation.typeAnnotation;
|
|
24
|
-
// If the type is an inline object (TSTypeLiteral), we want to report it.
|
|
25
|
-
if (annotation.type === "TSTypeLiteral") {
|
|
26
|
-
context.report({
|
|
27
|
-
messageId: "noInlinePropTypes",
|
|
28
|
-
node: param
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
// Applies to FunctionDeclaration, ArrowFunctionExpression, and FunctionExpression nodes.
|
|
35
|
-
"FunctionDeclaration, ArrowFunctionExpression, FunctionExpression"(
|
|
36
|
-
node:
|
|
37
|
-
| TSESTree.FunctionDeclaration
|
|
38
|
-
| TSESTree.ArrowFunctionExpression
|
|
39
|
-
| TSESTree.FunctionExpression
|
|
40
|
-
) {
|
|
41
|
-
// It is common to define props as the first parameter.
|
|
42
|
-
if (node.params.length === 0) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const [firstParam] = node.params;
|
|
47
|
-
if (!firstParam) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
checkParameter(firstParam);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
defaultOptions: [],
|
|
56
|
-
meta: {
|
|
57
|
-
docs: {
|
|
58
|
-
description:
|
|
59
|
-
"Enforce that component prop types are not defined inline (using an object literal) but rather use a named type or interface."
|
|
60
|
-
},
|
|
61
|
-
messages: {
|
|
62
|
-
noInlinePropTypes:
|
|
63
|
-
"Inline prop type definitions are not allowed. Use a named type alias or interface instead of an inline object type."
|
|
64
|
-
},
|
|
65
|
-
schema: [],
|
|
66
|
-
type: "suggestion"
|
|
67
|
-
}
|
|
68
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @fileoverview Disallow grouping CSS style objects in a single export.
|
|
5
|
-
* Instead of exporting an object that contains multiple CSS style objects,
|
|
6
|
-
* export each style separately.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
type Options = [];
|
|
10
|
-
type MessageIds = "noMultiStyleObjects";
|
|
11
|
-
|
|
12
|
-
const getPropertyName = (prop: TSESTree.Property) => {
|
|
13
|
-
const { key } = prop;
|
|
14
|
-
if (key.type === "Identifier") {
|
|
15
|
-
return key.name;
|
|
16
|
-
}
|
|
17
|
-
if (key.type === "Literal" && typeof key.value === "string") {
|
|
18
|
-
return key.value;
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const noMultiStyleObjects: TSESLint.RuleModule<MessageIds, Options> = {
|
|
24
|
-
create(context) {
|
|
25
|
-
/**
|
|
26
|
-
* Checks if the given ObjectExpression node contains multiple properties
|
|
27
|
-
* that look like CSS style objects (i.e. property keys ending with "Style").
|
|
28
|
-
*/
|
|
29
|
-
const checkObjectExpression = (node: TSESTree.ObjectExpression) => {
|
|
30
|
-
if (!node.properties.length) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const cssStyleProperties = node.properties.filter((prop) => {
|
|
35
|
-
if (prop.type !== "Property") {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
const name = getPropertyName(prop);
|
|
39
|
-
return name !== null && name.endsWith("Style");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (cssStyleProperties.length > 1) {
|
|
43
|
-
context.report({
|
|
44
|
-
messageId: "noMultiStyleObjects",
|
|
45
|
-
node
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
// Check default exports that are object literals.
|
|
52
|
-
ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) {
|
|
53
|
-
const { declaration } = node;
|
|
54
|
-
if (declaration && declaration.type === "ObjectExpression") {
|
|
55
|
-
checkObjectExpression(declaration);
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
// Optionally, also check for object literals returned from exported functions.
|
|
59
|
-
ReturnStatement(node: TSESTree.ReturnStatement) {
|
|
60
|
-
const { argument } = node;
|
|
61
|
-
if (argument && argument.type === "ObjectExpression") {
|
|
62
|
-
checkObjectExpression(argument);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
},
|
|
67
|
-
defaultOptions: [],
|
|
68
|
-
meta: {
|
|
69
|
-
docs: {
|
|
70
|
-
description:
|
|
71
|
-
"Disallow grouping CSS style objects in a single export; export each style separately."
|
|
72
|
-
},
|
|
73
|
-
messages: {
|
|
74
|
-
noMultiStyleObjects:
|
|
75
|
-
"Do not group CSS style objects in a single export; export each style separately."
|
|
76
|
-
},
|
|
77
|
-
schema: [], // no options,
|
|
78
|
-
type: "problem"
|
|
79
|
-
}
|
|
80
|
-
};
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { TSESLint, TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @fileoverview Disallow nested functions that return non-component, non-singular JSX
|
|
5
|
-
* to enforce one component per file.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
type Options = [];
|
|
9
|
-
type MessageIds =
|
|
10
|
-
| "nestedFunctionJSX"
|
|
11
|
-
| "nestedArrowJSX"
|
|
12
|
-
| "nestedArrowFragment";
|
|
13
|
-
|
|
14
|
-
type AnyFunctionNode =
|
|
15
|
-
| TSESTree.FunctionDeclaration
|
|
16
|
-
| TSESTree.FunctionExpression
|
|
17
|
-
| TSESTree.ArrowFunctionExpression;
|
|
18
|
-
|
|
19
|
-
export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
|
|
20
|
-
create(context) {
|
|
21
|
-
// Returns true if the node is a JSX element or fragment.
|
|
22
|
-
const isJSX = (
|
|
23
|
-
node: TSESTree.Node | null | undefined
|
|
24
|
-
): node is TSESTree.JSXElement | TSESTree.JSXFragment =>
|
|
25
|
-
node !== null &&
|
|
26
|
-
node !== undefined &&
|
|
27
|
-
(node.type === AST_NODE_TYPES.JSXElement ||
|
|
28
|
-
node.type === AST_NODE_TYPES.JSXFragment);
|
|
29
|
-
|
|
30
|
-
const getLeftmostJSXIdentifier = (
|
|
31
|
-
name: TSESTree.JSXTagNameExpression
|
|
32
|
-
) => {
|
|
33
|
-
let current: TSESTree.JSXTagNameExpression = name;
|
|
34
|
-
while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
|
|
35
|
-
current = current.object;
|
|
36
|
-
}
|
|
37
|
-
if (current.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
38
|
-
return current;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Returns true if the JSX element is a component (its tag name starts with an uppercase letter).
|
|
44
|
-
const isJSXComponentElement = (
|
|
45
|
-
node: TSESTree.Node | null | undefined
|
|
46
|
-
) => {
|
|
47
|
-
if (!node || node.type !== AST_NODE_TYPES.JSXElement) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
const opening = node.openingElement;
|
|
51
|
-
const nameNode = opening.name;
|
|
52
|
-
|
|
53
|
-
if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
54
|
-
return /^[A-Z]/.test(nameNode.name);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const leftmost = getLeftmostJSXIdentifier(nameNode);
|
|
58
|
-
if (!leftmost) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
return /^[A-Z]/.test(leftmost.name);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const hasNoMeaningfulChildren = (children: TSESTree.JSXChild[]) => {
|
|
65
|
-
const filtered = children.filter((child) => {
|
|
66
|
-
if (child.type === AST_NODE_TYPES.JSXText) {
|
|
67
|
-
return child.value.trim() !== "";
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
});
|
|
71
|
-
return filtered.length === 0;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Returns true if the returned JSX is singular.
|
|
75
|
-
// For both JSXElement and JSXFragment, singular means 0 or 1 non-whitespace child.
|
|
76
|
-
const isSingularJSXReturn = (
|
|
77
|
-
node: TSESTree.JSXElement | TSESTree.JSXFragment
|
|
78
|
-
) => {
|
|
79
|
-
if (!isJSX(node)) return false;
|
|
80
|
-
|
|
81
|
-
const children = node.children.filter((child) => {
|
|
82
|
-
if (child.type === AST_NODE_TYPES.JSXText) {
|
|
83
|
-
return child.value.trim() !== "";
|
|
84
|
-
}
|
|
85
|
-
return true;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// If there are no children, it's singular.
|
|
89
|
-
if (children.length === 0) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check if the returned element has exactly one child.
|
|
94
|
-
if (children.length !== 1) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const [child] = children;
|
|
99
|
-
if (!child) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// If the singular child is also a JSX element or fragment,
|
|
104
|
-
// ensure that it doesn't have any meaningful children.
|
|
105
|
-
if (
|
|
106
|
-
child.type === AST_NODE_TYPES.JSXElement ||
|
|
107
|
-
child.type === AST_NODE_TYPES.JSXFragment
|
|
108
|
-
) {
|
|
109
|
-
return hasNoMeaningfulChildren(child.children);
|
|
110
|
-
}
|
|
111
|
-
// If it's not a JSX element (maybe a simple expression), it's acceptable.
|
|
112
|
-
return true;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Stack to track nested function nodes.
|
|
116
|
-
const functionStack: AnyFunctionNode[] = [];
|
|
117
|
-
const pushFunction = (node: AnyFunctionNode) => {
|
|
118
|
-
functionStack.push(node);
|
|
119
|
-
};
|
|
120
|
-
const popFunction = () => {
|
|
121
|
-
functionStack.pop();
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
// For implicit returns in arrow functions, use the same checks.
|
|
126
|
-
"ArrowFunctionExpression > JSXElement"(node: TSESTree.JSXElement) {
|
|
127
|
-
if (functionStack.length <= 1) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (
|
|
131
|
-
!isJSXComponentElement(node) &&
|
|
132
|
-
!isSingularJSXReturn(node)
|
|
133
|
-
) {
|
|
134
|
-
context.report({
|
|
135
|
-
messageId: "nestedArrowJSX",
|
|
136
|
-
node
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
"ArrowFunctionExpression > JSXFragment"(
|
|
141
|
-
node: TSESTree.JSXFragment
|
|
142
|
-
) {
|
|
143
|
-
if (functionStack.length <= 1) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (!isSingularJSXReturn(node)) {
|
|
147
|
-
context.report({
|
|
148
|
-
messageId: "nestedArrowFragment",
|
|
149
|
-
node
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
"ArrowFunctionExpression:exit"() {
|
|
154
|
-
popFunction();
|
|
155
|
-
},
|
|
156
|
-
"FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
|
|
157
|
-
node: AnyFunctionNode
|
|
158
|
-
) {
|
|
159
|
-
pushFunction(node);
|
|
160
|
-
},
|
|
161
|
-
"FunctionDeclaration:exit"() {
|
|
162
|
-
popFunction();
|
|
163
|
-
},
|
|
164
|
-
"FunctionExpression:exit"() {
|
|
165
|
-
popFunction();
|
|
166
|
-
},
|
|
167
|
-
// For explicit return statements, report if the returned JSX is not a component and not singular.
|
|
168
|
-
ReturnStatement(node: TSESTree.ReturnStatement) {
|
|
169
|
-
if (functionStack.length <= 1) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const { argument } = node;
|
|
173
|
-
if (!isJSX(argument)) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (
|
|
177
|
-
!isJSXComponentElement(argument) &&
|
|
178
|
-
!isSingularJSXReturn(argument)
|
|
179
|
-
) {
|
|
180
|
-
context.report({
|
|
181
|
-
messageId: "nestedFunctionJSX",
|
|
182
|
-
node
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
},
|
|
188
|
-
defaultOptions: [],
|
|
189
|
-
meta: {
|
|
190
|
-
docs: {
|
|
191
|
-
description:
|
|
192
|
-
"Disallow nested functions that return non-component, non-singular JSX to enforce one component per file"
|
|
193
|
-
},
|
|
194
|
-
messages: {
|
|
195
|
-
nestedArrowFragment:
|
|
196
|
-
"Nested arrow function returning a non-singular JSX fragment detected. Extract it into its own component.",
|
|
197
|
-
nestedArrowJSX:
|
|
198
|
-
"Nested arrow function returning non-component, non-singular JSX detected. Extract it into its own component.",
|
|
199
|
-
nestedFunctionJSX:
|
|
200
|
-
"Nested function returning non-component, non-singular JSX detected. Extract it into its own component."
|
|
201
|
-
},
|
|
202
|
-
schema: [],
|
|
203
|
-
type: "problem"
|
|
204
|
-
}
|
|
205
|
-
};
|