eslint-plugin-absolute 0.2.0 → 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.
@@ -11,36 +11,21 @@ type HandlerState = {
11
11
  };
12
12
 
13
13
  export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
14
- meta: {
15
- type: "suggestion",
16
- docs: {
17
- description:
18
- "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 + …)."
19
- },
20
- schema: [],
21
- messages: {
22
- noButtonNavigation:
23
- "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."
24
- }
25
- },
26
-
27
- defaultOptions: [],
28
-
29
14
  create(context) {
30
15
  const handlerStack: HandlerState[] = [];
31
16
 
32
- function getCurrentHandler(): HandlerState | null {
17
+ const getCurrentHandler = () => {
33
18
  const state = handlerStack[handlerStack.length - 1];
34
19
  if (!state) {
35
20
  return null;
36
21
  }
37
22
  return state;
38
- }
23
+ };
39
24
 
40
- function isOnClickButtonHandler(
25
+ const isOnClickButtonHandler = (
41
26
  node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression
42
- ): TSESTree.JSXAttribute | null {
43
- const parent = node.parent;
27
+ ) => {
28
+ const { parent } = node;
44
29
  if (!parent || parent.type !== "JSXExpressionContainer") {
45
30
  return null;
46
31
  }
@@ -66,8 +51,7 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
66
51
  ) {
67
52
  return null;
68
53
  }
69
- const openingElement = openingElementCandidate;
70
- const tagNameNode = openingElement.name;
54
+ const tagNameNode = openingElementCandidate.name;
71
55
  if (
72
56
  tagNameNode.type !== "JSXIdentifier" ||
73
57
  tagNameNode.name !== "button"
@@ -75,47 +59,60 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
75
59
  return null;
76
60
  }
77
61
  return attr;
78
- }
62
+ };
79
63
 
80
- function isWindowLocationMember(
81
- member: TSESTree.MemberExpression
82
- ): boolean {
83
- const object = member.object;
64
+ const isWindowLocationMember = (member: TSESTree.MemberExpression) => {
65
+ const { object } = member;
84
66
  if (object.type !== "MemberExpression") {
85
67
  return false;
86
68
  }
87
69
  const outerObject = object.object;
88
70
  const outerProperty = object.property;
89
- if (
71
+ return (
90
72
  outerObject.type === "Identifier" &&
91
73
  outerObject.name === "window" &&
92
74
  outerProperty.type === "Identifier" &&
93
75
  outerProperty.name === "location"
94
- ) {
95
- return true;
96
- }
97
- return false;
98
- }
76
+ );
77
+ };
99
78
 
100
- function isWindowHistoryMember(
101
- member: TSESTree.MemberExpression
102
- ): boolean {
103
- const object = member.object;
79
+ const isWindowHistoryMember = (member: TSESTree.MemberExpression) => {
80
+ const { object } = member;
104
81
  if (object.type !== "MemberExpression") {
105
82
  return false;
106
83
  }
107
84
  const outerObject = object.object;
108
85
  const outerProperty = object.property;
109
- if (
86
+ return (
110
87
  outerObject.type === "Identifier" &&
111
88
  outerObject.name === "window" &&
112
89
  outerProperty.type === "Identifier" &&
113
90
  outerProperty.name === "history"
114
- ) {
115
- return true;
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
+ });
116
114
  }
117
- return false;
118
- }
115
+ };
119
116
 
120
117
  return {
121
118
  ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {
@@ -126,8 +123,8 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
126
123
  handlerStack.push({
127
124
  attribute: attr,
128
125
  reason: null,
129
- sawReplaceCall: false,
130
- sawAllowedLocationRead: false
126
+ sawAllowedLocationRead: false,
127
+ sawReplaceCall: false
131
128
  });
132
129
  },
133
130
  "ArrowFunctionExpression:exit"(
@@ -142,30 +139,67 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
142
139
  return;
143
140
  }
144
141
 
145
- const reason = state.reason;
146
- const sawReplaceCall = state.sawReplaceCall;
147
- const sawAllowedLocationRead = state.sawAllowedLocationRead;
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;
148
153
 
149
- if (reason) {
150
- context.report({
151
- node: state.attribute,
152
- messageId: "noButtonNavigation",
153
- data: { reason }
154
- });
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";
155
163
  return;
156
164
  }
157
165
 
158
- if (sawReplaceCall && !sawAllowedLocationRead) {
159
- context.report({
160
- node: state.attribute,
161
- messageId: "noButtonNavigation",
162
- data: {
163
- reason: "history.replaceState/pushState without reading window.location"
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";
166
169
  }
167
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
+ }
168
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
+ },
169
203
  FunctionExpression(node: TSESTree.FunctionExpression) {
170
204
  const attr = isOnClickButtonHandler(node);
171
205
  if (!attr) {
@@ -174,8 +208,8 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
174
208
  handlerStack.push({
175
209
  attribute: attr,
176
210
  reason: null,
177
- sawReplaceCall: false,
178
- sawAllowedLocationRead: false
211
+ sawAllowedLocationRead: false,
212
+ sawReplaceCall: false
179
213
  });
180
214
  },
181
215
  "FunctionExpression:exit"(node: TSESTree.FunctionExpression) {
@@ -188,30 +222,8 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
188
222
  return;
189
223
  }
190
224
 
191
- const reason = state.reason;
192
- const sawReplaceCall = state.sawReplaceCall;
193
- const sawAllowedLocationRead = state.sawAllowedLocationRead;
194
-
195
- if (reason) {
196
- context.report({
197
- node: state.attribute,
198
- messageId: "noButtonNavigation",
199
- data: { reason }
200
- });
201
- return;
202
- }
203
-
204
- if (sawReplaceCall && !sawAllowedLocationRead) {
205
- context.report({
206
- node: state.attribute,
207
- messageId: "noButtonNavigation",
208
- data: {
209
- reason: "history.replaceState/pushState without reading window.location"
210
- }
211
- });
212
- }
225
+ reportHandlerExit(state);
213
226
  },
214
-
215
227
  MemberExpression(node: TSESTree.MemberExpression) {
216
228
  const state = getCurrentHandler();
217
229
  if (!state) {
@@ -223,11 +235,10 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
223
235
  node.object.type === "Identifier" &&
224
236
  node.object.name === "window" &&
225
237
  node.property.type === "Identifier" &&
226
- node.property.name === "open"
238
+ node.property.name === "open" &&
239
+ !state.reason
227
240
  ) {
228
- if (!state.reason) {
229
- state.reason = "window.open";
230
- }
241
+ state.reason = "window.open";
231
242
  }
232
243
 
233
244
  // 5) Reading window.location.search, .pathname, or .hash
@@ -240,73 +251,20 @@ export const noButtonNavigation: TSESLint.RuleModule<MessageIds, Options> = {
240
251
  ) {
241
252
  state.sawAllowedLocationRead = true;
242
253
  }
243
- },
244
-
245
- AssignmentExpression(node: TSESTree.AssignmentExpression) {
246
- const state = getCurrentHandler();
247
- if (!state) {
248
- return;
249
- }
250
- if (node.left.type !== "MemberExpression") {
251
- return;
252
- }
253
- const left = node.left;
254
-
255
- // window.location = ...
256
- if (
257
- left.object.type === "Identifier" &&
258
- left.object.name === "window" &&
259
- left.property.type === "Identifier" &&
260
- left.property.name === "location"
261
- ) {
262
- if (!state.reason) {
263
- state.reason = "assignment to window.location";
264
- }
265
- return;
266
- }
267
-
268
- // window.location.href = ... OR window.location.pathname = ...
269
- if (isWindowLocationMember(left)) {
270
- if (!state.reason) {
271
- state.reason =
272
- "assignment to window.location sub-property";
273
- }
274
- }
275
- },
276
-
277
- CallExpression(node: TSESTree.CallExpression) {
278
- const state = getCurrentHandler();
279
- if (!state) {
280
- return;
281
- }
282
- const callee = node.callee;
283
-
284
- if (callee.type !== "MemberExpression") {
285
- return;
286
- }
287
-
288
- // 3) window.location.replace(...)
289
- if (
290
- isWindowLocationMember(callee) &&
291
- callee.property.type === "Identifier" &&
292
- callee.property.name === "replace"
293
- ) {
294
- if (!state.reason) {
295
- state.reason = "window.location.replace";
296
- }
297
- return;
298
- }
299
-
300
- // 4) window.history.pushState(...) or replaceState(...)
301
- if (
302
- isWindowHistoryMember(callee) &&
303
- callee.property.type === "Identifier" &&
304
- (callee.property.name === "pushState" ||
305
- callee.property.name === "replaceState")
306
- ) {
307
- state.sawReplaceCall = true;
308
- }
309
254
  }
310
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"
311
269
  }
312
270
  };
@@ -9,50 +9,31 @@ type AnyFunctionNode =
9
9
  | TSESTree.ArrowFunctionExpression;
10
10
 
11
11
  export const noExplicitReturnTypes: TSESLint.RuleModule<MessageIds, Options> = {
12
- meta: {
13
- type: "suggestion",
14
- docs: {
15
- description:
16
- "Disallow explicit return type annotations on functions, except when using type predicates for type guards or inline object literal returns (e.g., style objects)."
17
- },
18
- schema: [],
19
- messages: {
20
- noExplicitReturnType:
21
- "Explicit return types are disallowed; rely on TypeScript's inference instead."
22
- }
23
- },
24
-
25
- defaultOptions: [],
26
-
27
12
  create(context) {
28
- function hasSingleObjectReturn(body: TSESTree.BlockStatement) {
29
- let returnCount = 0;
30
- let returnedObject: TSESTree.ObjectExpression | null = null;
13
+ const hasSingleObjectReturn = (body: TSESTree.BlockStatement) => {
14
+ const returnStatements = body.body.filter(
15
+ (stmt) => stmt.type === "ReturnStatement"
16
+ );
31
17
 
32
- for (const stmt of body.body) {
33
- if (stmt.type === "ReturnStatement") {
34
- returnCount++;
35
- const arg = stmt.argument;
36
- if (arg && arg.type === "ObjectExpression") {
37
- returnedObject = arg;
38
- }
39
- }
18
+ if (returnStatements.length !== 1) {
19
+ return false;
40
20
  }
41
21
 
42
- return returnCount === 1 && returnedObject !== null;
43
- }
22
+ const [returnStmt] = returnStatements;
23
+ return returnStmt?.argument?.type === "ObjectExpression";
24
+ };
44
25
 
45
26
  return {
46
27
  "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
47
28
  node: AnyFunctionNode
48
29
  ) {
49
- const returnType = node.returnType;
30
+ const { returnType } = node;
50
31
  if (!returnType) {
51
32
  return;
52
33
  }
53
34
 
54
35
  // Allow type predicate annotations for type guards.
55
- const typeAnnotation = returnType.typeAnnotation;
36
+ const { typeAnnotation } = returnType;
56
37
  if (
57
38
  typeAnnotation &&
58
39
  typeAnnotation.type === "TSTypePredicate"
@@ -70,18 +51,33 @@ export const noExplicitReturnTypes: TSESLint.RuleModule<MessageIds, Options> = {
70
51
  }
71
52
 
72
53
  // Allow if the function has a block body with a single return statement that returns an object literal.
73
- if (node.body && node.body.type === "BlockStatement") {
74
- if (hasSingleObjectReturn(node.body)) {
75
- return;
76
- }
54
+ if (
55
+ node.body &&
56
+ node.body.type === "BlockStatement" &&
57
+ hasSingleObjectReturn(node.body)
58
+ ) {
59
+ return;
77
60
  }
78
61
 
79
62
  // Otherwise, report an error.
80
63
  context.report({
81
- node: returnType,
82
- messageId: "noExplicitReturnType"
64
+ messageId: "noExplicitReturnType",
65
+ node: returnType
83
66
  });
84
67
  }
85
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"
86
82
  }
87
83
  };
@@ -4,44 +4,31 @@ type Options = [];
4
4
  type MessageIds = "noInlinePropTypes";
5
5
 
6
6
  export const noInlinePropTypes: TSESLint.RuleModule<MessageIds, Options> = {
7
- meta: {
8
- type: "suggestion",
9
- docs: {
10
- description:
11
- "Enforce that component prop types are not defined inline (using an object literal) but rather use a named type or interface."
12
- },
13
- schema: [],
14
- messages: {
15
- noInlinePropTypes:
16
- "Inline prop type definitions are not allowed. Use a named type alias or interface instead of an inline object type."
17
- }
18
- },
19
-
20
- defaultOptions: [],
21
-
22
7
  create(context) {
23
8
  /**
24
9
  * Checks the node representing a parameter to determine if it is an ObjectPattern with an inline type literal.
25
10
  * @param {ASTNode} param The parameter node from the function declaration/expression.
26
11
  */
27
- function checkParameter(param: TSESTree.Parameter) {
12
+ const checkParameter = (param: TSESTree.Parameter) => {
28
13
  // Ensure we are dealing with a destructured object pattern with a type annotation.
29
14
  if (
30
- param.type === "ObjectPattern" &&
31
- param.typeAnnotation &&
32
- param.typeAnnotation.type === "TSTypeAnnotation"
15
+ param.type !== "ObjectPattern" ||
16
+ !param.typeAnnotation ||
17
+ param.typeAnnotation.type !== "TSTypeAnnotation"
33
18
  ) {
34
- // The actual type annotation node (for example, { mode: string } yields a TSTypeLiteral).
35
- const annotation = param.typeAnnotation.typeAnnotation;
36
- // If the type is an inline object (TSTypeLiteral), we want to report it.
37
- if (annotation.type === "TSTypeLiteral") {
38
- context.report({
39
- node: param,
40
- messageId: "noInlinePropTypes"
41
- });
42
- }
19
+ return;
43
20
  }
44
- }
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
+ };
45
32
 
46
33
  return {
47
34
  // Applies to FunctionDeclaration, ArrowFunctionExpression, and FunctionExpression nodes.
@@ -56,7 +43,7 @@ export const noInlinePropTypes: TSESLint.RuleModule<MessageIds, Options> = {
56
43
  return;
57
44
  }
58
45
 
59
- const firstParam = node.params[0];
46
+ const [firstParam] = node.params;
60
47
  if (!firstParam) {
61
48
  return;
62
49
  }
@@ -64,5 +51,18 @@ export const noInlinePropTypes: TSESLint.RuleModule<MessageIds, Options> = {
64
51
  checkParameter(firstParam);
65
52
  }
66
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
67
  }
68
68
  };
@@ -9,79 +9,72 @@ import { TSESLint, TSESTree } from "@typescript-eslint/utils";
9
9
  type Options = [];
10
10
  type MessageIds = "noMultiStyleObjects";
11
11
 
12
- export const noMultiStyleObjects: TSESLint.RuleModule<MessageIds, Options> = {
13
- meta: {
14
- type: "problem",
15
- docs: {
16
- description:
17
- "Disallow grouping CSS style objects in a single export; export each style separately."
18
- },
19
- schema: [], // no options
20
- messages: {
21
- noMultiStyleObjects:
22
- "Do not group CSS style objects in a single export; export each style separately."
23
- }
24
- },
25
-
26
- defaultOptions: [],
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
+ };
27
22
 
23
+ export const noMultiStyleObjects: TSESLint.RuleModule<MessageIds, Options> = {
28
24
  create(context) {
29
25
  /**
30
26
  * Checks if the given ObjectExpression node contains multiple properties
31
27
  * that look like CSS style objects (i.e. property keys ending with "Style").
32
28
  */
33
- function checkObjectExpression(node: TSESTree.ObjectExpression) {
29
+ const checkObjectExpression = (node: TSESTree.ObjectExpression) => {
34
30
  if (!node.properties.length) {
35
31
  return;
36
32
  }
37
33
 
38
- const cssStyleProperties: TSESTree.Property[] = [];
39
-
40
- for (const prop of node.properties) {
34
+ const cssStyleProperties = node.properties.filter((prop) => {
41
35
  if (prop.type !== "Property") {
42
- continue;
36
+ return false;
43
37
  }
44
-
45
- const key = prop.key;
46
- let name: string | null = null;
47
-
48
- if (key.type === "Identifier") {
49
- name = key.name;
50
- } else if (
51
- key.type === "Literal" &&
52
- typeof key.value === "string"
53
- ) {
54
- name = key.value;
55
- }
56
-
57
- if (name && name.endsWith("Style")) {
58
- cssStyleProperties.push(prop);
59
- }
60
- }
38
+ const name = getPropertyName(prop);
39
+ return name !== null && name.endsWith("Style");
40
+ });
61
41
 
62
42
  if (cssStyleProperties.length > 1) {
63
43
  context.report({
64
- node,
65
- messageId: "noMultiStyleObjects"
44
+ messageId: "noMultiStyleObjects",
45
+ node
66
46
  });
67
47
  }
68
- }
48
+ };
69
49
 
70
50
  return {
71
51
  // Check default exports that are object literals.
72
52
  ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) {
73
- const declaration = node.declaration;
53
+ const { declaration } = node;
74
54
  if (declaration && declaration.type === "ObjectExpression") {
75
55
  checkObjectExpression(declaration);
76
56
  }
77
57
  },
78
58
  // Optionally, also check for object literals returned from exported functions.
79
59
  ReturnStatement(node: TSESTree.ReturnStatement) {
80
- const argument = node.argument;
60
+ const { argument } = node;
81
61
  if (argument && argument.type === "ObjectExpression") {
82
62
  checkObjectExpression(argument);
83
63
  }
84
64
  }
85
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"
86
79
  }
87
80
  };