@hypersolid-dev/eslint-config 5.0.0 → 7.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 +39 -0
- package/README.md +37 -21
- package/dist/base.js +19 -6
- package/dist/jest.d.ts +3 -0
- package/dist/jest.js +56 -0
- package/dist/plugins/{hypersolid.d.ts → base/index.d.ts} +1 -0
- package/dist/plugins/base/index.js +15 -0
- package/dist/plugins/{rules → base/rules}/comment-style.js +45 -36
- package/dist/plugins/{rules → base/rules}/regex-pattern-const.js +2 -2
- package/dist/plugins/{rules → base/rules}/undefined-check.js +4 -4
- package/dist/plugins/react/index.d.ts +4 -0
- package/dist/plugins/react/index.js +23 -0
- package/dist/plugins/{rules → react/rules}/boolean-guard.js +5 -5
- package/dist/plugins/{rules → react/rules}/effect-takes-callback.js +2 -2
- package/dist/plugins/{rules → react/rules}/jsdoc-for-props.js +3 -3
- package/dist/plugins/{rules → react/rules}/no-callback-payload.js +20 -17
- package/dist/plugins/{rules → react/rules}/no-markup-variable.js +43 -37
- package/dist/plugins/{rules → react/rules}/no-unnecessary-use-callback.js +3 -3
- package/dist/plugins/react/rules/props-destructuring.d.ts +3 -0
- package/dist/plugins/{props-destructuring.js → react/rules/props-destructuring.js} +13 -17
- package/dist/plugins/{react-native-a11y → react-native/a11y}/index.js +1 -1
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-actions-need-handler.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-hint.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-label.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-role.js +3 -3
- package/dist/plugins/react-native/a11y/rules/accessibility-value-needs-range.js +47 -0
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/adjustable-needs-value.js +17 -19
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/gate-announce-for-accessibility.js +5 -5
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/input-purpose.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-aria-legacy-mix.js +21 -23
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-deprecated-accessibility-focus.js +13 -15
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-dropped-aria-on-text.js +3 -3
- package/dist/plugins/react-native/a11y/rules/no-font-scaling-disabled.js +47 -0
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-ineffective-accessibility-props.js +35 -37
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-invalid-accessibility-state.js +32 -34
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-nested-focus-stops.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-press-in-activation.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-unnamed-interactive.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-invert-colors-opt-out.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-modal-a11y-on-sheets.js +3 -3
- package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-amount-of-text-children.js +15 -13
- package/dist/plugins/{props-destructuring.d.ts → react-native/index.d.ts} +1 -0
- package/dist/plugins/react-native/index.js +17 -0
- package/dist/plugins/{rules → react-native/rules}/test-id-default.js +24 -28
- package/dist/plugins/{rules → react-native/rules}/test-id-format.js +16 -20
- package/dist/plugins/{rules → react-native/rules}/test-id-from-props.js +25 -29
- package/dist/plugins/{rules → react-native/rules}/webview-allow-list.js +24 -28
- package/dist/plugins/{utils → shared}/expressions.d.ts +3 -3
- package/dist/plugins/{utils → shared}/expressions.js +94 -78
- package/dist/plugins/{utils → shared}/types.js +14 -12
- package/dist/react-native.js +37 -8
- package/dist/shared.d.ts +3 -3
- package/dist/shared.js +169 -113
- package/package.json +12 -2
- package/dist/plugins/hypersolid.js +0 -38
- package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +0 -49
- package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +0 -49
- package/dist/plugins/react-native-a11y/utils/create-rule.js +0 -3
- package/dist/plugins/utils/create-rule.d.ts +0 -5
- /package/dist/plugins/{rules → base/rules}/comment-style.d.ts +0 -0
- /package/dist/plugins/{rules → base/rules}/regex-pattern-const.d.ts +0 -0
- /package/dist/plugins/{rules → base/rules}/undefined-check.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/boolean-guard.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/effect-takes-callback.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/jsdoc-for-props.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/no-callback-payload.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/no-markup-variable.d.ts +0 -0
- /package/dist/plugins/{rules → react/rules}/no-unnecessary-use-callback.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-props.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-props.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-state.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/accessibility-state.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/interactive.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/interactive.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/react-native-components.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/react-native-components.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/schema-properties.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/constants/schema-properties.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/index.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/models/optional-options.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/models/optional-options.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-actions-need-handler.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-hint.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-label.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-role.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/accessibility-value-needs-range.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/adjustable-needs-value.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/gate-announce-for-accessibility.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/input-purpose.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-aria-legacy-mix.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-deprecated-accessibility-focus.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-dropped-aria-on-text.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-font-scaling-disabled.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-ineffective-accessibility-props.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-invalid-accessibility-state.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-nested-focus-stops.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-press-in-activation.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/no-unnamed-interactive.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-invert-colors-opt-out.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/rules/require-modal-a11y-on-sheets.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-amount-of-text-children.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-components.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-components.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-element-name.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-element-name.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-jsx-descendants.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-jsx-descendants.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-property-value.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-property-value.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-text-child.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-literal-text-child.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-property.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/get-property.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-accessible-name.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-accessible-name.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-property.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-property.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-spread-props.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/has-spread-props.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-definitely-grouped.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-definitely-grouped.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-hidden-from-accessibility.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-hidden-from-accessibility.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-inside-focus-stop.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-inside-focus-stop.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-interactive.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-interactive.js +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-not-a-focus-stop.d.ts +0 -0
- /package/dist/plugins/{react-native-a11y → react-native/a11y}/utils/is-not-a-focus-stop.js +0 -0
- /package/dist/plugins/{rules → react-native/rules}/test-id-default.d.ts +0 -0
- /package/dist/plugins/{rules → react-native/rules}/test-id-format.d.ts +0 -0
- /package/dist/plugins/{rules → react-native/rules}/test-id-from-props.d.ts +0 -0
- /package/dist/plugins/{rules → react-native/rules}/webview-allow-list.d.ts +0 -0
- /package/dist/plugins/{utils → shared}/comments.d.ts +0 -0
- /package/dist/plugins/{utils → shared}/comments.js +0 -0
- /package/dist/plugins/{utils → shared}/component-name.d.ts +0 -0
- /package/dist/plugins/{utils → shared}/component-name.js +0 -0
- /package/dist/plugins/{react-native-a11y/utils → shared}/create-rule.d.ts +0 -0
- /package/dist/plugins/{utils → shared}/create-rule.js +0 -0
- /package/dist/plugins/{utils → shared}/types.d.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import createRule from '
|
|
3
|
-
import { leavesOf } from '
|
|
2
|
+
import createRule from '../../shared/create-rule.js';
|
|
3
|
+
import { leavesOf } from '../../shared/expressions.js';
|
|
4
4
|
/**
|
|
5
5
|
* A component's `testID` comes from its props, never a string it writes itself.
|
|
6
6
|
*
|
|
@@ -23,11 +23,9 @@ import { leavesOf } from '../utils/expressions.js';
|
|
|
23
23
|
*/
|
|
24
24
|
const TEST_ID = 'testID';
|
|
25
25
|
/** A value spelled out where it stands, rather than reached for. */
|
|
26
|
-
|
|
27
|
-
return (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') || (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0);
|
|
28
|
-
}
|
|
26
|
+
const isWritten = (node) => (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') || (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0);
|
|
29
27
|
/** Whether an expression reaches outside itself for its value. */
|
|
30
|
-
|
|
28
|
+
const isReadingAName = (context, node) => {
|
|
31
29
|
switch (node.type) {
|
|
32
30
|
// A local name bound to a written string is that string; anything unresolved is trusted.
|
|
33
31
|
case AST_NODE_TYPES.Identifier:
|
|
@@ -41,26 +39,26 @@ function readsAName(context, node) {
|
|
|
41
39
|
case AST_NODE_TYPES.TemplateLiteral:
|
|
42
40
|
return node.expressions.length > 0 && node.quasis[0]?.value.cooked === '';
|
|
43
41
|
case AST_NODE_TYPES.ConditionalExpression:
|
|
44
|
-
return
|
|
42
|
+
return isReadingAName(context, node.consequent) || isReadingAName(context, node.alternate);
|
|
45
43
|
case AST_NODE_TYPES.LogicalExpression:
|
|
46
|
-
return
|
|
44
|
+
return isReadingAName(context, node.left) || isReadingAName(context, node.right);
|
|
47
45
|
case AST_NODE_TYPES.TSAsExpression:
|
|
48
46
|
case AST_NODE_TYPES.TSSatisfiesExpression:
|
|
49
47
|
case AST_NODE_TYPES.TSNonNullExpression:
|
|
50
|
-
return
|
|
48
|
+
return isReadingAName(context, node.expression);
|
|
51
49
|
default:
|
|
52
50
|
return false;
|
|
53
51
|
}
|
|
54
|
-
}
|
|
52
|
+
};
|
|
55
53
|
/** How to name the offending id: a name is followed to the string behind it. */
|
|
56
|
-
|
|
54
|
+
const writtenValue = (context, node) => {
|
|
57
55
|
if (node?.type === AST_NODE_TYPES.Literal && typeof node.value === 'string')
|
|
58
56
|
return `"${node.value}"`;
|
|
59
57
|
const strings = (node ? leavesOf(context, node) : [])
|
|
60
58
|
.filter(isWritten)
|
|
61
59
|
.map((leaf) => (leaf.type === AST_NODE_TYPES.Literal ? leaf.value : leaf.quasis[0]?.value.cooked));
|
|
62
60
|
return strings.length === 1 ? `"${String(strings[0])}"` : 'a fixed value';
|
|
63
|
-
}
|
|
61
|
+
};
|
|
64
62
|
const testIdFromProps = createRule({
|
|
65
63
|
name: 'test-id-from-props',
|
|
66
64
|
meta: {
|
|
@@ -73,22 +71,20 @@ const testIdFromProps = createRule({
|
|
|
73
71
|
prefixed: 'Start this `testID` from the `testID` prop rather than the written "{{ prefix }}": what follows it may differ per instance, but the prefix spells out a name the prop already carries, so a caller cannot scope or rename it. Write `` `${testID}-…` ``.',
|
|
74
72
|
},
|
|
75
73
|
},
|
|
76
|
-
create(context) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
},
|
|
74
|
+
create: (context) => ({
|
|
75
|
+
JSXAttribute: (node) => {
|
|
76
|
+
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== TEST_ID)
|
|
77
|
+
return;
|
|
78
|
+
const { value } = node;
|
|
79
|
+
const expression = value?.type === AST_NODE_TYPES.JSXExpressionContainer ? value.expression : (value ?? undefined);
|
|
80
|
+
if (value?.type === AST_NODE_TYPES.JSXExpressionContainer && isReadingAName(context, value.expression))
|
|
81
|
+
return;
|
|
82
|
+
if (expression?.type === AST_NODE_TYPES.TemplateLiteral && expression.expressions.length > 0) {
|
|
83
|
+
context.report({ node, messageId: 'prefixed', data: { prefix: expression.quasis[0]?.value.cooked } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
context.report({ node, messageId: 'static', data: { value: writtenValue(context, expression) } });
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
93
89
|
});
|
|
94
90
|
export default testIdFromProps;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
import createRule from '
|
|
3
|
-
import { unwrap } from '
|
|
2
|
+
import createRule from '../../shared/create-rule.js';
|
|
3
|
+
import { unwrap } from '../../shared/expressions.js';
|
|
4
4
|
/**
|
|
5
5
|
* A `react-native-webview` `WebView` sends every navigation through its
|
|
6
6
|
* `onShouldStartLoadWithRequest`, so that handler is the allow-list.
|
|
@@ -23,7 +23,7 @@ const SOURCE = 'react-native-webview';
|
|
|
23
23
|
const WEBVIEW = 'WebView';
|
|
24
24
|
const HANDLER = 'onShouldStartLoadWithRequest';
|
|
25
25
|
/** Whether a JSX name resolves to the library's `WebView` import. */
|
|
26
|
-
|
|
26
|
+
const isWebView = (context, name) => {
|
|
27
27
|
const identifier = name.type === AST_NODE_TYPES.JSXMemberExpression ? name.object : name;
|
|
28
28
|
if (identifier.type !== AST_NODE_TYPES.JSXIdentifier)
|
|
29
29
|
return false;
|
|
@@ -44,9 +44,9 @@ function isWebView(context, name) {
|
|
|
44
44
|
return false;
|
|
45
45
|
const imported = node.imported.type === AST_NODE_TYPES.Identifier ? node.imported.name : node.imported.value;
|
|
46
46
|
return imported === WEBVIEW;
|
|
47
|
-
}
|
|
47
|
+
};
|
|
48
48
|
/** `undefined`, `null`, or a branch that can be one: the prop is set and still absent. */
|
|
49
|
-
|
|
49
|
+
const canBeAbsent = (expression) => {
|
|
50
50
|
switch (expression.type) {
|
|
51
51
|
case AST_NODE_TYPES.JSXEmptyExpression:
|
|
52
52
|
return true;
|
|
@@ -61,18 +61,16 @@ function canBeAbsent(expression) {
|
|
|
61
61
|
default:
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return opening.attributes.find((item) => item.type === AST_NODE_TYPES.JSXAttribute && item.name.name === name);
|
|
67
|
-
}
|
|
64
|
+
};
|
|
65
|
+
const attribute = (opening, name) => opening.attributes.find((item) => item.type === AST_NODE_TYPES.JSXAttribute && item.name.name === name);
|
|
68
66
|
/** Whether the prop's value is an inline array literal holding the string `"*"`. */
|
|
69
|
-
|
|
67
|
+
const isHoldingWildcard = (property) => {
|
|
70
68
|
const expression = unwrap(property.value?.type === AST_NODE_TYPES.JSXExpressionContainer ? property.value.expression : undefined);
|
|
71
69
|
if (expression?.type !== AST_NODE_TYPES.ArrayExpression)
|
|
72
70
|
return false;
|
|
73
71
|
return expression.elements.some((element) => (element?.type === AST_NODE_TYPES.Literal && element.value === '*') ||
|
|
74
72
|
(element?.type === AST_NODE_TYPES.TemplateLiteral && element.expressions.length === 0 && element.quasis[0]?.value.cooked === '*'));
|
|
75
|
-
}
|
|
73
|
+
};
|
|
76
74
|
const webviewAllowList = createRule({
|
|
77
75
|
name: 'webview-allow-list',
|
|
78
76
|
meta: {
|
|
@@ -88,22 +86,20 @@ const webviewAllowList = createRule({
|
|
|
88
86
|
narrowWhitelist: 'Make `originWhitelist` the inline `["*"]`: a URL outside the list skips `{{ handler }}` and the library opens it in another app, so the handler is the only allow-list that holds.',
|
|
89
87
|
},
|
|
90
88
|
},
|
|
91
|
-
create(context) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
};
|
|
107
|
-
},
|
|
89
|
+
create: (context) => ({
|
|
90
|
+
JSXOpeningElement: (node) => {
|
|
91
|
+
if (!isWebView(context, node.name))
|
|
92
|
+
return;
|
|
93
|
+
const handler = attribute(node, HANDLER);
|
|
94
|
+
if (handler?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer || canBeAbsent(handler.value.expression)) {
|
|
95
|
+
context.report({ node, messageId: 'missingHandler', data: { handler: HANDLER } });
|
|
96
|
+
}
|
|
97
|
+
const whitelist = attribute(node, 'originWhitelist');
|
|
98
|
+
if (!whitelist)
|
|
99
|
+
context.report({ node, messageId: 'missingWhitelist', data: { handler: HANDLER } });
|
|
100
|
+
else if (!isHoldingWildcard(whitelist))
|
|
101
|
+
context.report({ node: whitelist, messageId: 'narrowWhitelist', data: { handler: HANDLER } });
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
108
104
|
});
|
|
109
105
|
export default webviewAllowList;
|
|
@@ -28,13 +28,13 @@ export type LeavesOptions = {
|
|
|
28
28
|
seen?: Set<Node>;
|
|
29
29
|
};
|
|
30
30
|
/** Through `as`, `satisfies`, `!` and `?.`. */
|
|
31
|
-
export declare
|
|
31
|
+
export declare const unwrap: (node: Node | null | undefined) => Node | undefined;
|
|
32
32
|
/**
|
|
33
33
|
* A property's key when it is written literally (`a`, `"a"`, `1`, `["a"]`,
|
|
34
34
|
* `[\`a\`]`), else null: a computed key that is a variable, a call or a
|
|
35
35
|
* template with expressions, a spread and a pattern have no key to read.
|
|
36
36
|
*/
|
|
37
|
-
export declare
|
|
37
|
+
export declare const keyOf: (property: Node) => string | null;
|
|
38
38
|
/**
|
|
39
39
|
* Every leaf an expression can produce.
|
|
40
40
|
*
|
|
@@ -48,5 +48,5 @@ export declare function keyOf(property: Node): string | null;
|
|
|
48
48
|
* itself: a name from another file, a `let`, a method call, a computed member,
|
|
49
49
|
* a spread. A cycle yields nothing rather than looping.
|
|
50
50
|
*/
|
|
51
|
-
export declare
|
|
51
|
+
export declare const leavesOf: (context: ScopeContext, node: Node | null | undefined, options?: LeavesOptions) => Node[];
|
|
52
52
|
export {};
|
|
@@ -8,14 +8,14 @@ const DEFAULT_SELECT_OBJECTS = new Set(['Platform']);
|
|
|
8
8
|
const isWrapper = (node) => TS_WRAPPERS.has(node.type);
|
|
9
9
|
const isFunction = (node) => FUNCTIONS.has(node.type);
|
|
10
10
|
/** Through `as`, `satisfies`, `!` and `?.`. */
|
|
11
|
-
export
|
|
11
|
+
export const unwrap = (node) => {
|
|
12
12
|
let current = node ?? undefined;
|
|
13
13
|
while (current && isWrapper(current))
|
|
14
14
|
current = current.expression;
|
|
15
15
|
return current;
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
17
|
/** The name of the memo hook a callee calls (`useMemo`, `React.useMemo`), if it is one. */
|
|
18
|
-
|
|
18
|
+
const memoHookName = (callee) => {
|
|
19
19
|
if (callee.type === AST_NODE_TYPES.Identifier)
|
|
20
20
|
return MEMO_HOOKS.has(callee.name) ? callee.name : undefined;
|
|
21
21
|
if (callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
@@ -26,29 +26,27 @@ function memoHookName(callee) {
|
|
|
26
26
|
return callee.property.name;
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
29
|
+
};
|
|
30
|
+
const findVariable = (scope, name) => {
|
|
31
31
|
for (let current = scope; current; current = current.upper) {
|
|
32
32
|
const variable = current.set.get(name);
|
|
33
33
|
if (variable)
|
|
34
34
|
return variable;
|
|
35
35
|
}
|
|
36
36
|
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
40
|
-
}
|
|
37
|
+
};
|
|
38
|
+
const bindingOf = (context, identifier) => findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
41
39
|
/** The declarator a name resolves to, when it is a local with an initialiser; `.init` need not be the name's own value. */
|
|
42
|
-
|
|
40
|
+
const localDeclarator = (context, identifier) => {
|
|
43
41
|
const definition = bindingOf(context, identifier)?.defs[0];
|
|
44
42
|
return definition?.type === DefinitionType.Variable && definition.node.init ? definition.node : undefined;
|
|
45
|
-
}
|
|
43
|
+
};
|
|
46
44
|
/**
|
|
47
45
|
* A property's key when it is written literally (`a`, `"a"`, `1`, `["a"]`,
|
|
48
46
|
* `[\`a\`]`), else null: a computed key that is a variable, a call or a
|
|
49
47
|
* template with expressions, a spread and a pattern have no key to read.
|
|
50
48
|
*/
|
|
51
|
-
export
|
|
49
|
+
export const keyOf = (property) => {
|
|
52
50
|
if (property.type !== AST_NODE_TYPES.Property)
|
|
53
51
|
return null;
|
|
54
52
|
const { key } = property;
|
|
@@ -57,13 +55,13 @@ export function keyOf(property) {
|
|
|
57
55
|
if (key.type === AST_NODE_TYPES.Literal)
|
|
58
56
|
return String(key.value);
|
|
59
57
|
return key.type === AST_NODE_TYPES.TemplateLiteral && key.expressions.length === 0 ? (key.quasis[0]?.value.cooked ?? null) : null;
|
|
60
|
-
}
|
|
58
|
+
};
|
|
61
59
|
/**
|
|
62
60
|
* The expression a destructuring pattern hands to `name`, read off a literal
|
|
63
61
|
* initialiser. A non-literal value, a rest element, a computed key and a spread
|
|
64
62
|
* before the wanted element all give up, since the value is not in this file.
|
|
65
63
|
*/
|
|
66
|
-
|
|
64
|
+
const picked = (pattern, init, name) => {
|
|
67
65
|
if (pattern.type === AST_NODE_TYPES.Identifier)
|
|
68
66
|
return pattern.name === name ? (init ?? undefined) : undefined;
|
|
69
67
|
// The default only applies when the source has nothing there.
|
|
@@ -71,37 +69,44 @@ function picked(pattern, init, name) {
|
|
|
71
69
|
return picked(pattern.left, init ?? pattern.right, name);
|
|
72
70
|
const source = unwrap(init);
|
|
73
71
|
if (pattern.type === AST_NODE_TYPES.ObjectPattern && source?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
74
|
-
|
|
75
|
-
const wanted = keyOf(property);
|
|
76
|
-
if (wanted === null || property.type !== AST_NODE_TYPES.Property)
|
|
77
|
-
continue;
|
|
78
|
-
const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
|
|
79
|
-
const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
|
|
80
|
-
if (found)
|
|
81
|
-
return found;
|
|
82
|
-
}
|
|
83
|
-
return undefined;
|
|
72
|
+
return pickedFromObject(pattern, source, name);
|
|
84
73
|
}
|
|
85
74
|
if (pattern.type === AST_NODE_TYPES.ArrayPattern && source?.type === AST_NODE_TYPES.ArrayExpression) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
75
|
+
return pickedFromArray(pattern, source, name);
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
const pickedFromObject = (pattern, source, name) => {
|
|
80
|
+
for (const property of pattern.properties) {
|
|
81
|
+
const wanted = keyOf(property);
|
|
82
|
+
if (wanted === null || property.type !== AST_NODE_TYPES.Property)
|
|
83
|
+
continue;
|
|
84
|
+
const entry = source.properties.find((candidate) => keyOf(candidate) === wanted);
|
|
85
|
+
const found = picked(property.value, entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined, name);
|
|
86
|
+
if (found)
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
};
|
|
91
|
+
const pickedFromArray = (pattern, source, name) => {
|
|
92
|
+
for (const [index, element] of pattern.elements.entries()) {
|
|
93
|
+
if (!element)
|
|
94
|
+
continue;
|
|
95
|
+
const before = source.elements.slice(0, index + 1);
|
|
96
|
+
if (before.some((entry) => entry?.type === AST_NODE_TYPES.SpreadElement))
|
|
97
|
+
continue;
|
|
98
|
+
const found = picked(element, source.elements[index], name);
|
|
99
|
+
if (found)
|
|
100
|
+
return found;
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
|
-
}
|
|
103
|
+
};
|
|
99
104
|
/**
|
|
100
105
|
* What a name stands for: a `const`'s own value, or a declared function itself.
|
|
101
106
|
* A `let` or `var` is not followed, and a destructured `const` resolves only
|
|
102
107
|
* when its initialiser is a literal holding the matching entry.
|
|
103
108
|
*/
|
|
104
|
-
|
|
109
|
+
const boundTo = (context, identifier) => {
|
|
105
110
|
const definition = bindingOf(context, identifier)?.defs[0];
|
|
106
111
|
if (definition?.type === DefinitionType.FunctionName)
|
|
107
112
|
return definition.node;
|
|
@@ -111,17 +116,17 @@ function boundTo(context, identifier) {
|
|
|
111
116
|
return undefined;
|
|
112
117
|
const { id, init } = definition.node;
|
|
113
118
|
return id.type === AST_NODE_TYPES.Identifier ? init : picked(id, init, identifier.name);
|
|
114
|
-
}
|
|
115
|
-
|
|
119
|
+
};
|
|
120
|
+
const selectBranches = (node, selectObjects) => {
|
|
116
121
|
const { callee } = node;
|
|
117
122
|
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed)
|
|
118
123
|
return undefined;
|
|
119
124
|
if (callee.property.name !== 'select' || callee.object.type !== AST_NODE_TYPES.Identifier)
|
|
120
125
|
return undefined;
|
|
121
126
|
return selectObjects.has(callee.object.name) ? node.arguments[0] : undefined;
|
|
122
|
-
}
|
|
127
|
+
};
|
|
123
128
|
/** Every `return` with an argument in a body, including nested blocks but not nested functions. */
|
|
124
|
-
|
|
129
|
+
const returnsIn = (body) => {
|
|
125
130
|
const found = [];
|
|
126
131
|
const visit = (value) => {
|
|
127
132
|
if (Array.isArray(value)) {
|
|
@@ -146,7 +151,7 @@ function returnsIn(body) {
|
|
|
146
151
|
};
|
|
147
152
|
visit(body);
|
|
148
153
|
return found;
|
|
149
|
-
}
|
|
154
|
+
};
|
|
150
155
|
/**
|
|
151
156
|
* Every leaf an expression can produce.
|
|
152
157
|
*
|
|
@@ -160,7 +165,7 @@ function returnsIn(body) {
|
|
|
160
165
|
* itself: a name from another file, a `let`, a method call, a computed member,
|
|
161
166
|
* a spread. A cycle yields nothing rather than looping.
|
|
162
167
|
*/
|
|
163
|
-
export
|
|
168
|
+
export const leavesOf = (context, node, options = {}) => {
|
|
164
169
|
const { names = true, members = true, returns = false, selectObjects = DEFAULT_SELECT_OBJECTS, calls, seen = new Set(), } = options;
|
|
165
170
|
const out = [];
|
|
166
171
|
const walkFunction = (fn) => {
|
|
@@ -171,31 +176,38 @@ export function leavesOf(context, node, options = {}) {
|
|
|
171
176
|
for (const argument of returnsIn(fn.body))
|
|
172
177
|
walk(argument);
|
|
173
178
|
};
|
|
179
|
+
/** Each branch is a value for whatever slot the call sits in. */
|
|
180
|
+
const walkSelect = (branches) => {
|
|
181
|
+
for (const branch of leavesOf(context, branches, { ...options, seen })) {
|
|
182
|
+
if (branch.type !== AST_NODE_TYPES.ObjectExpression)
|
|
183
|
+
continue;
|
|
184
|
+
for (const property of branch.properties) {
|
|
185
|
+
if (property.type === AST_NODE_TYPES.Property)
|
|
186
|
+
walk(property.value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/** A call to a local function is what it returns (a factory); a method call is never followed. */
|
|
191
|
+
const factoryOf = (call) => {
|
|
192
|
+
if (!returns || call.callee.type !== AST_NODE_TYPES.Identifier)
|
|
193
|
+
return undefined;
|
|
194
|
+
const bound = boundTo(context, call.callee);
|
|
195
|
+
return bound && isFunction(bound) ? bound : undefined;
|
|
196
|
+
};
|
|
174
197
|
const didWalkCall = (call) => {
|
|
175
198
|
const branches = selectBranches(call, selectObjects);
|
|
176
199
|
if (branches) {
|
|
177
|
-
|
|
178
|
-
for (const branch of leavesOf(context, branches, { ...options, seen })) {
|
|
179
|
-
if (branch.type !== AST_NODE_TYPES.ObjectExpression)
|
|
180
|
-
continue;
|
|
181
|
-
for (const property of branch.properties) {
|
|
182
|
-
if (property.type === AST_NODE_TYPES.Property)
|
|
183
|
-
walk(property.value);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
200
|
+
walkSelect(branches);
|
|
186
201
|
return true;
|
|
187
202
|
}
|
|
188
203
|
if (calls?.(call)) {
|
|
189
204
|
walk(call.arguments[0]);
|
|
190
205
|
return true;
|
|
191
206
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
walk(bound);
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
207
|
+
const factory = factoryOf(call);
|
|
208
|
+
if (factory) {
|
|
209
|
+
walk(factory);
|
|
210
|
+
return true;
|
|
199
211
|
}
|
|
200
212
|
const hook = memoHookName(call.callee);
|
|
201
213
|
const callback = call.arguments.at(0);
|
|
@@ -208,6 +220,25 @@ export function leavesOf(context, node, options = {}) {
|
|
|
208
220
|
walkFunction(callback);
|
|
209
221
|
return true;
|
|
210
222
|
};
|
|
223
|
+
/** `OBJ.key`'s value when `OBJ` is a local `const` object literal. */
|
|
224
|
+
const memberEntry = (member) => {
|
|
225
|
+
if (member.computed || member.object.type !== AST_NODE_TYPES.Identifier)
|
|
226
|
+
return undefined;
|
|
227
|
+
const declarator = localDeclarator(context, member.object);
|
|
228
|
+
// A destructured name is not what the whole initialiser holds.
|
|
229
|
+
const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
|
|
230
|
+
if (held?.type !== AST_NODE_TYPES.ObjectExpression)
|
|
231
|
+
return undefined;
|
|
232
|
+
const key = member.property.name;
|
|
233
|
+
const entry = held.properties.find((property) => keyOf(property) === key);
|
|
234
|
+
return entry?.type === AST_NODE_TYPES.Property ? entry.value : undefined;
|
|
235
|
+
};
|
|
236
|
+
/** What a name or `OBJ.key` stands for, when the options let it be followed. */
|
|
237
|
+
const referenced = (node) => {
|
|
238
|
+
if (node.type === AST_NODE_TYPES.Identifier)
|
|
239
|
+
return names ? boundTo(context, node) : undefined;
|
|
240
|
+
return members ? memberEntry(node) : undefined;
|
|
241
|
+
};
|
|
211
242
|
const walk = (current) => {
|
|
212
243
|
if (!current || seen.has(current))
|
|
213
244
|
return;
|
|
@@ -229,26 +260,11 @@ export function leavesOf(context, node, options = {}) {
|
|
|
229
260
|
walk(current.left);
|
|
230
261
|
walk(current.right);
|
|
231
262
|
return;
|
|
232
|
-
case AST_NODE_TYPES.Identifier:
|
|
233
|
-
const bound = names ? boundTo(context, current) : undefined;
|
|
234
|
-
if (bound) {
|
|
235
|
-
walk(bound);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
263
|
+
case AST_NODE_TYPES.Identifier:
|
|
240
264
|
case AST_NODE_TYPES.MemberExpression: {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// A destructured name is not what the whole initialiser holds.
|
|
245
|
-
const held = declarator?.id.type === AST_NODE_TYPES.Identifier ? unwrap(declarator.init) : undefined;
|
|
246
|
-
if (held?.type !== AST_NODE_TYPES.ObjectExpression)
|
|
247
|
-
break;
|
|
248
|
-
const key = current.property.name;
|
|
249
|
-
const entry = held.properties.find((property) => keyOf(property) === key);
|
|
250
|
-
if (entry?.type === AST_NODE_TYPES.Property) {
|
|
251
|
-
walk(entry.value);
|
|
265
|
+
const target = referenced(current);
|
|
266
|
+
if (target) {
|
|
267
|
+
walk(target);
|
|
252
268
|
return;
|
|
253
269
|
}
|
|
254
270
|
break;
|
|
@@ -271,4 +287,4 @@ export function leavesOf(context, node, options = {}) {
|
|
|
271
287
|
};
|
|
272
288
|
walk(node);
|
|
273
289
|
return out;
|
|
274
|
-
}
|
|
290
|
+
};
|
|
@@ -21,6 +21,18 @@ const UNRESOLVED = TypeFlags.TypeParameter |
|
|
|
21
21
|
TypeFlags.Conditional |
|
|
22
22
|
TypeFlags.Index |
|
|
23
23
|
TypeFlags.Substitution;
|
|
24
|
+
/**
|
|
25
|
+
* A callable or constructable type is truthy. Any other object type is only
|
|
26
|
+
* as good as what the falsy primitives do against it: `{}`, `Object` and
|
|
27
|
+
* `{ length: number }` all accept `""`, `0` or `false`.
|
|
28
|
+
*/
|
|
29
|
+
const isTruthyObject = (type, checker) => {
|
|
30
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
|
|
31
|
+
return true;
|
|
32
|
+
if (!checker)
|
|
33
|
+
return false;
|
|
34
|
+
return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
|
|
35
|
+
};
|
|
24
36
|
/**
|
|
25
37
|
* Whether every value of this type is truthy, `undefined` aside.
|
|
26
38
|
*
|
|
@@ -50,18 +62,8 @@ export const isSameAsBangBang = (type, checker) => {
|
|
|
50
62
|
if (type.flags & (TypeFlags.ESSymbolLike | TypeFlags.NonPrimitive)) {
|
|
51
63
|
return true;
|
|
52
64
|
}
|
|
53
|
-
if (type.flags & TypeFlags.Object)
|
|
54
|
-
|
|
55
|
-
* A callable or constructable type is truthy. Any other object type is only
|
|
56
|
-
* as good as what the falsy primitives do against it: `{}`, `Object` and
|
|
57
|
-
* `{ length: number }` all accept `""`, `0` or `false`.
|
|
58
|
-
*/
|
|
59
|
-
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
|
|
60
|
-
return true;
|
|
61
|
-
if (!checker)
|
|
62
|
-
return false;
|
|
63
|
-
return [checker.getStringLiteralType(""), checker.getNumberLiteralType(0), checker.getFalseType()].every((falsy) => !checker.isTypeAssignableTo(falsy, type));
|
|
64
|
-
}
|
|
65
|
+
if (type.flags & TypeFlags.Object)
|
|
66
|
+
return isTruthyObject(type, checker);
|
|
65
67
|
// A literal is as truthy as its value: only `""`, `0` and `0n` are falsy.
|
|
66
68
|
if (type.isStringLiteral())
|
|
67
69
|
return type.value !== "";
|
package/dist/react-native.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import expo from 'eslint-plugin-expo';
|
|
2
|
+
import reactDoctor from 'eslint-plugin-react-doctor';
|
|
2
3
|
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
|
|
3
4
|
import { createNodeResolver } from 'eslint-plugin-import-x';
|
|
4
5
|
import globals from 'globals';
|
|
5
6
|
import noSecrets from 'eslint-plugin-no-secrets';
|
|
6
|
-
import reactNativeA11y from './plugins/react-native
|
|
7
|
+
import reactNativeA11y from './plugins/react-native/a11y/index.js';
|
|
8
|
+
import reactNativePlugin from './plugins/react-native/index.js';
|
|
7
9
|
import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, REACT_BOOLEAN_NAME_OPTIONS, REACT_RESTRICTED_SYNTAX, reactConfigs, TEST_FILES, } from './shared.js';
|
|
8
|
-
const A11Y = '@hypersolid-dev/react-native
|
|
10
|
+
const A11Y = '@hypersolid-dev/react-native/a11y';
|
|
9
11
|
/**
|
|
10
12
|
* Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
|
|
11
13
|
* `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
|
|
@@ -44,7 +46,7 @@ export default function reactNative() {
|
|
|
44
46
|
name: '@hypersolid-dev/react-native',
|
|
45
47
|
files: CODE_FILES,
|
|
46
48
|
// eslint-plugin-expo is typed with typescript-eslint's RuleModule, which ESLint's Plugin type does not accept.
|
|
47
|
-
plugins: { expo: expo, 'no-secrets': noSecrets },
|
|
49
|
+
plugins: { '@hypersolid-dev/react-native': reactNativePlugin, expo: expo, 'no-secrets': noSecrets },
|
|
48
50
|
settings: {
|
|
49
51
|
'import-x/resolver-next': [
|
|
50
52
|
createTypeScriptImportResolver({ extensions: RESOLVE_EXTENSIONS }),
|
|
@@ -135,9 +137,9 @@ export default function reactNative() {
|
|
|
135
137
|
},
|
|
136
138
|
],
|
|
137
139
|
// `testID` is lowercase kebab-case, so test selectors match exactly. Bad: `testID="Header.Title"`.
|
|
138
|
-
'@hypersolid-dev/test-id-format': 'error',
|
|
140
|
+
'@hypersolid-dev/react-native/test-id-format': 'error',
|
|
139
141
|
// A WebView allows every origin and filters each navigation in `onShouldStartLoadWithRequest`. Bad: `<WebView source={{ uri }} />`.
|
|
140
|
-
'@hypersolid-dev/webview-allow-list': 'error',
|
|
142
|
+
'@hypersolid-dev/react-native/webview-allow-list': 'error',
|
|
141
143
|
},
|
|
142
144
|
},
|
|
143
145
|
{
|
|
@@ -147,9 +149,36 @@ export default function reactNative() {
|
|
|
147
149
|
ignores: TEST_FILES,
|
|
148
150
|
rules: {
|
|
149
151
|
// A component's `testID` comes from its props, so two instances stay apart. Bad: `<View testID="card" />` inside `Card`.
|
|
150
|
-
'@hypersolid-dev/test-id-from-props': 'error',
|
|
152
|
+
'@hypersolid-dev/react-native/test-id-from-props': 'error',
|
|
151
153
|
// The `testID` prop defaults to the component's kebab-case name. Bad: `({ testID }: Props)` in `Card` (use `testID = 'card'`).
|
|
152
|
-
'@hypersolid-dev/test-id-default': 'error',
|
|
154
|
+
'@hypersolid-dev/react-native/test-id-default': 'error',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
// React Doctor's React Native rules, as used in the ESPN Goal pilot.
|
|
159
|
+
name: '@hypersolid-dev/react-native/react-doctor',
|
|
160
|
+
files: CODE_FILES,
|
|
161
|
+
ignores: TEST_FILES,
|
|
162
|
+
plugins: { 'react-doctor': reactDoctor },
|
|
163
|
+
rules: {
|
|
164
|
+
// A state update per scroll event re-renders mid-gesture; track position in a shared value. Bad: `onScroll={(event) => setOffset(event.nativeEvent.contentOffset.y)}`.
|
|
165
|
+
'react-doctor/rn-no-scroll-state': 'error',
|
|
166
|
+
// A reaction that only copies one value into another is a `useDerivedValue`. Bad: `useAnimatedReaction(() => x.value, (value) => { y.value = value; })`.
|
|
167
|
+
'react-doctor/rn-animation-reaction-as-derived': 'error',
|
|
168
|
+
// A string outside `<Text>` crashes on native. Bad: `<View>Hello</View>`.
|
|
169
|
+
'react-doctor/rn-no-raw-text': 'error',
|
|
170
|
+
// A list keys rows from `keyExtractor`; a `key` on the row is ignored. Bad: `renderItem={({ item }) => <Row key={item.id} />}`.
|
|
171
|
+
'react-doctor/rn-no-renderitem-key': 'error',
|
|
172
|
+
// A press animation set from `onPressIn` runs on the JS thread. Bad: `onPressIn={() => { scale.value = 0.9; }}`.
|
|
173
|
+
'react-doctor/rn-pressable-shared-value-mutation': 'error',
|
|
174
|
+
// `PanResponder` runs on the JS thread; `Gesture.Pan()` runs on the UI thread. Bad: `PanResponder.create({ … })`.
|
|
175
|
+
'react-doctor/rn-no-panresponder': 'error',
|
|
176
|
+
// `Dimensions.get()` reads the size once and misses rotation and split screen. Bad: `const { width } = Dimensions.get('window')` (use `useWindowDimensions`).
|
|
177
|
+
'react-doctor/rn-no-dimensions-get': 'error',
|
|
178
|
+
// `react-native/Libraries/…` paths are private and move on upgrade. Bad: `import … from 'react-native/Libraries/Image/resolveAssetSource'`.
|
|
179
|
+
'react-doctor/rn-no-deep-imports': 'error',
|
|
180
|
+
// Unmaintained Expo-era packages each have a successor. Bad: `import { Audio } from 'expo-av'` (use `expo-audio`).
|
|
181
|
+
'react-doctor/rn-no-legacy-expo-packages': 'error',
|
|
153
182
|
},
|
|
154
183
|
},
|
|
155
184
|
{
|
|
@@ -170,7 +199,7 @@ export default function reactNative() {
|
|
|
170
199
|
},
|
|
171
200
|
},
|
|
172
201
|
{
|
|
173
|
-
// React Native accessibility, each rule citing its WCAG criterion. Docs: src/plugins/react-native
|
|
202
|
+
// React Native accessibility, each rule citing its WCAG criterion. Docs: src/plugins/react-native/a11y/docs.
|
|
174
203
|
name: '@hypersolid-dev/react-native/a11y',
|
|
175
204
|
files: CODE_FILES,
|
|
176
205
|
plugins: { [A11Y]: reactNativeA11y },
|
package/dist/shared.d.ts
CHANGED
|
@@ -31,6 +31,6 @@ export declare const NAMING_CONVENTION: ({
|
|
|
31
31
|
format: string[];
|
|
32
32
|
modifiers?: undefined;
|
|
33
33
|
})[];
|
|
34
|
-
export declare
|
|
35
|
-
export declare
|
|
36
|
-
export declare
|
|
34
|
+
export declare const raiseWarnings: (configs: Linter.Config[]) => Linter.Config[];
|
|
35
|
+
export declare const scopeToCode: (configs: readonly Linter.Config[]) => Linter.Config[];
|
|
36
|
+
export declare const reactConfigs: () => Linter.Config[];
|