@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,24 +31,20 @@ const PASCAL_CASE_PATTERN = /^[A-Z]/;
|
|
|
31
31
|
const LOWER_TO_UPPER_PATTERN = /(?<=[a-z0-9])(?=[A-Z])/g;
|
|
32
32
|
const ACRONYM_END_PATTERN = /(?<=[A-Z])(?=[A-Z][a-z])/g;
|
|
33
33
|
/** `CompetitionLogo` -> `competition-logo`, `AdSlotURLBar` -> `ad-slot-url-bar`. */
|
|
34
|
-
|
|
35
|
-
return name.replaceAll(LOWER_TO_UPPER_PATTERN, '-').replaceAll(ACRONYM_END_PATTERN, '-').toLowerCase();
|
|
36
|
-
}
|
|
34
|
+
const kebab = (name) => name.replaceAll(LOWER_TO_UPPER_PATTERN, '-').replaceAll(ACRONYM_END_PATTERN, '-').toLowerCase();
|
|
37
35
|
/** The component a file is named for: its basename, or its folder's for an `index` file. */
|
|
38
|
-
|
|
36
|
+
const componentName = (filename) => {
|
|
39
37
|
const name = path.basename(filename).replace(EXTENSION_PATTERN, '').replace(PLATFORM_PATTERN, '');
|
|
40
38
|
return name === 'index' ? path.basename(path.dirname(filename)) : name;
|
|
41
|
-
}
|
|
42
|
-
|
|
39
|
+
};
|
|
40
|
+
const isHoc = (callee) => {
|
|
43
41
|
if (callee.type === AST_NODE_TYPES.Identifier)
|
|
44
42
|
return HOC_NAMES.has(callee.name);
|
|
45
43
|
return callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && HOC_NAMES.has(callee.property.name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return Boolean(node && FUNCTION_TYPES.has(node.type));
|
|
49
|
-
}
|
|
44
|
+
};
|
|
45
|
+
const isFunction = (node) => Boolean(node && FUNCTION_TYPES.has(node.type));
|
|
50
46
|
/** Through `memo(…)`, `forwardRef(…)` and type assertions, to the component inside. */
|
|
51
|
-
|
|
47
|
+
const unwrapComponent = (node) => {
|
|
52
48
|
let current = node ?? undefined;
|
|
53
49
|
while (current) {
|
|
54
50
|
if (TS_WRAPPERS.has(current.type))
|
|
@@ -59,31 +55,31 @@ function unwrapComponent(node) {
|
|
|
59
55
|
return current;
|
|
60
56
|
}
|
|
61
57
|
return current;
|
|
62
|
-
}
|
|
58
|
+
};
|
|
63
59
|
/** What a function is held by, out through the calls and assertions it is declared behind. */
|
|
64
|
-
|
|
60
|
+
const heldBy = (fn) => {
|
|
65
61
|
let held = fn.parent;
|
|
66
62
|
while (held && (held.type === AST_NODE_TYPES.CallExpression || TS_WRAPPERS.has(held.type)))
|
|
67
63
|
held = held.parent;
|
|
68
64
|
return held;
|
|
69
|
-
}
|
|
65
|
+
};
|
|
70
66
|
/** Whether a function is the file's own component: named `component`, or an anonymous default export. */
|
|
71
|
-
|
|
67
|
+
const isComponent = (fn, component) => {
|
|
72
68
|
if (fn.type === AST_NODE_TYPES.FunctionDeclaration)
|
|
73
69
|
return !fn.id || fn.id.name === component;
|
|
74
70
|
const held = heldBy(fn);
|
|
75
71
|
if (held?.type === AST_NODE_TYPES.ExportDefaultDeclaration)
|
|
76
72
|
return true;
|
|
77
73
|
return held?.type === AST_NODE_TYPES.VariableDeclarator && held.id.type === AST_NODE_TYPES.Identifier && held.id.name === component;
|
|
78
|
-
}
|
|
74
|
+
};
|
|
79
75
|
/** The `testID` property of a destructured parameter or declarator. */
|
|
80
|
-
|
|
76
|
+
const testIdProperty = (pattern) => {
|
|
81
77
|
if (pattern?.type !== AST_NODE_TYPES.ObjectPattern)
|
|
82
78
|
return undefined;
|
|
83
79
|
return pattern.properties.find((property) => property.type === AST_NODE_TYPES.Property && !property.computed && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'testID');
|
|
84
|
-
}
|
|
80
|
+
};
|
|
85
81
|
/** The names a component's props object goes by: the first parameter, or the rest of a destructured one without `testID`. */
|
|
86
|
-
|
|
82
|
+
const propsNames = (fn) => {
|
|
87
83
|
const first = fn.params.at(0);
|
|
88
84
|
if (first?.type === AST_NODE_TYPES.Identifier)
|
|
89
85
|
return [first.name];
|
|
@@ -91,15 +87,15 @@ function propsNames(fn) {
|
|
|
91
87
|
return [];
|
|
92
88
|
const rest = first.properties.find((property) => property.type === AST_NODE_TYPES.RestElement);
|
|
93
89
|
return rest?.argument.type === AST_NODE_TYPES.Identifier ? [rest.argument.name] : [];
|
|
94
|
-
}
|
|
90
|
+
};
|
|
95
91
|
/** The file's own component that `node` sits in, however deep in callbacks. */
|
|
96
|
-
|
|
92
|
+
const ownComponentAround = (node, component) => {
|
|
97
93
|
for (let current = node.parent; current; current = current.parent) {
|
|
98
94
|
if (isFunction(current) && isComponent(current, component))
|
|
99
95
|
return current;
|
|
100
96
|
}
|
|
101
97
|
return undefined;
|
|
102
|
-
}
|
|
98
|
+
};
|
|
103
99
|
const testIdDefault = createRule({
|
|
104
100
|
name: 'test-id-default',
|
|
105
101
|
meta: {
|
|
@@ -113,7 +109,7 @@ const testIdDefault = createRule({
|
|
|
113
109
|
wrong: 'Default this to `"{{ expected }}"`, not `{{ actual }}`: the default is the component\'s own name in kebab-case, so a reader can predict the id without opening the file.',
|
|
114
110
|
},
|
|
115
111
|
},
|
|
116
|
-
create(context) {
|
|
112
|
+
create: (context) => {
|
|
117
113
|
const component = componentName(context.filename);
|
|
118
114
|
// The file's own component only; anything else in here is private to it.
|
|
119
115
|
if (!PASCAL_CASE_PATTERN.test(component))
|
|
@@ -146,7 +142,7 @@ const testIdDefault = createRule({
|
|
|
146
142
|
check(testIdProperty(node.params[0]));
|
|
147
143
|
};
|
|
148
144
|
return {
|
|
149
|
-
VariableDeclarator(node) {
|
|
145
|
+
VariableDeclarator: (node) => {
|
|
150
146
|
// `const { testID } = props` inside the file's own component reads the same prop.
|
|
151
147
|
if (node.id.type === AST_NODE_TYPES.ObjectPattern && node.init?.type === AST_NODE_TYPES.Identifier) {
|
|
152
148
|
const fn = ownComponentAround(node, component);
|
|
@@ -157,20 +153,20 @@ const testIdDefault = createRule({
|
|
|
157
153
|
if (node.id.type === AST_NODE_TYPES.Identifier)
|
|
158
154
|
checkFunction(unwrapComponent(node.init), node.id.name);
|
|
159
155
|
},
|
|
160
|
-
FunctionDeclaration(node) {
|
|
156
|
+
FunctionDeclaration: (node) => {
|
|
161
157
|
if (node.id)
|
|
162
158
|
checkFunction(node, node.id.name);
|
|
163
159
|
else if (node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration)
|
|
164
160
|
checkFunction(node, component);
|
|
165
161
|
},
|
|
166
162
|
// `export default memo(({ testID }) => …)`: the anonymous component.
|
|
167
|
-
ExportDefaultDeclaration(node) {
|
|
163
|
+
ExportDefaultDeclaration: (node) => {
|
|
168
164
|
const init = unwrapComponent(node.declaration);
|
|
169
165
|
if (init?.type !== AST_NODE_TYPES.FunctionDeclaration)
|
|
170
166
|
checkFunction(init, component);
|
|
171
167
|
},
|
|
172
168
|
// `props.testID` read inside the component, with no default anywhere.
|
|
173
|
-
MemberExpression(node) {
|
|
169
|
+
MemberExpression: (node) => {
|
|
174
170
|
if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== 'testID')
|
|
175
171
|
return;
|
|
176
172
|
if (node.object.type !== AST_NODE_TYPES.Identifier)
|
|
@@ -29,34 +29,30 @@ const LEADING_SEPARATOR_PATTERN = /^[^A-Za-z0-9]/;
|
|
|
29
29
|
const TRAILING_SEPARATOR_PATTERN = /[^A-Za-z0-9]$/;
|
|
30
30
|
const TEST_ID = 'testID';
|
|
31
31
|
/** `Match.playerItem_photo` -> `match-player-item-photo`. */
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.toLowerCase();
|
|
39
|
-
}
|
|
32
|
+
const kebab = (value) => value
|
|
33
|
+
.replaceAll(LOWER_TO_UPPER_PATTERN, '-')
|
|
34
|
+
.replaceAll(ACRONYM_END_PATTERN, '-')
|
|
35
|
+
.replaceAll(NON_ALPHANUMERIC_RUN_PATTERN, '-')
|
|
36
|
+
.replaceAll(EDGE_DASHES_PATTERN, '')
|
|
37
|
+
.toLowerCase();
|
|
40
38
|
/** A piece between substitutions keeps a separator where it meets one: `_photo` becomes `-photo`. */
|
|
41
|
-
|
|
39
|
+
const kebabFragment = (value) => {
|
|
42
40
|
const core = kebab(value);
|
|
43
41
|
const lead = LEADING_SEPARATOR_PATTERN.test(value) ? '-' : '';
|
|
44
42
|
const trail = TRAILING_SEPARATOR_PATTERN.test(value) ? '-' : '';
|
|
45
43
|
return core ? `${lead}${core}${trail}` : '-';
|
|
46
|
-
}
|
|
44
|
+
};
|
|
47
45
|
/** Whether `inner` is written inside `outer`, rather than elsewhere in the file. */
|
|
48
|
-
|
|
49
|
-
return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
|
|
50
|
-
}
|
|
46
|
+
const isInside = (outer, inner) => inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
|
|
51
47
|
/** The written string an expression is, or `undefined` when it is not one. */
|
|
52
|
-
|
|
48
|
+
const writtenString = (node) => {
|
|
53
49
|
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string')
|
|
54
50
|
return node.value;
|
|
55
51
|
// A template with nothing substituted is a literal written the long way.
|
|
56
52
|
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0)
|
|
57
53
|
return node.quasis[0]?.value.cooked ?? undefined;
|
|
58
54
|
return undefined;
|
|
59
|
-
}
|
|
55
|
+
};
|
|
60
56
|
const testIdFormat = createRule({
|
|
61
57
|
name: 'test-id-format',
|
|
62
58
|
meta: {
|
|
@@ -68,7 +64,7 @@ const testIdFormat = createRule({
|
|
|
68
64
|
format: 'Write this `testID` as `{{ suggestion }}`, not "{{ value }}": an id is lowercase kebab-case throughout, depth included. A dot is a wildcard in Maestro\'s `id:` matcher, and a capital is one shift key away from a flake — so a child derives with a dash, `` `${testID}-remote-image` ``, at every level.',
|
|
69
65
|
},
|
|
70
66
|
},
|
|
71
|
-
create(context) {
|
|
67
|
+
create: (context) => {
|
|
72
68
|
// Reported on the string when it sits inside the expression, else on the expression a name led from.
|
|
73
69
|
const check = (expression, leaf) => {
|
|
74
70
|
const value = writtenString(leaf);
|
|
@@ -101,7 +97,7 @@ const testIdFormat = createRule({
|
|
|
101
97
|
}
|
|
102
98
|
};
|
|
103
99
|
return {
|
|
104
|
-
JSXAttribute(node) {
|
|
100
|
+
JSXAttribute: (node) => {
|
|
105
101
|
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== TEST_ID)
|
|
106
102
|
return;
|
|
107
103
|
const { value } = node;
|
|
@@ -111,7 +107,7 @@ const testIdFormat = createRule({
|
|
|
111
107
|
checkExpression(value.expression);
|
|
112
108
|
},
|
|
113
109
|
// A `testID` default, and a `testID:` in an options or fixture object.
|
|
114
|
-
Property(node) {
|
|
110
|
+
Property: (node) => {
|
|
115
111
|
if (node.computed || node.key.type !== AST_NODE_TYPES.Identifier || node.key.name !== TEST_ID)
|
|
116
112
|
return;
|
|
117
113
|
checkExpression(node.value.type === AST_NODE_TYPES.AssignmentPattern ? node.value.right : node.value);
|
|
@@ -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;
|
|
@@ -35,13 +35,13 @@ const undefinedCheck = createRule({
|
|
|
35
35
|
redundant: "`{{ text }}` is `{{ suggestion }}` for this type: `undefined` is the only falsy value it admits, so the comparison draws no line. Write the value.",
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
-
create(context) {
|
|
38
|
+
create: (context) => {
|
|
39
39
|
const services = ESLintUtils.getParserServices(context, true);
|
|
40
40
|
if (!services.program)
|
|
41
41
|
return {};
|
|
42
42
|
const checker = services.program.getTypeChecker();
|
|
43
43
|
return {
|
|
44
|
-
BinaryExpression(node) {
|
|
44
|
+
BinaryExpression: (node) => {
|
|
45
45
|
if (!isCondition(node))
|
|
46
46
|
return;
|
|
47
47
|
const compared = undefinedComparison(node);
|
|
@@ -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 {};
|