eslint-plugin-absolute 0.2.0 → 0.2.2

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.
@@ -17,40 +17,19 @@ type AnyFunctionNode =
17
17
  | TSESTree.ArrowFunctionExpression;
18
18
 
19
19
  export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
20
- meta: {
21
- type: "problem",
22
- docs: {
23
- description:
24
- "Disallow nested functions that return non-component, non-singular JSX to enforce one component per file"
25
- },
26
- schema: [],
27
- messages: {
28
- nestedFunctionJSX:
29
- "Nested function returning non-component, non-singular JSX detected. Extract it into its own component.",
30
- nestedArrowJSX:
31
- "Nested arrow function returning non-component, non-singular JSX detected. Extract it into its own component.",
32
- nestedArrowFragment:
33
- "Nested arrow function returning a non-singular JSX fragment detected. Extract it into its own component."
34
- }
35
- },
36
-
37
- defaultOptions: [],
38
-
39
20
  create(context) {
40
21
  // Returns true if the node is a JSX element or fragment.
41
- function isJSX(
22
+ const isJSX = (
42
23
  node: TSESTree.Node | null | undefined
43
- ): node is TSESTree.JSXElement | TSESTree.JSXFragment {
44
- return (
45
- !!node &&
46
- (node.type === AST_NODE_TYPES.JSXElement ||
47
- node.type === AST_NODE_TYPES.JSXFragment)
48
- );
49
- }
50
-
51
- function getLeftmostJSXIdentifier(
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 = (
52
31
  name: TSESTree.JSXTagNameExpression
53
- ): TSESTree.JSXIdentifier | null {
32
+ ) => {
54
33
  let current: TSESTree.JSXTagNameExpression = name;
55
34
  while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
56
35
  current = current.object;
@@ -59,10 +38,12 @@ export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
59
38
  return current;
60
39
  }
61
40
  return null;
62
- }
41
+ };
63
42
 
64
43
  // Returns true if the JSX element is a component (its tag name starts with an uppercase letter).
65
- function isJSXComponentElement(node: TSESTree.Node | null | undefined) {
44
+ const isJSXComponentElement = (
45
+ node: TSESTree.Node | null | undefined
46
+ ) => {
66
47
  if (!node || node.type !== AST_NODE_TYPES.JSXElement) {
67
48
  return false;
68
49
  }
@@ -78,13 +59,23 @@ export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
78
59
  return false;
79
60
  }
80
61
  return /^[A-Z]/.test(leftmost.name);
81
- }
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
+ };
82
73
 
83
74
  // Returns true if the returned JSX is singular.
84
75
  // For both JSXElement and JSXFragment, singular means 0 or 1 non-whitespace child.
85
- function isSingularJSXReturn(
76
+ const isSingularJSXReturn = (
86
77
  node: TSESTree.JSXElement | TSESTree.JSXFragment
87
- ) {
78
+ ) => {
88
79
  if (!isJSX(node)) return false;
89
80
 
90
81
  const children = node.children.filter((child) => {
@@ -100,69 +91,85 @@ export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
100
91
  }
101
92
 
102
93
  // Check if the returned element has exactly one child.
103
- if (children.length === 1) {
104
- const child = children[0];
105
- if (!child) {
106
- return false;
107
- }
94
+ if (children.length !== 1) {
95
+ return false;
96
+ }
108
97
 
109
- // If the singular child is also a JSX element or fragment,
110
- // ensure that it doesn't have any meaningful children.
111
- if (
112
- child.type === AST_NODE_TYPES.JSXElement ||
113
- child.type === AST_NODE_TYPES.JSXFragment
114
- ) {
115
- const innerChildren = child.children.filter(
116
- (innerChild) => {
117
- if (innerChild.type === AST_NODE_TYPES.JSXText) {
118
- return innerChild.value.trim() !== "";
119
- }
120
- return true;
121
- }
122
- );
123
- return innerChildren.length === 0;
124
- }
125
- // If it’s not a JSX element (maybe a simple expression), it's acceptable.
126
- return true;
98
+ const [child] = children;
99
+ if (!child) {
100
+ return false;
127
101
  }
128
102
 
129
- // If there is more than one meaningful child, it's not singular.
130
- return false;
131
- }
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
+ };
132
114
 
133
115
  // Stack to track nested function nodes.
134
116
  const functionStack: AnyFunctionNode[] = [];
135
- function pushFunction(node: AnyFunctionNode) {
117
+ const pushFunction = (node: AnyFunctionNode) => {
136
118
  functionStack.push(node);
137
- }
138
- function popFunction() {
119
+ };
120
+ const popFunction = () => {
139
121
  functionStack.pop();
140
- }
122
+ };
141
123
 
142
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
+ },
143
156
  "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
144
157
  node: AnyFunctionNode
145
158
  ) {
146
159
  pushFunction(node);
147
160
  },
148
- "FunctionDeclaration:exit"(_node: TSESTree.FunctionDeclaration) {
161
+ "FunctionDeclaration:exit"() {
149
162
  popFunction();
150
163
  },
151
- "FunctionExpression:exit"(_node: TSESTree.FunctionExpression) {
152
- popFunction();
153
- },
154
- "ArrowFunctionExpression:exit"(
155
- _node: TSESTree.ArrowFunctionExpression
156
- ) {
164
+ "FunctionExpression:exit"() {
157
165
  popFunction();
158
166
  },
159
-
160
167
  // For explicit return statements, report if the returned JSX is not a component and not singular.
161
168
  ReturnStatement(node: TSESTree.ReturnStatement) {
162
169
  if (functionStack.length <= 1) {
163
170
  return;
164
171
  }
165
- const argument = node.argument;
172
+ const { argument } = node;
166
173
  if (!isJSX(argument)) {
167
174
  return;
168
175
  }
@@ -171,40 +178,28 @@ export const noNestedJSXReturn: TSESLint.RuleModule<MessageIds, Options> = {
171
178
  !isSingularJSXReturn(argument)
172
179
  ) {
173
180
  context.report({
174
- node,
175
- messageId: "nestedFunctionJSX"
176
- });
177
- }
178
- },
179
-
180
- // For implicit returns in arrow functions, use the same checks.
181
- "ArrowFunctionExpression > JSXElement"(node: TSESTree.JSXElement) {
182
- if (functionStack.length <= 1) {
183
- return;
184
- }
185
- if (
186
- !isJSXComponentElement(node) &&
187
- !isSingularJSXReturn(node)
188
- ) {
189
- context.report({
190
- node,
191
- messageId: "nestedArrowJSX"
192
- });
193
- }
194
- },
195
- "ArrowFunctionExpression > JSXFragment"(
196
- node: TSESTree.JSXFragment
197
- ) {
198
- if (functionStack.length <= 1) {
199
- return;
200
- }
201
- if (!isSingularJSXReturn(node)) {
202
- context.report({
203
- node,
204
- messageId: "nestedArrowFragment"
181
+ messageId: "nestedFunctionJSX",
182
+ node
205
183
  });
206
184
  }
207
185
  }
208
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"
209
204
  }
210
205
  };
@@ -4,25 +4,10 @@ type Options = [];
4
4
  type MessageIds = "useLogicalAnd";
5
5
 
6
6
  export const noOrNoneComponent: TSESLint.RuleModule<MessageIds, Options> = {
7
- meta: {
8
- type: "suggestion",
9
- docs: {
10
- description:
11
- "Prefer using logical && operator over ternary with null/undefined for conditional JSX rendering."
12
- },
13
- schema: [],
14
- messages: {
15
- useLogicalAnd:
16
- "Prefer using the logical '&&' operator instead of a ternary with null/undefined for conditional rendering."
17
- }
18
- },
19
-
20
- defaultOptions: [],
21
-
22
7
  create(context) {
23
8
  return {
24
9
  ConditionalExpression(node: TSESTree.ConditionalExpression) {
25
- const alternate = node.alternate;
10
+ const { alternate } = node;
26
11
 
27
12
  // Check if alternate is explicitly null or undefined
28
13
  const isNullAlternate =
@@ -40,7 +25,7 @@ export const noOrNoneComponent: TSESLint.RuleModule<MessageIds, Options> = {
40
25
  }
41
26
 
42
27
  // Check if the node is within a JSX expression container.
43
- const parent = node.parent;
28
+ const { parent } = node;
44
29
  if (!parent || parent.type !== "JSXExpressionContainer") {
45
30
  return;
46
31
  }
@@ -53,12 +38,25 @@ export const noOrNoneComponent: TSESLint.RuleModule<MessageIds, Options> = {
53
38
  containerParent.type !== "JSXAttribute"
54
39
  ) {
55
40
  context.report({
56
- node,
57
- messageId: "useLogicalAnd"
41
+ messageId: "useLogicalAnd",
42
+ node
58
43
  });
59
44
  }
60
45
  }
61
46
  };
47
+ },
48
+ defaultOptions: [],
49
+ meta: {
50
+ docs: {
51
+ description:
52
+ "Prefer using logical && operator over ternary with null/undefined for conditional JSX rendering."
53
+ },
54
+ messages: {
55
+ useLogicalAnd:
56
+ "Prefer using the logical '&&' operator instead of a ternary with null/undefined for conditional rendering."
57
+ },
58
+ schema: [],
59
+ type: "suggestion"
62
60
  }
63
61
  };
64
62
 
@@ -13,27 +13,62 @@ import { TSESLint, TSESTree } from "@typescript-eslint/utils";
13
13
  type Options = [];
14
14
  type MessageIds = "forbiddenTransition";
15
15
 
16
+ const getKeyName = (prop: TSESTree.Property) => {
17
+ if (prop.key.type === "Identifier") {
18
+ return prop.key.name;
19
+ }
20
+ if (prop.key.type !== "Literal") {
21
+ return null;
22
+ }
23
+ return typeof prop.key.value === "string"
24
+ ? prop.key.value
25
+ : String(prop.key.value);
26
+ };
27
+
28
+ const checkPropForTransition = (
29
+ context: TSESLint.RuleContext<MessageIds, Options>,
30
+ prop: TSESTree.Property
31
+ ) => {
32
+ if (prop.computed) {
33
+ return;
34
+ }
35
+ const keyName = getKeyName(prop);
36
+ if (keyName === "transition") {
37
+ context.report({
38
+ messageId: "forbiddenTransition",
39
+ node: prop
40
+ });
41
+ }
42
+ };
43
+
16
44
  export const noTransitionCSSProperties: TSESLint.RuleModule<
17
45
  MessageIds,
18
46
  Options
19
47
  > = {
20
- meta: {
21
- type: "problem",
22
- docs: {
23
- description:
24
- "Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring."
25
- },
26
- schema: [], // no options
27
- messages: {
28
- forbiddenTransition:
29
- "Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring."
30
- }
31
- },
48
+ create(context) {
49
+ const { sourceCode } = context;
32
50
 
33
- defaultOptions: [],
51
+ const isCSSPropertiesType = (typeAnnotation: TSESTree.TypeNode) => {
52
+ if (typeAnnotation.type !== "TSTypeReference") {
53
+ return false;
54
+ }
34
55
 
35
- create(context) {
36
- const sourceCode = context.sourceCode;
56
+ const { typeName } = typeAnnotation;
57
+
58
+ if (
59
+ typeName.type === "Identifier" &&
60
+ typeName.name === "CSSProperties"
61
+ ) {
62
+ return true;
63
+ }
64
+
65
+ return (
66
+ typeName.type === "TSQualifiedName" &&
67
+ typeName.right &&
68
+ typeName.right.type === "Identifier" &&
69
+ typeName.right.name === "CSSProperties"
70
+ );
71
+ };
37
72
 
38
73
  return {
39
74
  VariableDeclarator(node: TSESTree.VariableDeclarator) {
@@ -46,39 +81,17 @@ export const noTransitionCSSProperties: TSESLint.RuleModule<
46
81
  return;
47
82
  }
48
83
 
49
- let isStyleType = false;
50
- const typeAnnotation = node.id.typeAnnotation.typeAnnotation;
84
+ const { typeAnnotation } = node.id.typeAnnotation;
51
85
 
52
- // First try: check if it's a TSTypeReference with typeName "CSSProperties"
53
- if (
54
- typeAnnotation &&
55
- typeAnnotation.type === "TSTypeReference"
56
- ) {
57
- const typeName = typeAnnotation.typeName;
58
-
59
- if (
60
- typeName.type === "Identifier" &&
61
- typeName.name === "CSSProperties"
62
- ) {
63
- isStyleType = true;
64
- } else if (
65
- typeName.type === "TSQualifiedName" &&
66
- typeName.right &&
67
- typeName.right.type === "Identifier" &&
68
- typeName.right.name === "CSSProperties"
69
- ) {
70
- isStyleType = true;
71
- }
72
- }
86
+ // Check if the type annotation is CSSProperties
87
+ let isStyleType = isCSSPropertiesType(typeAnnotation);
73
88
 
74
89
  // Fallback: if the AST shape doesn't match, check the raw text of the annotation.
75
90
  if (!isStyleType) {
76
91
  const annotationText = sourceCode.getText(
77
92
  node.id.typeAnnotation
78
93
  );
79
- if (annotationText.includes("CSSProperties")) {
80
- isStyleType = true;
81
- }
94
+ isStyleType = annotationText.includes("CSSProperties");
82
95
  }
83
96
 
84
97
  if (!isStyleType) {
@@ -86,40 +99,33 @@ export const noTransitionCSSProperties: TSESLint.RuleModule<
86
99
  }
87
100
 
88
101
  // Check that the initializer is an object literal.
89
- const init = node.init;
102
+ const { init } = node;
90
103
  if (!init || init.type !== "ObjectExpression") {
91
104
  return;
92
105
  }
93
106
 
94
- for (const prop of init.properties) {
95
- // Only consider regular properties.
96
- if (prop.type !== "Property") {
97
- continue;
98
- }
99
- if (prop.computed) {
100
- continue;
101
- }
102
-
103
- let keyName: string | null = null;
104
-
105
- if (prop.key.type === "Identifier") {
106
- keyName = prop.key.name;
107
- } else if (prop.key.type === "Literal") {
108
- if (typeof prop.key.value === "string") {
109
- keyName = prop.key.value;
110
- } else {
111
- keyName = String(prop.key.value);
112
- }
113
- }
114
-
115
- if (keyName === "transition") {
116
- context.report({
117
- node: prop,
118
- messageId: "forbiddenTransition"
119
- });
120
- }
121
- }
107
+ const properties = init.properties.filter(
108
+ (prop): prop is TSESTree.Property =>
109
+ prop.type === "Property"
110
+ );
111
+
112
+ properties.forEach((prop) => {
113
+ checkPropForTransition(context, prop);
114
+ });
122
115
  }
123
116
  };
117
+ },
118
+ defaultOptions: [],
119
+ meta: {
120
+ docs: {
121
+ description:
122
+ "Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring."
123
+ },
124
+ messages: {
125
+ forbiddenTransition:
126
+ "Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring."
127
+ },
128
+ schema: [], // no options,
129
+ type: "problem"
124
130
  }
125
131
  };
@@ -4,45 +4,24 @@ type Options = [];
4
4
  type MessageIds = "unnecessaryDivWrapper";
5
5
 
6
6
  export const noUnnecessaryDiv: TSESLint.RuleModule<MessageIds, Options> = {
7
- meta: {
8
- type: "suggestion",
9
- docs: {
10
- description:
11
- "Flag unnecessary <div> wrappers that enclose a single JSX element. Remove the wrapper if it doesn't add semantic or functional value, or replace it with a semantic element if wrapping is needed."
12
- },
13
- schema: [],
14
- messages: {
15
- unnecessaryDivWrapper:
16
- "Unnecessary <div> wrapper detected. Remove it if not needed, or replace with a semantic element that reflects its purpose."
17
- }
18
- },
19
-
20
- defaultOptions: [],
21
-
22
7
  create(context) {
23
- function isDivElement(node: TSESTree.JSXElement) {
8
+ const isDivElement = (node: TSESTree.JSXElement) => {
24
9
  const nameNode = node.openingElement.name;
25
10
  return (
26
11
  nameNode.type === AST_NODE_TYPES.JSXIdentifier &&
27
12
  nameNode.name === "div"
28
13
  );
29
- }
14
+ };
30
15
 
31
- function getMeaningfulChildren(
32
- node: TSESTree.JSXElement
33
- ): TSESTree.JSXChild[] {
34
- const result: TSESTree.JSXChild[] = [];
35
- for (const child of node.children) {
36
- if (child.type === AST_NODE_TYPES.JSXText) {
37
- if (child.value.trim() !== "") {
38
- result.push(child);
39
- }
40
- } else {
41
- result.push(child);
42
- }
16
+ const isMeaningfulChild = (child: TSESTree.JSXChild) => {
17
+ if (child.type === AST_NODE_TYPES.JSXText) {
18
+ return child.value.trim() !== "";
43
19
  }
44
- return result;
45
- }
20
+ return true;
21
+ };
22
+
23
+ const getMeaningfulChildren = (node: TSESTree.JSXElement) =>
24
+ node.children.filter(isMeaningfulChild);
46
25
 
47
26
  return {
48
27
  JSXElement(node: TSESTree.JSXElement) {
@@ -56,18 +35,31 @@ export const noUnnecessaryDiv: TSESLint.RuleModule<MessageIds, Options> = {
56
35
  return;
57
36
  }
58
37
 
59
- const onlyChild = meaningfulChildren[0];
38
+ const [onlyChild] = meaningfulChildren;
60
39
  if (!onlyChild) {
61
40
  return;
62
41
  }
63
42
 
64
43
  if (onlyChild.type === AST_NODE_TYPES.JSXElement) {
65
44
  context.report({
66
- node,
67
- messageId: "unnecessaryDivWrapper"
45
+ messageId: "unnecessaryDivWrapper",
46
+ node
68
47
  });
69
48
  }
70
49
  }
71
50
  };
51
+ },
52
+ defaultOptions: [],
53
+ meta: {
54
+ docs: {
55
+ description:
56
+ "Flag unnecessary <div> wrappers that enclose a single JSX element. Remove the wrapper if it doesn't add semantic or functional value, or replace it with a semantic element if wrapping is needed."
57
+ },
58
+ messages: {
59
+ unnecessaryDivWrapper:
60
+ "Unnecessary <div> wrapper detected. Remove it if not needed, or replace with a semantic element that reflects its purpose."
61
+ },
62
+ schema: [],
63
+ type: "suggestion"
72
64
  }
73
65
  };