@hypersolid-dev/eslint-config 5.0.0 → 6.0.0
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/CHANGELOG.md +15 -0
- package/README.md +17 -1
- package/dist/base.js +14 -1
- package/dist/jest.d.ts +3 -0
- package/dist/jest.js +56 -0
- package/dist/plugins/props-destructuring.js +12 -12
- package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-label.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-role.js +2 -2
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +25 -27
- package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +16 -18
- package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +4 -4
- package/dist/plugins/react-native-a11y/rules/input-purpose.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +20 -22
- package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +12 -14
- package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +24 -26
- package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +34 -36
- package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +31 -33
- package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +2 -2
- package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +2 -2
- package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +2 -2
- package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +2 -2
- package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +15 -13
- package/dist/plugins/rules/boolean-guard.js +3 -3
- package/dist/plugins/rules/comment-style.js +43 -34
- package/dist/plugins/rules/effect-takes-callback.js +1 -1
- package/dist/plugins/rules/jsdoc-for-props.js +1 -1
- package/dist/plugins/rules/no-callback-payload.js +18 -15
- package/dist/plugins/rules/no-markup-variable.js +41 -35
- package/dist/plugins/rules/no-unnecessary-use-callback.js +2 -2
- package/dist/plugins/rules/regex-pattern-const.js +1 -1
- package/dist/plugins/rules/test-id-default.js +23 -27
- package/dist/plugins/rules/test-id-format.js +14 -18
- package/dist/plugins/rules/test-id-from-props.js +23 -27
- package/dist/plugins/rules/undefined-check.js +2 -2
- package/dist/plugins/rules/webview-allow-list.js +22 -26
- package/dist/plugins/utils/expressions.d.ts +3 -3
- package/dist/plugins/utils/expressions.js +94 -78
- package/dist/plugins/utils/types.js +14 -12
- package/dist/react-native.js +28 -0
- package/dist/shared.d.ts +3 -3
- package/dist/shared.js +168 -112
- package/package.json +12 -2
|
@@ -31,46 +31,44 @@ const noIneffectiveAccessibilityProps = createRule({
|
|
|
31
31
|
},
|
|
32
32
|
schema: [],
|
|
33
33
|
},
|
|
34
|
-
create(context) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
create: (context) => ({
|
|
35
|
+
JSXOpeningElement: (node) => {
|
|
36
|
+
const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
|
|
37
|
+
if (role === "listitem") {
|
|
38
|
+
context.report({ node, messageId: "listItemRole" });
|
|
39
|
+
}
|
|
40
|
+
if (role === "alert") {
|
|
41
|
+
context.report({ node, messageId: "alertRole" });
|
|
42
|
+
}
|
|
43
|
+
const isHidden = isDefinitelyHidden(node);
|
|
44
|
+
if (isHidden && hasLabelProperty(node)) {
|
|
45
|
+
context.report({ node, messageId: "hiddenWithLabel" });
|
|
46
|
+
}
|
|
47
|
+
if (getLiteralPropertyValue(node, "accessible") === false) {
|
|
48
|
+
const name = getElementName(node);
|
|
49
|
+
if (isHidden) {
|
|
50
|
+
context.report({ node, messageId: "hiddenWithAccessibleFalse" });
|
|
40
51
|
}
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (isHidden && hasLabelProperty(node)) {
|
|
46
|
-
context.report({ node, messageId: "hiddenWithLabel" });
|
|
47
|
-
}
|
|
48
|
-
if (getLiteralPropertyValue(node, "accessible") === false) {
|
|
49
|
-
const name = getElementName(node);
|
|
50
|
-
if (isHidden) {
|
|
51
|
-
context.report({ node, messageId: "hiddenWithAccessibleFalse" });
|
|
52
|
-
}
|
|
53
|
-
else if (name !== undefined &&
|
|
54
|
-
DEFAULTS_TO_INACCESSIBLE.has(name) &&
|
|
55
|
-
// A spread may carry the `accessible` this one is overriding.
|
|
56
|
-
!hasSpreadProps(node)) {
|
|
57
|
-
context.report({
|
|
58
|
-
node,
|
|
59
|
-
messageId: "accessibleFalseIsDefault",
|
|
60
|
-
data: { name },
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const labelledBy = getProperty(node, LABELLED_BY_PROPS);
|
|
65
|
-
if (labelledBy && !hasLabelProperty(node)) {
|
|
52
|
+
else if (name !== undefined &&
|
|
53
|
+
DEFAULTS_TO_INACCESSIBLE.has(name) &&
|
|
54
|
+
// A spread may carry the `accessible` this one is overriding.
|
|
55
|
+
!hasSpreadProps(node)) {
|
|
66
56
|
context.report({
|
|
67
57
|
node,
|
|
68
|
-
messageId: "
|
|
69
|
-
data: {
|
|
58
|
+
messageId: "accessibleFalseIsDefault",
|
|
59
|
+
data: { name },
|
|
70
60
|
});
|
|
71
61
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
}
|
|
63
|
+
const labelledBy = getProperty(node, LABELLED_BY_PROPS);
|
|
64
|
+
if (labelledBy && !hasLabelProperty(node)) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "labelledByWithoutLabel",
|
|
68
|
+
data: { prop: labelledBy.name.name },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
75
73
|
});
|
|
76
74
|
export default noIneffectiveAccessibilityProps;
|
|
@@ -44,39 +44,37 @@ const noInvalidAccessibilityState = createRule({
|
|
|
44
44
|
},
|
|
45
45
|
schema: [],
|
|
46
46
|
},
|
|
47
|
-
create(context) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
create: (context) => ({
|
|
48
|
+
JSXOpeningElement: (node) => {
|
|
49
|
+
const attribute = getProperty(node, "accessibilityState");
|
|
50
|
+
if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const { expression } = attribute.value;
|
|
54
|
+
// An object literal here is already rejected by the React Native types
|
|
55
|
+
if (expression.type !== AST_NODE_TYPES.Identifier) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const property of resolved.properties) {
|
|
63
|
+
if (property.type !== AST_NODE_TYPES.Property ||
|
|
64
|
+
property.key.type !== AST_NODE_TYPES.Identifier ||
|
|
65
|
+
ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
|
|
66
|
+
continue;
|
|
53
67
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
property.key.type !== AST_NODE_TYPES.Identifier ||
|
|
66
|
-
ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
context.report({
|
|
70
|
-
node: property,
|
|
71
|
-
messageId: "invalidStateKey",
|
|
72
|
-
data: {
|
|
73
|
-
key: property.key.name,
|
|
74
|
-
supported: ACCESSIBILITY_STATE_KEYS.join(", "),
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
},
|
|
68
|
+
context.report({
|
|
69
|
+
node: property,
|
|
70
|
+
messageId: "invalidStateKey",
|
|
71
|
+
data: {
|
|
72
|
+
key: property.key.name,
|
|
73
|
+
supported: ACCESSIBILITY_STATE_KEYS.join(", "),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
81
79
|
});
|
|
82
80
|
export default noInvalidAccessibilityState;
|
|
@@ -30,7 +30,7 @@ const noNestedFocusStops = createRule({
|
|
|
30
30
|
},
|
|
31
31
|
],
|
|
32
32
|
},
|
|
33
|
-
create(context) {
|
|
33
|
+
create: (context) => {
|
|
34
34
|
// The touchables plus whatever this project wraps them in
|
|
35
35
|
const components = getComponents({
|
|
36
36
|
context,
|
|
@@ -44,7 +44,7 @@ const noNestedFocusStops = createRule({
|
|
|
44
44
|
hasTouchHandler(node));
|
|
45
45
|
};
|
|
46
46
|
return {
|
|
47
|
-
JSXOpeningElement(node) {
|
|
47
|
+
JSXOpeningElement: (node) => {
|
|
48
48
|
const componentName = getElementName(node);
|
|
49
49
|
if (componentName === undefined ||
|
|
50
50
|
!isDefinitelyGrouped(node) ||
|
|
@@ -29,13 +29,13 @@ const noPressInActivation = createRule({
|
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
},
|
|
32
|
-
create(context) {
|
|
32
|
+
create: (context) => {
|
|
33
33
|
const components = getComponents({
|
|
34
34
|
context,
|
|
35
35
|
defaultComponents: TOUCHABLES,
|
|
36
36
|
});
|
|
37
37
|
return {
|
|
38
|
-
JSXOpeningElement(node) {
|
|
38
|
+
JSXOpeningElement: (node) => {
|
|
39
39
|
const componentName = getElementName(node);
|
|
40
40
|
if (componentName === undefined) {
|
|
41
41
|
return;
|
|
@@ -38,10 +38,10 @@ const noUnnamedInteractive = createRule({
|
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
},
|
|
41
|
-
create(context) {
|
|
41
|
+
create: (context) => {
|
|
42
42
|
const components = getComponents({ context });
|
|
43
43
|
return {
|
|
44
|
-
JSXOpeningElement(node) {
|
|
44
|
+
JSXOpeningElement: (node) => {
|
|
45
45
|
const componentName = getElementName(node);
|
|
46
46
|
if (componentName === undefined) {
|
|
47
47
|
return;
|
|
@@ -29,7 +29,7 @@ const requireInvertColorsOptOut = createRule({
|
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
},
|
|
32
|
-
create(context) {
|
|
32
|
+
create: (context) => {
|
|
33
33
|
/**
|
|
34
34
|
* Matched on the element name, so an image from expo-image or a wrapper
|
|
35
35
|
* named through additionalComponents is checked the same way. Deliberately
|
|
@@ -41,7 +41,7 @@ const requireInvertColorsOptOut = createRule({
|
|
|
41
41
|
defaultComponents: IMAGES,
|
|
42
42
|
});
|
|
43
43
|
return {
|
|
44
|
-
JSXOpeningElement(node) {
|
|
44
|
+
JSXOpeningElement: (node) => {
|
|
45
45
|
const componentName = getElementName(node);
|
|
46
46
|
if (componentName === undefined) {
|
|
47
47
|
return;
|
|
@@ -27,7 +27,7 @@ const requireModalA11yOnSheets = createRule({
|
|
|
27
27
|
},
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
|
-
create(context) {
|
|
30
|
+
create: (context) => {
|
|
31
31
|
/**
|
|
32
32
|
* No default list: a sheet is whatever this project calls one, and
|
|
33
33
|
* hardcoding one library's component names would fire on a codebase that
|
|
@@ -35,7 +35,7 @@ const requireModalA11yOnSheets = createRule({
|
|
|
35
35
|
*/
|
|
36
36
|
const components = getComponents({ context });
|
|
37
37
|
return {
|
|
38
|
-
JSXOpeningElement(node) {
|
|
38
|
+
JSXOpeningElement: (node) => {
|
|
39
39
|
const componentName = getElementName(node);
|
|
40
40
|
if (componentName === undefined) {
|
|
41
41
|
return;
|
|
@@ -32,6 +32,20 @@ const branchesOf = (expression) => {
|
|
|
32
32
|
};
|
|
33
33
|
const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
|
|
34
34
|
expression.type === AST_NODE_TYPES.JSXFragment;
|
|
35
|
+
const countExpressionText = (expression, amount) => {
|
|
36
|
+
if (expression.type === AST_NODE_TYPES.Literal ||
|
|
37
|
+
expression.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
38
|
+
return amount + 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* JSX inside an expression renders like any other child, and a branch
|
|
42
|
+
* that renders text still names the control
|
|
43
|
+
*/
|
|
44
|
+
const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
|
|
45
|
+
return rendered.length > 0
|
|
46
|
+
? getAmountOfTextChildren(rendered, amount)
|
|
47
|
+
: amount;
|
|
48
|
+
};
|
|
35
49
|
/**
|
|
36
50
|
* Counts children that render visible text, which is what makes an
|
|
37
51
|
* `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
|
|
@@ -42,19 +56,7 @@ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((am
|
|
|
42
56
|
return child.value.trim().length > 0 ? amount + 1 : amount;
|
|
43
57
|
}
|
|
44
58
|
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
45
|
-
|
|
46
|
-
if (expression.type === AST_NODE_TYPES.Literal ||
|
|
47
|
-
expression.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
48
|
-
return amount + 1;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* JSX inside an expression renders like any other child, and a branch
|
|
52
|
-
* that renders text still names the control
|
|
53
|
-
*/
|
|
54
|
-
const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
|
|
55
|
-
return rendered.length > 0
|
|
56
|
-
? getAmountOfTextChildren(rendered, amount)
|
|
57
|
-
: amount;
|
|
59
|
+
return countExpressionText(child.expression, amount);
|
|
58
60
|
}
|
|
59
61
|
if (child.type === AST_NODE_TYPES.JSXFragment) {
|
|
60
62
|
return getAmountOfTextChildren(child.children, amount);
|
|
@@ -69,7 +69,7 @@ const booleanGuard = createRule({
|
|
|
69
69
|
preferBangBang: "`!== undefined` is `!!` for this type. Write `!!{{condition}} && …`.",
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
|
-
create(context) {
|
|
72
|
+
create: (context) => {
|
|
73
73
|
const services = ESLintUtils.getParserServices(context, true);
|
|
74
74
|
if (!services.program)
|
|
75
75
|
return {};
|
|
@@ -99,7 +99,7 @@ const booleanGuard = createRule({
|
|
|
99
99
|
return reads.every((reference) => isInRenderPosition(reference.identifier));
|
|
100
100
|
};
|
|
101
101
|
return {
|
|
102
|
-
ConditionalExpression(node) {
|
|
102
|
+
ConditionalExpression: (node) => {
|
|
103
103
|
// The shape is what proves a `const` holding this is a guard.
|
|
104
104
|
const isConsequentGuarded = isRenderable(node.consequent) && isNothing(node.alternate);
|
|
105
105
|
const isAlternateGuarded = isRenderable(node.alternate) && isNothing(node.consequent);
|
|
@@ -122,7 +122,7 @@ const booleanGuard = createRule({
|
|
|
122
122
|
data: { condition: isAlternateGuarded ? `!${nameOf(node.test)}` : nameOf(node.test) },
|
|
123
123
|
});
|
|
124
124
|
},
|
|
125
|
-
LogicalExpression(node) {
|
|
125
|
+
LogicalExpression: (node) => {
|
|
126
126
|
if (node.operator !== "&&")
|
|
127
127
|
return;
|
|
128
128
|
if (!isInRenderPosition(node))
|
|
@@ -74,7 +74,7 @@ export default createRule({
|
|
|
74
74
|
tooLong: "This comment line is {{length}} characters. Keep it to {{max}}.",
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
|
-
create(context) {
|
|
77
|
+
create: (context) => {
|
|
78
78
|
const { sourceCode } = context;
|
|
79
79
|
const maxLength = context.options[0]?.maxLength;
|
|
80
80
|
const isTooLong = (comment) => Boolean(maxLength) && lineLength(comment) > (maxLength ?? 0);
|
|
@@ -86,7 +86,7 @@ export default createRule({
|
|
|
86
86
|
});
|
|
87
87
|
};
|
|
88
88
|
return {
|
|
89
|
-
"Program:exit"() {
|
|
89
|
+
"Program:exit": () => {
|
|
90
90
|
let run = [];
|
|
91
91
|
const doesContinue = (comment) => {
|
|
92
92
|
const last = run.at(-1);
|
|
@@ -108,48 +108,57 @@ export default createRule({
|
|
|
108
108
|
}
|
|
109
109
|
run = [];
|
|
110
110
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
if (!doesContinue(comment))
|
|
122
|
-
flush();
|
|
123
|
-
run.push(comment);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
flush();
|
|
127
|
-
if (isJsdoc(comment)) {
|
|
128
|
-
if (isTooLong(comment))
|
|
129
|
-
reportTooLong(comment);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (rows > 1) {
|
|
133
|
-
context.report({
|
|
134
|
-
loc: comment.loc,
|
|
135
|
-
messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
|
|
136
|
-
data: { rows: String(rows) },
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
else if (isTooLong(comment)) {
|
|
111
|
+
/** A JSX comment's `{` is syntax, so a row holding only a braced JSX comment starts its row. */
|
|
112
|
+
const isOwnRow = (comment) => isRowStart(sourceCode, comment) ||
|
|
113
|
+
(isJsxComment(sourceCode, comment) &&
|
|
114
|
+
!sourceCode.lines[comment.loc.start.line - 1]
|
|
115
|
+
?.slice(0, Math.max(0, comment.loc.start.column - 1))
|
|
116
|
+
.trim());
|
|
117
|
+
/** A block comment that is not part of a run: JSDoc, multi-row, or sharing its row with code. */
|
|
118
|
+
const checkLoneBlock = (comment, rows) => {
|
|
119
|
+
if (isJsdoc(comment)) {
|
|
120
|
+
if (isTooLong(comment))
|
|
140
121
|
reportTooLong(comment);
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (rows > 1) {
|
|
125
|
+
context.report({
|
|
126
|
+
loc: comment.loc,
|
|
127
|
+
messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
|
|
128
|
+
data: { rows: String(rows) },
|
|
129
|
+
});
|
|
143
130
|
}
|
|
131
|
+
else if (isTooLong(comment)) {
|
|
132
|
+
reportTooLong(comment);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const visitBlock = (comment) => {
|
|
136
|
+
const rows = comment.loc.end.line - comment.loc.start.line + 1;
|
|
137
|
+
if (!isJsdoc(comment) && rows === 1 && isOwnRow(comment)) {
|
|
138
|
+
if (!doesContinue(comment))
|
|
139
|
+
flush();
|
|
140
|
+
run.push(comment);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
flush();
|
|
144
|
+
checkLoneBlock(comment, rows);
|
|
145
|
+
};
|
|
146
|
+
const visitLine = (comment) => {
|
|
144
147
|
if (!doesContinue(comment))
|
|
145
148
|
flush();
|
|
146
149
|
if (!isRowStart(sourceCode, comment)) {
|
|
147
150
|
flush();
|
|
148
151
|
if (isTooLong(comment))
|
|
149
152
|
reportTooLong(comment);
|
|
150
|
-
|
|
153
|
+
return;
|
|
151
154
|
}
|
|
152
155
|
run.push(comment);
|
|
156
|
+
};
|
|
157
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
158
|
+
if (comment.type === AST_TOKEN_TYPES.Block)
|
|
159
|
+
visitBlock(comment);
|
|
160
|
+
else
|
|
161
|
+
visitLine(comment);
|
|
153
162
|
}
|
|
154
163
|
flush();
|
|
155
164
|
},
|
|
@@ -48,7 +48,7 @@ const effectTakesCallback = createRule({
|
|
|
48
48
|
callbackFirst: "`{{name}}` ends in Effect but its first parameter is not a callback. Take the callback first, or drop the Effect suffix.",
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
|
-
create(context) {
|
|
51
|
+
create: (context) => {
|
|
52
52
|
const services = context.sourceCode.parserServices;
|
|
53
53
|
const checker = services?.program ? services.program.getTypeChecker() : undefined;
|
|
54
54
|
/** Whether a value of this type can be called, once `undefined` is set aside. */
|
|
@@ -28,7 +28,7 @@ export default createRule({
|
|
|
28
28
|
form: "Write this as a JSDoc block, not `{{ form }}`. A member's comment is what an editor shows when someone hovers it elsewhere, and only JSDoc reaches there.",
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
|
-
create(context) {
|
|
31
|
+
create: (context) => {
|
|
32
32
|
const { sourceCode } = context;
|
|
33
33
|
const check = (node) => {
|
|
34
34
|
// A directive neither documents the member nor separates it from the comment above.
|
|
@@ -2,6 +2,18 @@ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
|
2
2
|
import createRule from "../utils/create-rule.js";
|
|
3
3
|
import { componentName, isWrapperCall } from "../utils/component-name.js";
|
|
4
4
|
const CALLBACK_PATTERN = /^on[A-Z]/;
|
|
5
|
+
/** Adds each identifier an object pattern binds to `props`, and its rest binding to `objects` too. */
|
|
6
|
+
const addPatternBindings = (pattern, props, objects) => {
|
|
7
|
+
for (const property of pattern.properties) {
|
|
8
|
+
const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
|
|
9
|
+
const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
|
|
10
|
+
if (binding.type !== AST_NODE_TYPES.Identifier)
|
|
11
|
+
continue;
|
|
12
|
+
props.add(binding);
|
|
13
|
+
if (property.type === AST_NODE_TYPES.RestElement)
|
|
14
|
+
objects.add(binding);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
5
17
|
/** The first parameter's bindings: each prop it destructures, and the props object itself or a rest of it. */
|
|
6
18
|
const propertyBindings = (parameter) => {
|
|
7
19
|
const props = new Set();
|
|
@@ -11,17 +23,8 @@ const propertyBindings = (parameter) => {
|
|
|
11
23
|
props.add(target);
|
|
12
24
|
objects.add(target);
|
|
13
25
|
}
|
|
14
|
-
if (target?.type === AST_NODE_TYPES.ObjectPattern)
|
|
15
|
-
|
|
16
|
-
const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
|
|
17
|
-
const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
|
|
18
|
-
if (binding.type !== AST_NODE_TYPES.Identifier)
|
|
19
|
-
continue;
|
|
20
|
-
props.add(binding);
|
|
21
|
-
if (property.type === AST_NODE_TYPES.RestElement)
|
|
22
|
-
objects.add(binding);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
26
|
+
if (target?.type === AST_NODE_TYPES.ObjectPattern)
|
|
27
|
+
addPatternBindings(target, props, objects);
|
|
25
28
|
return { props, objects };
|
|
26
29
|
};
|
|
27
30
|
/**
|
|
@@ -53,7 +56,7 @@ const noCallbackPayload = createRule({
|
|
|
53
56
|
payload: "`{{ callback }}` is called with values `{{ component }}` only received from its parent. Let the parent bind them where it passes `{{ callback }}`, and call it with what this component itself knows (or nothing), so the component stays free of the payload's domain shape.",
|
|
54
57
|
},
|
|
55
58
|
},
|
|
56
|
-
create(context) {
|
|
59
|
+
create: (context) => {
|
|
57
60
|
const { sourceCode } = context;
|
|
58
61
|
const exported = new Set();
|
|
59
62
|
const pending = [];
|
|
@@ -173,7 +176,7 @@ const noCallbackPayload = createRule({
|
|
|
173
176
|
ArrowFunctionExpression: check,
|
|
174
177
|
FunctionDeclaration: check,
|
|
175
178
|
FunctionExpression: check,
|
|
176
|
-
ExportDefaultDeclaration(node) {
|
|
179
|
+
ExportDefaultDeclaration: (node) => {
|
|
177
180
|
// `export default memo(Row)` exports `Row`, however many wrappers it is under.
|
|
178
181
|
let declaration = node.declaration;
|
|
179
182
|
while (isWrapperCall(declaration))
|
|
@@ -187,7 +190,7 @@ const noCallbackPayload = createRule({
|
|
|
187
190
|
exported.add(declaration.id.name);
|
|
188
191
|
}
|
|
189
192
|
},
|
|
190
|
-
ExportNamedDeclaration(node) {
|
|
193
|
+
ExportNamedDeclaration: (node) => {
|
|
191
194
|
for (const specifier of node.specifiers) {
|
|
192
195
|
if (specifier.local.type === AST_NODE_TYPES.Identifier)
|
|
193
196
|
exported.add(specifier.local.name);
|
|
@@ -202,7 +205,7 @@ const noCallbackPayload = createRule({
|
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
},
|
|
205
|
-
"Program:exit"() {
|
|
208
|
+
"Program:exit": () => {
|
|
206
209
|
for (const { node, data } of pending) {
|
|
207
210
|
if (exported.has(data.component))
|
|
208
211
|
context.report({ node, messageId: "payload", data });
|
|
@@ -63,7 +63,45 @@ const noMarkupVariable = createRule({
|
|
|
63
63
|
inline: "`{{ name }}` holds JSX that `{{ component }}` reads only once. Write it where it is rendered.",
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
|
-
create(context) {
|
|
66
|
+
create: (context) => {
|
|
67
|
+
/** Read once, never reassigned, and that read is a JSX child rather than an attribute value. */
|
|
68
|
+
const isRenderedOnceInline = (declarator) => {
|
|
69
|
+
const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
|
|
70
|
+
const references = variable?.references ?? [];
|
|
71
|
+
const reads = references.filter((ref) => ref.isRead());
|
|
72
|
+
const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
|
|
73
|
+
const container = reads.at(0)?.identifier.parent;
|
|
74
|
+
return (reads.length === 1 &&
|
|
75
|
+
!isReassigned &&
|
|
76
|
+
container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
77
|
+
container.parent.type !== AST_NODE_TYPES.JSXAttribute);
|
|
78
|
+
};
|
|
79
|
+
const checkDeclarator = (declarator, component) => {
|
|
80
|
+
if (declarator.id.type !== AST_NODE_TYPES.Identifier)
|
|
81
|
+
return;
|
|
82
|
+
const jsx = drawnJsx(declarator.init);
|
|
83
|
+
if (jsx.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
if (jsx.some(hasStyledElement)) {
|
|
86
|
+
context.report({
|
|
87
|
+
node: declarator.id,
|
|
88
|
+
messageId: "extract",
|
|
89
|
+
data: {
|
|
90
|
+
name: declarator.id.name,
|
|
91
|
+
component,
|
|
92
|
+
suggestion: `${component}${toComponentName(declarator.id.name)}`,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isRenderedOnceInline(declarator)) {
|
|
98
|
+
context.report({
|
|
99
|
+
node: declarator.id,
|
|
100
|
+
messageId: "inline",
|
|
101
|
+
data: { name: declarator.id.name, component },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
67
105
|
const check = (fn) => {
|
|
68
106
|
const component = componentName(fn);
|
|
69
107
|
if (!component || fn.body.type !== AST_NODE_TYPES.BlockStatement)
|
|
@@ -71,40 +109,8 @@ const noMarkupVariable = createRule({
|
|
|
71
109
|
for (const statement of fn.body.body) {
|
|
72
110
|
if (statement.type !== AST_NODE_TYPES.VariableDeclaration)
|
|
73
111
|
continue;
|
|
74
|
-
for (const declarator of statement.declarations)
|
|
75
|
-
|
|
76
|
-
continue;
|
|
77
|
-
const jsx = drawnJsx(declarator.init);
|
|
78
|
-
if (jsx.length === 0)
|
|
79
|
-
continue;
|
|
80
|
-
if (!jsx.some(hasStyledElement)) {
|
|
81
|
-
const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
|
|
82
|
-
const references = variable?.references ?? [];
|
|
83
|
-
const reads = references.filter((ref) => ref.isRead());
|
|
84
|
-
const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
|
|
85
|
-
const container = reads.at(0)?.identifier.parent;
|
|
86
|
-
if (reads.length === 1 &&
|
|
87
|
-
!isReassigned &&
|
|
88
|
-
container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
|
|
89
|
-
container.parent.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
90
|
-
context.report({
|
|
91
|
-
node: declarator.id,
|
|
92
|
-
messageId: "inline",
|
|
93
|
-
data: { name: declarator.id.name, component },
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
context.report({
|
|
99
|
-
node: declarator.id,
|
|
100
|
-
messageId: "extract",
|
|
101
|
-
data: {
|
|
102
|
-
name: declarator.id.name,
|
|
103
|
-
component,
|
|
104
|
-
suggestion: `${component}${toComponentName(declarator.id.name)}`,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
}
|
|
112
|
+
for (const declarator of statement.declarations)
|
|
113
|
+
checkDeclarator(declarator, component);
|
|
108
114
|
}
|
|
109
115
|
};
|
|
110
116
|
return {
|
|
@@ -84,10 +84,10 @@ const noUnnecessaryUseCallback = createRule({
|
|
|
84
84
|
unnecessary: "This callback captures no render-local values. Use an existing function or define it at module scope, unless a proven per-instance identity contract requires memoization; explain that contract in a scoped suppression.",
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
-
create(context) {
|
|
87
|
+
create: (context) => {
|
|
88
88
|
const { sourceCode } = context;
|
|
89
89
|
return {
|
|
90
|
-
CallExpression(node) {
|
|
90
|
+
CallExpression: (node) => {
|
|
91
91
|
if (!isUseCallback(sourceCode, node.callee))
|
|
92
92
|
return;
|
|
93
93
|
const [callback, dependencies] = [node.arguments.at(0), node.arguments.at(1)];
|
|
@@ -64,7 +64,7 @@ export default createRule({
|
|
|
64
64
|
rename: "Rename `{{name}}` to end in `_PATTERN` (SCREAMING_SNAKE_CASE), so the name says it is a regular expression.",
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
-
create(context) {
|
|
67
|
+
create: (context) => {
|
|
68
68
|
const check = (node) => {
|
|
69
69
|
if (!isRegex(node))
|
|
70
70
|
return;
|