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.
@@ -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
- };