@murky-web/oxlint-plugin-solid 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/package.json +44 -0
- package/src/compat.mjs +53 -0
- package/src/index.mjs +56 -0
- package/src/rules/components_return_once.mjs +202 -0
- package/src/rules/event_handlers.mjs +298 -0
- package/src/rules/imports.mjs +205 -0
- package/src/rules/jsx_no_duplicate_props.mjs +87 -0
- package/src/rules/jsx_no_script_url.mjs +54 -0
- package/src/rules/jsx_no_undef.mjs +217 -0
- package/src/rules/jsx_uses_vars.mjs +55 -0
- package/src/rules/no_array_handlers.mjs +53 -0
- package/src/rules/no_destructure.mjs +210 -0
- package/src/rules/no_innerhtml.mjs +145 -0
- package/src/rules/no_proxy_apis.mjs +96 -0
- package/src/rules/no_react_deps.mjs +65 -0
- package/src/rules/no_react_specific_props.mjs +71 -0
- package/src/rules/no_unknown_namespaces.mjs +100 -0
- package/src/rules/prefer_arrow_components.mjs +411 -0
- package/src/rules/prefer_classlist.mjs +89 -0
- package/src/rules/prefer_for.mjs +92 -0
- package/src/rules/prefer_show.mjs +92 -0
- package/src/rules/reactivity.mjs +1300 -0
- package/src/rules/self_closing_comp.mjs +153 -0
- package/src/rules/style_prop.mjs +155 -0
- package/src/rules/validate_jsx_nesting.mjs +16 -0
- package/src/utils.mjs +337 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { getSourceCode } from "../compat.mjs";
|
|
4
|
+
import { isDOMElementName } from "../utils.mjs";
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
function isComponent(node) {
|
|
7
|
+
return (
|
|
8
|
+
(node.name.type === "JSXIdentifier" &&
|
|
9
|
+
!isDOMElementName(node.name.name)) ||
|
|
10
|
+
node.name.type === "JSXMemberExpression"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const voidDOMElementRegex =
|
|
14
|
+
/^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
|
|
15
|
+
function isVoidDOMElementName(name) {
|
|
16
|
+
return voidDOMElementRegex.test(name);
|
|
17
|
+
}
|
|
18
|
+
function childrenIsEmpty(node) {
|
|
19
|
+
return node.parent.children.length === 0;
|
|
20
|
+
}
|
|
21
|
+
function childrenIsMultilineSpaces(node) {
|
|
22
|
+
const childrens = node.parent.children;
|
|
23
|
+
return (
|
|
24
|
+
childrens.length === 1 &&
|
|
25
|
+
childrens[0].type === "JSXText" &&
|
|
26
|
+
childrens[0].value.indexOf("\n") !== -1 &&
|
|
27
|
+
childrens[0].value.replace(/(?!\xA0)\s/g, "") === ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* This rule is adapted from eslint-plugin-react's self-closing-comp rule under the MIT license,
|
|
32
|
+
* with some enhancements. Thank you for your work!
|
|
33
|
+
*/
|
|
34
|
+
export default createRule({
|
|
35
|
+
meta: {
|
|
36
|
+
type: "layout",
|
|
37
|
+
docs: {
|
|
38
|
+
description:
|
|
39
|
+
"Disallow extra closing tags for components without children.",
|
|
40
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/self-closing-comp.md",
|
|
41
|
+
},
|
|
42
|
+
fixable: "code",
|
|
43
|
+
schema: [
|
|
44
|
+
{
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
component: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description:
|
|
50
|
+
"which Solid components should be self-closing when possible",
|
|
51
|
+
enum: ["all", "none"],
|
|
52
|
+
default: "all",
|
|
53
|
+
},
|
|
54
|
+
html: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description:
|
|
57
|
+
"which native elements should be self-closing when possible",
|
|
58
|
+
enum: ["all", "void", "none"],
|
|
59
|
+
default: "all",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
messages: {
|
|
66
|
+
selfClose: "Empty components are self-closing.",
|
|
67
|
+
dontSelfClose: "This element should not be self-closing.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
defaultOptions: [],
|
|
71
|
+
create(context) {
|
|
72
|
+
function shouldBeSelfClosedWhenPossible(node) {
|
|
73
|
+
if (isComponent(node)) {
|
|
74
|
+
const whichComponents = context.options[0]?.component ?? "all";
|
|
75
|
+
return whichComponents === "all";
|
|
76
|
+
} else if (
|
|
77
|
+
node.name.type === "JSXIdentifier" &&
|
|
78
|
+
isDOMElementName(node.name.name)
|
|
79
|
+
) {
|
|
80
|
+
const whichComponents = context.options[0]?.html ?? "all";
|
|
81
|
+
switch (whichComponents) {
|
|
82
|
+
case "all":
|
|
83
|
+
return true;
|
|
84
|
+
case "void":
|
|
85
|
+
return isVoidDOMElementName(node.name.name);
|
|
86
|
+
case "none":
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true; // shouldn't encounter
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
JSXOpeningElement(node) {
|
|
94
|
+
const canSelfClose =
|
|
95
|
+
childrenIsEmpty(node) || childrenIsMultilineSpaces(node);
|
|
96
|
+
if (canSelfClose) {
|
|
97
|
+
const shouldSelfClose =
|
|
98
|
+
shouldBeSelfClosedWhenPossible(node);
|
|
99
|
+
if (shouldSelfClose && !node.selfClosing) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
messageId: "selfClose",
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
// Represents the last character of the JSXOpeningElement, the '>' character
|
|
105
|
+
const openingElementEnding = node.range[1] - 1;
|
|
106
|
+
// Represents the last character of the JSXClosingElement, the '>' character
|
|
107
|
+
const closingElementEnding =
|
|
108
|
+
node.parent.closingElement.range[1];
|
|
109
|
+
// Replace />.*<\/.*>/ with '/>'
|
|
110
|
+
const range = [
|
|
111
|
+
openingElementEnding,
|
|
112
|
+
closingElementEnding,
|
|
113
|
+
];
|
|
114
|
+
return fixer.replaceTextRange(range, " />");
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
} else if (!shouldSelfClose && node.selfClosing) {
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: "dontSelfClose",
|
|
121
|
+
fix(fixer) {
|
|
122
|
+
const sourceCode = getSourceCode(context);
|
|
123
|
+
const tagName = sourceCode.getText(node.name);
|
|
124
|
+
// Represents the last character of the JSXOpeningElement, the '>' character
|
|
125
|
+
const selfCloseEnding = node.range[1];
|
|
126
|
+
// Replace ' />' or '/>' with '></${tagName}>'
|
|
127
|
+
const lastTokens = sourceCode.getLastTokens(
|
|
128
|
+
node,
|
|
129
|
+
{ count: 3 },
|
|
130
|
+
); // JSXIdentifier, '/', '>'
|
|
131
|
+
const isSpaceBeforeSelfClose =
|
|
132
|
+
sourceCode.isSpaceBetween?.(
|
|
133
|
+
lastTokens[0],
|
|
134
|
+
lastTokens[1],
|
|
135
|
+
);
|
|
136
|
+
const range = [
|
|
137
|
+
isSpaceBeforeSelfClose
|
|
138
|
+
? selfCloseEnding - 3
|
|
139
|
+
: selfCloseEnding - 2,
|
|
140
|
+
selfCloseEnding,
|
|
141
|
+
];
|
|
142
|
+
return fixer.replaceTextRange(
|
|
143
|
+
range,
|
|
144
|
+
`></${tagName}>`,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
2
|
+
import kebabCase from "kebab-case";
|
|
3
|
+
import { all as allCssProperties } from "known-css-properties";
|
|
4
|
+
import parse from "style-to-object";
|
|
5
|
+
|
|
6
|
+
import { getScope } from "../compat.mjs";
|
|
7
|
+
import { jsxPropName } from "../utils.mjs";
|
|
8
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
9
|
+
const { getPropertyName, getStaticValue } = ASTUtils;
|
|
10
|
+
const lengthPercentageRegex =
|
|
11
|
+
/\b(?:width|height|margin|padding|border-width|font-size)\b/i;
|
|
12
|
+
export default createRule({
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Require CSS properties in the `style` prop to be valid and kebab-cased (ex. 'font-size'), not camel-cased (ex. 'fontSize') like in React, " +
|
|
18
|
+
"and that property values with dimensions are strings, not numbers with implicit 'px' units.",
|
|
19
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/style-prop.md",
|
|
20
|
+
},
|
|
21
|
+
fixable: "code",
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
styleProps: {
|
|
27
|
+
description:
|
|
28
|
+
"an array of prop names to treat as a CSS style object",
|
|
29
|
+
default: ["style"],
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "string",
|
|
33
|
+
},
|
|
34
|
+
minItems: 1,
|
|
35
|
+
uniqueItems: true,
|
|
36
|
+
},
|
|
37
|
+
allowString: {
|
|
38
|
+
description:
|
|
39
|
+
"if allowString is set to true, this rule will not convert a style string literal into a style object (not recommended for performance)",
|
|
40
|
+
type: "boolean",
|
|
41
|
+
default: false,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
messages: {
|
|
48
|
+
kebabStyleProp: "Use {{ kebabName }} instead of {{ name }}.",
|
|
49
|
+
invalidStyleProp: "{{ name }} is not a valid CSS property.",
|
|
50
|
+
numericStyleValue:
|
|
51
|
+
'This CSS property value should be a string with a unit; Solid does not automatically append a "px" unit.',
|
|
52
|
+
stringStyle:
|
|
53
|
+
"Use an object for the style prop instead of a string.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultOptions: [],
|
|
57
|
+
create(context) {
|
|
58
|
+
const allCssPropertiesSet = new Set(allCssProperties);
|
|
59
|
+
const allowString = Boolean(context.options[0]?.allowString);
|
|
60
|
+
const styleProps = context.options[0]?.styleProps || ["style"];
|
|
61
|
+
return {
|
|
62
|
+
JSXAttribute(node) {
|
|
63
|
+
if (styleProps.indexOf(jsxPropName(node)) === -1) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const style =
|
|
67
|
+
node.value?.type === "JSXExpressionContainer"
|
|
68
|
+
? node.value.expression
|
|
69
|
+
: node.value;
|
|
70
|
+
if (!style) {
|
|
71
|
+
return;
|
|
72
|
+
} else if (
|
|
73
|
+
style.type === "Literal" &&
|
|
74
|
+
typeof style.value === "string" &&
|
|
75
|
+
!allowString
|
|
76
|
+
) {
|
|
77
|
+
// Convert style="font-size: 10px" to style={{ "font-size": "10px" }}
|
|
78
|
+
let objectStyles;
|
|
79
|
+
try {
|
|
80
|
+
objectStyles = parse(style.value) ?? undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
// no-op
|
|
83
|
+
}
|
|
84
|
+
context.report({
|
|
85
|
+
node: style,
|
|
86
|
+
messageId: "stringStyle",
|
|
87
|
+
// replace full prop value, wrap in JSXExpressionContainer, more fixes may be applied below
|
|
88
|
+
fix:
|
|
89
|
+
objectStyles &&
|
|
90
|
+
((fixer) =>
|
|
91
|
+
fixer.replaceText(
|
|
92
|
+
node.value,
|
|
93
|
+
`{${JSON.stringify(objectStyles)}}`,
|
|
94
|
+
)),
|
|
95
|
+
});
|
|
96
|
+
} else if (style.type === "TemplateLiteral" && !allowString) {
|
|
97
|
+
context.report({
|
|
98
|
+
node: style,
|
|
99
|
+
messageId: "stringStyle",
|
|
100
|
+
});
|
|
101
|
+
} else if (style.type === "ObjectExpression") {
|
|
102
|
+
const properties = style.properties.filter(
|
|
103
|
+
(prop) => prop.type === "Property",
|
|
104
|
+
);
|
|
105
|
+
properties.forEach((prop) => {
|
|
106
|
+
const name = getPropertyName(
|
|
107
|
+
prop,
|
|
108
|
+
getScope(context, prop),
|
|
109
|
+
);
|
|
110
|
+
if (
|
|
111
|
+
name &&
|
|
112
|
+
!name.startsWith("--") &&
|
|
113
|
+
!allCssPropertiesSet.has(name)
|
|
114
|
+
) {
|
|
115
|
+
const kebabName = kebabCase(name);
|
|
116
|
+
if (allCssPropertiesSet.has(kebabName)) {
|
|
117
|
+
// if it's not valid simply because it's camelCased instead of kebab-cased, provide a fix
|
|
118
|
+
context.report({
|
|
119
|
+
node: prop.key,
|
|
120
|
+
messageId: "kebabStyleProp",
|
|
121
|
+
data: { name, kebabName },
|
|
122
|
+
fix: (fixer) =>
|
|
123
|
+
fixer.replaceText(
|
|
124
|
+
prop.key,
|
|
125
|
+
`"${kebabName}"`,
|
|
126
|
+
), // wrap kebab name in quotes to be a valid object key
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
context.report({
|
|
130
|
+
node: prop.key,
|
|
131
|
+
messageId: "invalidStyleProp",
|
|
132
|
+
data: { name },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else if (
|
|
136
|
+
!name ||
|
|
137
|
+
(!name.startsWith("--") &&
|
|
138
|
+
lengthPercentageRegex.test(name))
|
|
139
|
+
) {
|
|
140
|
+
// catches numeric values (ex. { "font-size": 12 }) for common <length-percentage> peroperties
|
|
141
|
+
// and suggests quoting or appending 'px'
|
|
142
|
+
const value = getStaticValue(prop.value)?.value;
|
|
143
|
+
if (typeof value === "number" && value !== 0) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: prop.value,
|
|
146
|
+
messageId: "numericStyleValue",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
defaultOptions: [],
|
|
3
|
+
meta: {
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Placeholder for the upstream validate-jsx-nesting rule, which currently has no implementation in eslint-plugin-solid.",
|
|
7
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/src/rules/validate-jsx-nesting.ts",
|
|
8
|
+
},
|
|
9
|
+
messages: {},
|
|
10
|
+
schema: [],
|
|
11
|
+
type: "problem",
|
|
12
|
+
},
|
|
13
|
+
create() {
|
|
14
|
+
return {};
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/utils.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { findVariable } from "./compat.mjs";
|
|
2
|
+
|
|
3
|
+
const DOM_ELEMENT_REGEX = /^[a-z]/;
|
|
4
|
+
const PROPS_REGEX = /[pP]rops/;
|
|
5
|
+
const FUNCTION_TYPES = [
|
|
6
|
+
"FunctionExpression",
|
|
7
|
+
"ArrowFunctionExpression",
|
|
8
|
+
"FunctionDeclaration",
|
|
9
|
+
];
|
|
10
|
+
const PROGRAM_OR_FUNCTION_TYPES = ["Program", ...FUNCTION_TYPES];
|
|
11
|
+
|
|
12
|
+
export function isDOMElementName(name) {
|
|
13
|
+
return DOM_ELEMENT_REGEX.test(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isPropsByName(name) {
|
|
17
|
+
return PROPS_REGEX.test(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatList(strings) {
|
|
21
|
+
if (strings.length === 0) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (strings.length === 1) {
|
|
26
|
+
return `'${strings[0]}'`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (strings.length === 2) {
|
|
30
|
+
return `'${strings[0]}' and '${strings[1]}'`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const last = strings.length - 1;
|
|
34
|
+
return `${strings
|
|
35
|
+
.slice(0, last)
|
|
36
|
+
.map((stringValue) => {
|
|
37
|
+
return `'${stringValue}'`;
|
|
38
|
+
})
|
|
39
|
+
.join(", ")}, and '${strings[last]}'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function find(node, predicate) {
|
|
43
|
+
let currentNode = node;
|
|
44
|
+
|
|
45
|
+
while (currentNode) {
|
|
46
|
+
if (predicate(currentNode)) {
|
|
47
|
+
return currentNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
currentNode = currentNode.parent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findParent(node, predicate) {
|
|
57
|
+
return node.parent ? find(node.parent, predicate) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function trace(node, context) {
|
|
61
|
+
if (node.type !== "Identifier") {
|
|
62
|
+
return node;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const variable = findVariable(context, node);
|
|
66
|
+
if (!variable) {
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const definition = variable.defs[0];
|
|
71
|
+
switch (definition?.type) {
|
|
72
|
+
case "FunctionName":
|
|
73
|
+
case "ClassName":
|
|
74
|
+
case "ImportBinding":
|
|
75
|
+
return definition.node;
|
|
76
|
+
case "Variable":
|
|
77
|
+
if (
|
|
78
|
+
(definition.node.parent.kind === "const" ||
|
|
79
|
+
variable.references.every((reference) => {
|
|
80
|
+
return reference.init || reference.isReadOnly();
|
|
81
|
+
})) &&
|
|
82
|
+
definition.node.id.type === "Identifier" &&
|
|
83
|
+
definition.node.init
|
|
84
|
+
) {
|
|
85
|
+
return trace(definition.node.init, context);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return node;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function ignoreTransparentWrappers(node, up = false) {
|
|
96
|
+
if (
|
|
97
|
+
node.type === "TSAsExpression" ||
|
|
98
|
+
node.type === "TSNonNullExpression" ||
|
|
99
|
+
node.type === "TSSatisfiesExpression"
|
|
100
|
+
) {
|
|
101
|
+
const nextNode = up ? node.parent : node.expression;
|
|
102
|
+
if (nextNode) {
|
|
103
|
+
return ignoreTransparentWrappers(nextNode, up);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isFunctionNode(node) {
|
|
111
|
+
return Boolean(node) && FUNCTION_TYPES.includes(node.type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isProgramOrFunctionNode(node) {
|
|
115
|
+
return Boolean(node) && PROGRAM_OR_FUNCTION_TYPES.includes(node.type);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isJSXElementOrFragment(node) {
|
|
119
|
+
return node?.type === "JSXElement" || node?.type === "JSXFragment";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getFunctionName(node) {
|
|
123
|
+
if (
|
|
124
|
+
(node.type === "FunctionDeclaration" ||
|
|
125
|
+
node.type === "FunctionExpression") &&
|
|
126
|
+
node.id != null
|
|
127
|
+
) {
|
|
128
|
+
return node.id.name;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
node.parent?.type === "VariableDeclarator" &&
|
|
133
|
+
node.parent.id.type === "Identifier"
|
|
134
|
+
) {
|
|
135
|
+
return node.parent.id.name;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function findInScope(node, scope, predicate) {
|
|
142
|
+
const foundNode = find(node, (innerNode) => {
|
|
143
|
+
return innerNode === scope || predicate(innerNode);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return foundNode === scope && !predicate(node) ? null : foundNode;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getCommentBefore(node, sourceCode) {
|
|
150
|
+
return sourceCode.getCommentsBefore(node).find((comment) => {
|
|
151
|
+
return comment.loc.end.line >= node.loc.start.line - 1;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getCommentAfter(node, sourceCode) {
|
|
156
|
+
return sourceCode.getCommentsAfter(node).find((comment) => {
|
|
157
|
+
return comment.loc.start.line === node.loc.end.line;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function trackImports(fromModule = /^solid-js(?:\/?|\b)/) {
|
|
162
|
+
const importMap = new Map();
|
|
163
|
+
|
|
164
|
+
function handleImportDeclaration(node) {
|
|
165
|
+
if (!fromModule.test(node.source.value)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const specifier of node.specifiers) {
|
|
170
|
+
if (specifier.type === "ImportSpecifier") {
|
|
171
|
+
importMap.set(specifier.imported.name, specifier.local.name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchImport(imports, value) {
|
|
177
|
+
const importList = Array.isArray(imports) ? imports : [imports];
|
|
178
|
+
return importList.find((importName) => {
|
|
179
|
+
return importMap.get(importName) === value;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
handleImportDeclaration,
|
|
185
|
+
matchImport,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function appendImports(fixer, sourceCode, importNode, identifiers) {
|
|
190
|
+
const identifiersString = identifiers.join(", ");
|
|
191
|
+
const namedSpecifier = importNode.specifiers
|
|
192
|
+
.slice()
|
|
193
|
+
.reverse()
|
|
194
|
+
.find((specifier) => {
|
|
195
|
+
return specifier.type === "ImportSpecifier";
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (namedSpecifier) {
|
|
199
|
+
return fixer.insertTextAfter(namedSpecifier, `, ${identifiersString}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const otherSpecifier = importNode.specifiers.find((specifier) => {
|
|
203
|
+
return (
|
|
204
|
+
specifier.type === "ImportDefaultSpecifier" ||
|
|
205
|
+
specifier.type === "ImportNamespaceSpecifier"
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (otherSpecifier) {
|
|
210
|
+
return fixer.insertTextAfter(
|
|
211
|
+
otherSpecifier,
|
|
212
|
+
`, { ${identifiersString} }`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (importNode.specifiers.length === 0) {
|
|
217
|
+
const [importToken, maybeBrace] = sourceCode.getFirstTokens(
|
|
218
|
+
importNode,
|
|
219
|
+
{
|
|
220
|
+
count: 2,
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (maybeBrace?.value === "{") {
|
|
225
|
+
return fixer.insertTextAfter(maybeBrace, ` ${identifiersString} `);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return importToken
|
|
229
|
+
? fixer.insertTextAfter(
|
|
230
|
+
importToken,
|
|
231
|
+
` { ${identifiersString} } from`,
|
|
232
|
+
)
|
|
233
|
+
: null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function insertImports(
|
|
240
|
+
fixer,
|
|
241
|
+
sourceCode,
|
|
242
|
+
source,
|
|
243
|
+
identifiers,
|
|
244
|
+
aboveImport,
|
|
245
|
+
isType = false,
|
|
246
|
+
) {
|
|
247
|
+
const identifiersString = identifiers.join(", ");
|
|
248
|
+
const programNode = sourceCode.ast;
|
|
249
|
+
const firstImport =
|
|
250
|
+
aboveImport ||
|
|
251
|
+
programNode.body.find((node) => {
|
|
252
|
+
return node.type === "ImportDeclaration";
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (firstImport) {
|
|
256
|
+
return fixer.insertTextBeforeRange(
|
|
257
|
+
(getCommentBefore(firstImport, sourceCode) ?? firstImport).range,
|
|
258
|
+
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return fixer.insertTextBeforeRange(
|
|
263
|
+
[0, 0],
|
|
264
|
+
`import ${isType ? "type " : ""}{ ${identifiersString} } from "${source}";\n`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function removeSpecifier(fixer, sourceCode, specifier, pure = true) {
|
|
269
|
+
const declaration = specifier.parent;
|
|
270
|
+
if (declaration.specifiers.length === 1 && pure) {
|
|
271
|
+
return fixer.remove(declaration);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const maybeComma = sourceCode.getTokenAfter(specifier);
|
|
275
|
+
if (maybeComma?.value === ",") {
|
|
276
|
+
return fixer.removeRange([specifier.range[0], maybeComma.range[1]]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return fixer.remove(specifier);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function jsxPropName(prop) {
|
|
283
|
+
if (prop.name.type === "JSXNamespacedName") {
|
|
284
|
+
return `${prop.name.namespace.name}:${prop.name.name.name}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return prop.name.name;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function* jsxGetAllProps(props) {
|
|
291
|
+
for (const attribute of props) {
|
|
292
|
+
if (
|
|
293
|
+
attribute.type === "JSXSpreadAttribute" &&
|
|
294
|
+
attribute.argument.type === "ObjectExpression"
|
|
295
|
+
) {
|
|
296
|
+
for (const property of attribute.argument.properties) {
|
|
297
|
+
if (property.type !== "Property") {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (property.key.type === "Identifier") {
|
|
302
|
+
yield [property.key.name, property.key];
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (property.key.type === "Literal") {
|
|
307
|
+
yield [String(property.key.value), property.key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (attribute.type === "JSXAttribute") {
|
|
315
|
+
yield [jsxPropName(attribute), attribute.name];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function jsxHasProp(props, prop) {
|
|
321
|
+
for (const [propName] of jsxGetAllProps(props)) {
|
|
322
|
+
if (propName === prop) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function jsxGetProp(props, prop) {
|
|
331
|
+
return props.find((attribute) => {
|
|
332
|
+
return (
|
|
333
|
+
attribute.type !== "JSXSpreadAttribute" &&
|
|
334
|
+
prop === jsxPropName(attribute)
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
}
|