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.
- package/.absolutejs/eslint.cache.json +49 -0
- package/.absolutejs/prettier.cache.json +49 -0
- package/.absolutejs/tsconfig.tsbuildinfo +1 -0
- package/.claude/settings.local.json +10 -0
- package/dist/index.js +1787 -1457
- package/eslint.config.mjs +107 -0
- package/package.json +15 -12
- package/src/index.ts +45 -0
- package/src/rules/explicit-object-types.ts +75 -0
- package/src/rules/inline-style-limit.ts +88 -0
- package/src/rules/localize-react-props.ts +454 -0
- package/src/rules/max-depth-extended.ts +153 -0
- package/src/rules/{max-jsx-nesting.js → max-jsx-nesting.ts} +37 -38
- package/src/rules/min-var-length.ts +360 -0
- package/src/rules/no-button-navigation.ts +270 -0
- package/src/rules/no-explicit-return-types.ts +83 -0
- package/src/rules/no-inline-prop-types.ts +68 -0
- package/src/rules/no-multi-style-objects.ts +80 -0
- package/src/rules/no-nested-jsx-return.ts +205 -0
- package/src/rules/no-or-none-component.ts +63 -0
- package/src/rules/no-transition-cssproperties.ts +131 -0
- package/src/rules/no-unnecessary-div.ts +65 -0
- package/src/rules/no-unnecessary-key.ts +111 -0
- package/src/rules/no-useless-function.ts +56 -0
- package/src/rules/seperate-style-files.ts +79 -0
- package/src/rules/sort-exports.ts +424 -0
- package/src/rules/sort-keys-fixable.ts +647 -0
- package/src/rules/spring-naming-convention.ts +160 -0
- package/tsconfig.json +4 -1
- package/src/index.js +0 -45
- package/src/rules/explicit-object-types.js +0 -54
- package/src/rules/inline-style-limit.js +0 -77
- package/src/rules/localize-react-props.js +0 -418
- package/src/rules/max-depth-extended.js +0 -124
- package/src/rules/min-var-length.js +0 -300
- package/src/rules/no-button-navigation.js +0 -232
- package/src/rules/no-explicit-return-types.js +0 -64
- package/src/rules/no-inline-prop-types.js +0 -55
- package/src/rules/no-multi-style-objects.js +0 -70
- package/src/rules/no-nested-jsx-return.js +0 -154
- package/src/rules/no-or-none-component.js +0 -50
- package/src/rules/no-transition-cssproperties.js +0 -102
- package/src/rules/no-unnecessary-div.js +0 -40
- package/src/rules/no-unnecessary-key.js +0 -128
- package/src/rules/no-useless-function.js +0 -43
- package/src/rules/seperate-style-files.js +0 -62
- package/src/rules/sort-exports.js +0 -397
- package/src/rules/sort-keys-fixable.js +0 -459
- package/src/rules/spring-naming-convention.js +0 -111
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Disallow grouping CSS style objects in a single export.
|
|
3
|
-
* Instead of exporting an object that contains multiple CSS style objects,
|
|
4
|
-
* export each style separately.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
meta: {
|
|
9
|
-
type: "problem",
|
|
10
|
-
docs: {
|
|
11
|
-
description:
|
|
12
|
-
"Disallow grouping CSS style objects in a single export; export each style separately.",
|
|
13
|
-
category: "Best Practices",
|
|
14
|
-
recommended: false
|
|
15
|
-
},
|
|
16
|
-
schema: [] // no options
|
|
17
|
-
},
|
|
18
|
-
create(context) {
|
|
19
|
-
/**
|
|
20
|
-
* Checks if the given ObjectExpression node contains multiple properties
|
|
21
|
-
* that look like CSS style objects (i.e. property keys ending with "Style").
|
|
22
|
-
*/
|
|
23
|
-
function checkObjectExpression(node) {
|
|
24
|
-
if (node.properties && node.properties.length > 0) {
|
|
25
|
-
const cssStyleProperties = node.properties.filter((prop) => {
|
|
26
|
-
if (prop.key) {
|
|
27
|
-
if (prop.key.type === "Identifier") {
|
|
28
|
-
return prop.key.name.endsWith("Style");
|
|
29
|
-
}
|
|
30
|
-
if (
|
|
31
|
-
prop.key.type === "Literal" &&
|
|
32
|
-
typeof prop.key.value === "string"
|
|
33
|
-
) {
|
|
34
|
-
return prop.key.value.endsWith("Style");
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
});
|
|
39
|
-
if (cssStyleProperties.length > 1) {
|
|
40
|
-
context.report({
|
|
41
|
-
node,
|
|
42
|
-
message:
|
|
43
|
-
"Do not group CSS style objects in a single export; export each style separately."
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
// Check default exports that are object literals.
|
|
51
|
-
ExportDefaultDeclaration(node) {
|
|
52
|
-
if (
|
|
53
|
-
node.declaration &&
|
|
54
|
-
node.declaration.type === "ObjectExpression"
|
|
55
|
-
) {
|
|
56
|
-
checkObjectExpression(node.declaration);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
// Optionally, also check for object literals returned from exported functions.
|
|
60
|
-
ReturnStatement(node) {
|
|
61
|
-
if (
|
|
62
|
-
node.argument &&
|
|
63
|
-
node.argument.type === "ObjectExpression"
|
|
64
|
-
) {
|
|
65
|
-
checkObjectExpression(node.argument);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
};
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "problem",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
"Disallow nested functions that return non-component, non-singular JSX to enforce one component per file",
|
|
7
|
-
recommended: false
|
|
8
|
-
},
|
|
9
|
-
schema: []
|
|
10
|
-
},
|
|
11
|
-
create(context) {
|
|
12
|
-
// Returns true if the node is a JSX element or fragment.
|
|
13
|
-
function isJSX(node) {
|
|
14
|
-
return (
|
|
15
|
-
node &&
|
|
16
|
-
(node.type === "JSXElement" || node.type === "JSXFragment")
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Returns true if the JSX element is a component (its tag name starts with an uppercase letter).
|
|
21
|
-
function isJSXComponentElement(node) {
|
|
22
|
-
if (node && node.type === "JSXElement") {
|
|
23
|
-
const opening = node.openingElement;
|
|
24
|
-
if (opening && opening.name) {
|
|
25
|
-
if (opening.name.type === "JSXIdentifier") {
|
|
26
|
-
return /^[A-Z]/.test(opening.name.name);
|
|
27
|
-
}
|
|
28
|
-
if (opening.name.type === "JSXMemberExpression") {
|
|
29
|
-
let current = opening.name;
|
|
30
|
-
while (
|
|
31
|
-
current &&
|
|
32
|
-
current.type === "JSXMemberExpression"
|
|
33
|
-
) {
|
|
34
|
-
current = current.object;
|
|
35
|
-
}
|
|
36
|
-
return (
|
|
37
|
-
current &&
|
|
38
|
-
current.type === "JSXIdentifier" &&
|
|
39
|
-
/^[A-Z]/.test(current.name)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Returns true if the returned JSX is singular.
|
|
49
|
-
// For both JSXElement and JSXFragment, singular means 0 or 1 non-whitespace child.
|
|
50
|
-
function isSingularJSXReturn(node) {
|
|
51
|
-
if (!isJSX(node)) return false;
|
|
52
|
-
let children = [];
|
|
53
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
54
|
-
children = node.children.filter((child) => {
|
|
55
|
-
if (child.type === "JSXText") {
|
|
56
|
-
return child.value.trim() !== "";
|
|
57
|
-
}
|
|
58
|
-
return true;
|
|
59
|
-
});
|
|
60
|
-
// Check if the returned element has exactly one child.
|
|
61
|
-
if (children.length === 1) {
|
|
62
|
-
const child = children[0];
|
|
63
|
-
// If the singular child is also a JSX element or fragment,
|
|
64
|
-
// ensure that it doesn't have any meaningful children.
|
|
65
|
-
if (
|
|
66
|
-
child.type === "JSXElement" ||
|
|
67
|
-
child.type === "JSXFragment"
|
|
68
|
-
) {
|
|
69
|
-
const innerChildren = child.children.filter(
|
|
70
|
-
(innerChild) => {
|
|
71
|
-
if (innerChild.type === "JSXText") {
|
|
72
|
-
return innerChild.value.trim() !== "";
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
);
|
|
77
|
-
return innerChildren.length === 0;
|
|
78
|
-
}
|
|
79
|
-
// If it’s not a JSX element (maybe a simple expression), it's acceptable.
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
// If there are no children or more than one child, it's not singular.
|
|
83
|
-
return children.length === 0;
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Stack to track nested function nodes.
|
|
89
|
-
const functionStack = [];
|
|
90
|
-
function pushFunction(node) {
|
|
91
|
-
functionStack.push(node);
|
|
92
|
-
}
|
|
93
|
-
function popFunction() {
|
|
94
|
-
functionStack.pop();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
"FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(
|
|
99
|
-
node
|
|
100
|
-
) {
|
|
101
|
-
pushFunction(node);
|
|
102
|
-
},
|
|
103
|
-
"FunctionDeclaration:exit"(node) {
|
|
104
|
-
popFunction();
|
|
105
|
-
},
|
|
106
|
-
"FunctionExpression:exit"(node) {
|
|
107
|
-
popFunction();
|
|
108
|
-
},
|
|
109
|
-
"ArrowFunctionExpression:exit"(node) {
|
|
110
|
-
popFunction();
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
// For explicit return statements, report if the returned JSX is not a component and not singular.
|
|
114
|
-
ReturnStatement(node) {
|
|
115
|
-
if (
|
|
116
|
-
functionStack.length > 1 &&
|
|
117
|
-
isJSX(node.argument) &&
|
|
118
|
-
!isJSXComponentElement(node.argument) &&
|
|
119
|
-
!isSingularJSXReturn(node.argument)
|
|
120
|
-
) {
|
|
121
|
-
context.report({
|
|
122
|
-
node,
|
|
123
|
-
message:
|
|
124
|
-
"Nested function returning non-component, non-singular JSX detected. Extract it into its own component."
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
// For implicit returns in arrow functions, use the same checks.
|
|
130
|
-
"ArrowFunctionExpression > JSXElement"(node) {
|
|
131
|
-
if (
|
|
132
|
-
functionStack.length > 1 &&
|
|
133
|
-
!isJSXComponentElement(node) &&
|
|
134
|
-
!isSingularJSXReturn(node)
|
|
135
|
-
) {
|
|
136
|
-
context.report({
|
|
137
|
-
node,
|
|
138
|
-
message:
|
|
139
|
-
"Nested arrow function returning non-component, non-singular JSX detected. Extract it into its own component."
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
"ArrowFunctionExpression > JSXFragment"(node) {
|
|
144
|
-
if (functionStack.length > 1 && !isSingularJSXReturn(node)) {
|
|
145
|
-
context.report({
|
|
146
|
-
node,
|
|
147
|
-
message:
|
|
148
|
-
"Nested arrow function returning a non-singular JSX fragment detected. Extract it into its own component."
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
"Prefer using logical && operator over ternary with null/undefined for conditional JSX rendering.",
|
|
7
|
-
recommended: false
|
|
8
|
-
},
|
|
9
|
-
messages: {
|
|
10
|
-
useLogicalAnd:
|
|
11
|
-
"Prefer using the logical '&&' operator instead of a ternary with null/undefined for conditional rendering."
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
create(context) {
|
|
15
|
-
return {
|
|
16
|
-
ConditionalExpression(node) {
|
|
17
|
-
const alternate = node.alternate;
|
|
18
|
-
// Check if alternate is explicitly null or undefined
|
|
19
|
-
if (
|
|
20
|
-
alternate &&
|
|
21
|
-
((alternate.type === "Literal" &&
|
|
22
|
-
alternate.value === null) ||
|
|
23
|
-
(alternate.type === "Identifier" &&
|
|
24
|
-
alternate.name === "undefined"))
|
|
25
|
-
) {
|
|
26
|
-
// Check if the node is within a JSX expression container.
|
|
27
|
-
if (
|
|
28
|
-
node.parent &&
|
|
29
|
-
node.parent.type === "JSXExpressionContainer"
|
|
30
|
-
) {
|
|
31
|
-
const containerParent = node.parent.parent;
|
|
32
|
-
// Only flag if the JSXExpressionContainer is used as a child,
|
|
33
|
-
// not as a prop (i.e. not within a JSXAttribute)
|
|
34
|
-
if (
|
|
35
|
-
containerParent &&
|
|
36
|
-
containerParent.type !== "JSXAttribute"
|
|
37
|
-
) {
|
|
38
|
-
context.report({
|
|
39
|
-
node,
|
|
40
|
-
messageId: "useLogicalAnd"
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// TODO : Add a fix function to this rule, it needs a deep unconflicting fix becasue of react/jsx-no-leaked-render, it needs to explicitly be === something like that
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Disallow the "transition" property in objects typed as CSSProperties.
|
|
3
|
-
*
|
|
4
|
-
* This rule inspects VariableDeclarators where the identifier has a type annotation that
|
|
5
|
-
* includes "CSSProperties" (either as a TSTypeReference or by a text check fallback).
|
|
6
|
-
* It then checks if the initializer is an object literal containing a property named "transition".
|
|
7
|
-
*
|
|
8
|
-
* This is intended to help avoid conflicts with react-spring.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export default {
|
|
12
|
-
meta: {
|
|
13
|
-
type: "problem",
|
|
14
|
-
docs: {
|
|
15
|
-
description:
|
|
16
|
-
"Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring.",
|
|
17
|
-
recommended: false
|
|
18
|
-
},
|
|
19
|
-
schema: [],
|
|
20
|
-
messages: {
|
|
21
|
-
forbiddenTransition:
|
|
22
|
-
"Objects typed as CSSProperties must not include a 'transition' property as it conflicts with react-spring."
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
create(context) {
|
|
27
|
-
const sourceCode = context.getSourceCode();
|
|
28
|
-
return {
|
|
29
|
-
VariableDeclarator(node) {
|
|
30
|
-
// Ensure the variable identifier exists, is an Identifier, and has a type annotation.
|
|
31
|
-
if (
|
|
32
|
-
!node.id ||
|
|
33
|
-
node.id.type !== "Identifier" ||
|
|
34
|
-
!node.id.typeAnnotation
|
|
35
|
-
) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
let isStyleType = false;
|
|
39
|
-
const typeAnnotation = node.id.typeAnnotation.typeAnnotation;
|
|
40
|
-
|
|
41
|
-
// First try: check if it's a TSTypeReference with typeName "CSSProperties"
|
|
42
|
-
if (
|
|
43
|
-
typeAnnotation &&
|
|
44
|
-
typeAnnotation.type === "TSTypeReference"
|
|
45
|
-
) {
|
|
46
|
-
const { typeName } = typeAnnotation;
|
|
47
|
-
if (
|
|
48
|
-
typeName.type === "Identifier" &&
|
|
49
|
-
typeName.name === "CSSProperties"
|
|
50
|
-
) {
|
|
51
|
-
isStyleType = true;
|
|
52
|
-
} else if (
|
|
53
|
-
typeName.type === "TSQualifiedName" &&
|
|
54
|
-
typeName.right &&
|
|
55
|
-
typeName.right.name === "CSSProperties"
|
|
56
|
-
) {
|
|
57
|
-
isStyleType = true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Fallback: if the AST shape doesn't match, check the raw text of the annotation.
|
|
62
|
-
if (!isStyleType) {
|
|
63
|
-
const annotationText = sourceCode.getText(
|
|
64
|
-
node.id.typeAnnotation
|
|
65
|
-
);
|
|
66
|
-
if (annotationText.includes("CSSProperties")) {
|
|
67
|
-
isStyleType = true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!isStyleType) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check that the initializer is an object literal.
|
|
76
|
-
if (node.init && node.init.type === "ObjectExpression") {
|
|
77
|
-
node.init.properties.forEach((prop) => {
|
|
78
|
-
// Only consider regular properties.
|
|
79
|
-
if (prop.type !== "Property") {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (prop.computed) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let keyName = null;
|
|
86
|
-
if (prop.key.type === "Identifier") {
|
|
87
|
-
keyName = prop.key.name;
|
|
88
|
-
} else if (prop.key.type === "Literal") {
|
|
89
|
-
keyName = String(prop.key.value);
|
|
90
|
-
}
|
|
91
|
-
if (keyName === "transition") {
|
|
92
|
-
context.report({
|
|
93
|
-
node: prop,
|
|
94
|
-
messageId: "forbiddenTransition"
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
"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.",
|
|
7
|
-
category: "Best Practices",
|
|
8
|
-
recommended: false
|
|
9
|
-
}
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
create(context) {
|
|
13
|
-
return {
|
|
14
|
-
JSXElement(node) {
|
|
15
|
-
if (
|
|
16
|
-
node.openingElement.name &&
|
|
17
|
-
node.openingElement.name.name === "div"
|
|
18
|
-
) {
|
|
19
|
-
const meaningfulChildren = node.children.filter((child) => {
|
|
20
|
-
if (child.type === "JSXText") {
|
|
21
|
-
return child.value.trim() !== "";
|
|
22
|
-
}
|
|
23
|
-
return true;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
meaningfulChildren.length === 1 &&
|
|
28
|
-
meaningfulChildren[0].type === "JSXElement"
|
|
29
|
-
) {
|
|
30
|
-
context.report({
|
|
31
|
-
node,
|
|
32
|
-
message:
|
|
33
|
-
"Unnecessary <div> wrapper detected. Remove it if not needed, or replace with a semantic element that reflects its purpose."
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
};
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Enforce that the key prop is only used on components
|
|
3
|
-
* rendered as part of an array mapping. This rule disallows having a key prop
|
|
4
|
-
* on a JSX element when it is not part of a mapping, except when the element is
|
|
5
|
-
* returned from a helper render function.
|
|
6
|
-
*
|
|
7
|
-
* The rule walks up the ancestors of the JSX element to check if one of them
|
|
8
|
-
* is a CallExpression where the callee is a MemberExpression with the property "map".
|
|
9
|
-
* If not—and if the JSX element is not directly returned from a helper function—
|
|
10
|
-
* then a key prop is considered unnecessary and an error is reported.
|
|
11
|
-
*
|
|
12
|
-
* Note: This rule does not auto-fix.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export default {
|
|
16
|
-
meta: {
|
|
17
|
-
type: "problem",
|
|
18
|
-
docs: {
|
|
19
|
-
description:
|
|
20
|
-
"enforce that the key prop is only used on components rendered as part of a mapping",
|
|
21
|
-
recommended: false
|
|
22
|
-
},
|
|
23
|
-
schema: [],
|
|
24
|
-
messages: {
|
|
25
|
-
unnecessaryKey:
|
|
26
|
-
"The key prop should only be used on elements that are directly rendered as part of an array mapping."
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
create(context) {
|
|
31
|
-
// Polyfill for context.getAncestors if it's not available.
|
|
32
|
-
function getAncestors(node) {
|
|
33
|
-
const ancestors = [];
|
|
34
|
-
let current = node.parent;
|
|
35
|
-
while (current) {
|
|
36
|
-
ancestors.push(current);
|
|
37
|
-
current = current.parent;
|
|
38
|
-
}
|
|
39
|
-
return ancestors;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Checks if any of the ancestors is a CallExpression
|
|
44
|
-
* representing an array mapping (i.e. its callee is a MemberExpression
|
|
45
|
-
* whose property is an identifier or literal named "map").
|
|
46
|
-
*
|
|
47
|
-
* @param {ASTNode[]} ancestors - The array of ancestor nodes.
|
|
48
|
-
* @returns {boolean} True if a mapping is detected; otherwise, false.
|
|
49
|
-
*/
|
|
50
|
-
function isInsideMapCall(ancestors) {
|
|
51
|
-
return ancestors.some((node) => {
|
|
52
|
-
if (
|
|
53
|
-
node.type === "CallExpression" &&
|
|
54
|
-
node.callee &&
|
|
55
|
-
node.callee.type === "MemberExpression"
|
|
56
|
-
) {
|
|
57
|
-
const property = node.callee.property;
|
|
58
|
-
return (
|
|
59
|
-
(property.type === "Identifier" &&
|
|
60
|
-
property.name === "map") ||
|
|
61
|
-
(property.type === "Literal" &&
|
|
62
|
-
property.value === "map")
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Checks whether the JSX element is being returned from a helper render
|
|
71
|
-
* function. If so, we assume the key prop might be needed when the function
|
|
72
|
-
* is eventually invoked from a mapping.
|
|
73
|
-
*
|
|
74
|
-
* @param {ASTNode[]} ancestors - The array of ancestor nodes.
|
|
75
|
-
* @returns {boolean} True if the element is inside a helper render function.
|
|
76
|
-
*/
|
|
77
|
-
function isReturnedFromFunction(ancestors) {
|
|
78
|
-
// Look for a ReturnStatement in the ancestry.
|
|
79
|
-
return ancestors.some((node) => node.type === "ReturnStatement");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Reports a JSX element if it has a key prop and is not rendered as part
|
|
84
|
-
* of an inline mapping (and not simply returned from a render helper function).
|
|
85
|
-
*
|
|
86
|
-
* @param {ASTNode} node - The JSXOpeningElement node.
|
|
87
|
-
*/
|
|
88
|
-
function checkJSXOpeningElement(node) {
|
|
89
|
-
// Find a key attribute.
|
|
90
|
-
const keyAttribute = node.attributes.find(
|
|
91
|
-
(attr) =>
|
|
92
|
-
attr.type === "JSXAttribute" &&
|
|
93
|
-
attr.name &&
|
|
94
|
-
attr.name.name === "key"
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
if (!keyAttribute) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Retrieve ancestors (using context.getAncestors if available).
|
|
102
|
-
const ancestors =
|
|
103
|
-
typeof context.getAncestors === "function"
|
|
104
|
-
? context.getAncestors()
|
|
105
|
-
: getAncestors(node);
|
|
106
|
-
|
|
107
|
-
// If the element is (directly or indirectly) part of a map call, allow it.
|
|
108
|
-
if (isInsideMapCall(ancestors)) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// If the element is simply returned from a helper function, allow it.
|
|
113
|
-
if (isReturnedFromFunction(ancestors)) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Otherwise, report the key prop as unnecessary.
|
|
118
|
-
context.report({
|
|
119
|
-
node: keyAttribute,
|
|
120
|
-
messageId: "unnecessaryKey"
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
JSXOpeningElement: checkJSXOpeningElement
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
"Disallow functions that have no parameters and just return an object literal; consider exporting the object directly, unless the function is used as a callback (e.g., in react-spring).",
|
|
7
|
-
category: "Best Practices",
|
|
8
|
-
recommended: false
|
|
9
|
-
},
|
|
10
|
-
fixable: null
|
|
11
|
-
},
|
|
12
|
-
create(context) {
|
|
13
|
-
function isCallbackFunction(node) {
|
|
14
|
-
// Check if the node is an argument of a CallExpression
|
|
15
|
-
return (
|
|
16
|
-
node.parent &&
|
|
17
|
-
node.parent.type === "CallExpression" &&
|
|
18
|
-
node.parent.arguments.includes(node)
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
ArrowFunctionExpression(node) {
|
|
24
|
-
// Check for functions with no parameters and a body that's an ObjectExpression
|
|
25
|
-
if (
|
|
26
|
-
node.params.length === 0 &&
|
|
27
|
-
node.body &&
|
|
28
|
-
node.body.type === "ObjectExpression"
|
|
29
|
-
) {
|
|
30
|
-
// If the function is used as a callback (like in react-spring), skip reporting.
|
|
31
|
-
if (isCallbackFunction(node)) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
context.report({
|
|
35
|
-
node,
|
|
36
|
-
message:
|
|
37
|
-
"This function has no parameters and simply returns an object. Consider exporting the object directly instead of wrapping it in a function."
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description:
|
|
6
|
-
"Warn when a component file (.jsx or .tsx) contains a style object typed as CSSProperties. " +
|
|
7
|
-
"Style objects should be moved to their own file under the style folder.",
|
|
8
|
-
recommended: false
|
|
9
|
-
},
|
|
10
|
-
schema: [],
|
|
11
|
-
messages: {
|
|
12
|
-
moveToFile:
|
|
13
|
-
'Style object "{{name}}" is typed as {{typeName}}. Move it to its own file under the style folder.'
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
create(context) {
|
|
18
|
-
// Only run this rule on .tsx or .jsx files.
|
|
19
|
-
const filename = context.getFilename();
|
|
20
|
-
if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
VariableDeclarator(node) {
|
|
26
|
-
// Ensure this is a variable declaration with an Identifier.
|
|
27
|
-
if (!node.id || node.id.type !== "Identifier") return;
|
|
28
|
-
|
|
29
|
-
const identifier = node.id;
|
|
30
|
-
// Check if there's a type annotation on the variable.
|
|
31
|
-
if (!identifier.typeAnnotation) return;
|
|
32
|
-
|
|
33
|
-
const typeNode = identifier.typeAnnotation.typeAnnotation;
|
|
34
|
-
// Handle both Identifier and TSQualifiedName cases.
|
|
35
|
-
if (typeNode.type === "TSTypeReference" && typeNode.typeName) {
|
|
36
|
-
let typeName = null;
|
|
37
|
-
|
|
38
|
-
// When typeName is a simple Identifier.
|
|
39
|
-
if (typeNode.typeName.type === "Identifier") {
|
|
40
|
-
typeName = typeNode.typeName.name;
|
|
41
|
-
}
|
|
42
|
-
// When typeName is a TSQualifiedName, e.g., React.CSSProperties.
|
|
43
|
-
else if (typeNode.typeName.type === "TSQualifiedName") {
|
|
44
|
-
typeName = typeNode.typeName.right.name;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Report if the type name is CSSProperties.
|
|
48
|
-
if (typeName === "CSSProperties") {
|
|
49
|
-
context.report({
|
|
50
|
-
node,
|
|
51
|
-
messageId: "moveToFile",
|
|
52
|
-
data: {
|
|
53
|
-
name: identifier.name,
|
|
54
|
-
typeName: typeName
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
};
|