@conorroberts/utils 0.0.60 → 0.0.62
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/dist/env.d.mts +19 -0
- package/dist/env.mjs +32 -0
- package/dist/env.mjs.map +1 -0
- package/dist/images.d.mts +101 -0
- package/dist/images.mjs +130 -0
- package/dist/images.mjs.map +1 -0
- package/dist/oxlint/config.json +70 -0
- package/dist/oxlint/index.d.mts +7 -0
- package/dist/oxlint/index.mjs +1133 -0
- package/dist/oxlint/index.mjs.map +1 -0
- package/dist/react.d.mts +73 -0
- package/dist/react.mjs +113 -0
- package/dist/react.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import { definePlugin, defineRule } from "oxlint";
|
|
2
|
+
|
|
3
|
+
//#region src/oxlint-plugins/jsx-component-pascal-case.js
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("oxlint").Context} RuleContext
|
|
6
|
+
* @typedef {import("oxlint").ESTree.Node} ESTNode
|
|
7
|
+
* @typedef {import("oxlint").ESTree.Expression} ESTExpression
|
|
8
|
+
* @typedef {import("oxlint").ESTree.ReturnStatement} ReturnStatementNode
|
|
9
|
+
* @typedef {import("oxlint").ESTree.Function | import("oxlint").ESTree.ArrowFunctionExpression} FunctionLikeNode
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} FunctionContext
|
|
13
|
+
* @property {FunctionLikeNode} node
|
|
14
|
+
* @property {string} name
|
|
15
|
+
* @property {boolean} returnsJsx
|
|
16
|
+
*/
|
|
17
|
+
const JSX_NODE_TYPES$2 = new Set(["JSXElement", "JSXFragment"]);
|
|
18
|
+
const FUNCTION_NODE_TYPES$3 = new Set([
|
|
19
|
+
"FunctionDeclaration",
|
|
20
|
+
"FunctionExpression",
|
|
21
|
+
"ArrowFunctionExpression"
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} node
|
|
25
|
+
* @returns {node is ESTNode & { type: string }}
|
|
26
|
+
*/
|
|
27
|
+
const isNode$3 = (node) => Boolean(node && typeof node === "object" && "type" in node);
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} node
|
|
30
|
+
* @returns {node is FunctionLikeNode}
|
|
31
|
+
*/
|
|
32
|
+
const isFunctionLike$3 = (node) => isNode$3(node) && FUNCTION_NODE_TYPES$3.has(node.type);
|
|
33
|
+
/**
|
|
34
|
+
* @param {unknown} name
|
|
35
|
+
* @returns {name is string}
|
|
36
|
+
*/
|
|
37
|
+
const isPascalCase = (name) => typeof name === "string" && /^[A-Z]/.test(name);
|
|
38
|
+
/**
|
|
39
|
+
* Check if a name is a valid higher-order component name (starts with "with")
|
|
40
|
+
* @param {unknown} name
|
|
41
|
+
* @returns {name is string}
|
|
42
|
+
*/
|
|
43
|
+
const isHOCName = (name) => typeof name === "string" && /^with[A-Z]/.test(name);
|
|
44
|
+
/**
|
|
45
|
+
* @param {FunctionLikeNode} node
|
|
46
|
+
*/
|
|
47
|
+
const getFunctionName$2 = (node) => {
|
|
48
|
+
if (node.type === "FunctionDeclaration" && node.id && node.id.type === "Identifier") return node.id.name;
|
|
49
|
+
if ((node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.id) {
|
|
50
|
+
if (node.id.type === "Identifier") return node.id.name;
|
|
51
|
+
}
|
|
52
|
+
const parent = node.parent;
|
|
53
|
+
if (!parent || !isNode$3(parent)) return "";
|
|
54
|
+
if (parent.type === "VariableDeclarator") return parent.id && parent.id.type === "Identifier" ? parent.id.name : "";
|
|
55
|
+
if (parent.type === "AssignmentExpression") return parent.left && parent.left.type === "Identifier" ? parent.left.name : "";
|
|
56
|
+
if (parent.type === "Property" || parent.type === "MethodDefinition") return "";
|
|
57
|
+
if (parent.type === "CallExpression") {
|
|
58
|
+
const callParent = parent.parent;
|
|
59
|
+
if (callParent && isNode$3(callParent)) {
|
|
60
|
+
if (callParent.type === "VariableDeclarator") return callParent.id && callParent.id.type === "Identifier" ? callParent.id.name : "";
|
|
61
|
+
if (callParent.type === "AssignmentExpression") return callParent.left && callParent.left.type === "Identifier" ? callParent.left.name : "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* @param {ESTExpression | null | undefined} root
|
|
68
|
+
*/
|
|
69
|
+
const expressionContainsJsx$2 = (root) => {
|
|
70
|
+
if (!root || !isNode$3(root)) return false;
|
|
71
|
+
const stack = [root];
|
|
72
|
+
while (stack.length > 0) {
|
|
73
|
+
const current = stack.pop();
|
|
74
|
+
if (!current || !isNode$3(current)) continue;
|
|
75
|
+
if (JSX_NODE_TYPES$2.has(current.type)) return true;
|
|
76
|
+
if (FUNCTION_NODE_TYPES$3.has(current.type) && current !== root) continue;
|
|
77
|
+
for (const key of Object.keys(current)) {
|
|
78
|
+
if (key === "parent") continue;
|
|
79
|
+
const value = current[key];
|
|
80
|
+
if (!value) continue;
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const element of value) if (isNode$3(element)) stack.push(element);
|
|
83
|
+
} else if (isNode$3(value)) stack.push(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
const rule$8 = defineRule({
|
|
89
|
+
meta: {
|
|
90
|
+
type: "problem",
|
|
91
|
+
docs: {
|
|
92
|
+
description: "Enforce PascalCase naming for functions that return JSX elements (components).",
|
|
93
|
+
recommended: false
|
|
94
|
+
},
|
|
95
|
+
schema: []
|
|
96
|
+
},
|
|
97
|
+
createOnce(context) {
|
|
98
|
+
/** @type {FunctionContext[]} */
|
|
99
|
+
const functionStack = [];
|
|
100
|
+
const currentFunction = () => functionStack[functionStack.length - 1] ?? null;
|
|
101
|
+
/**
|
|
102
|
+
* @param {FunctionLikeNode} node
|
|
103
|
+
*/
|
|
104
|
+
const enterFunction = (node) => {
|
|
105
|
+
/** @type {FunctionContext} */
|
|
106
|
+
const fnCtx = {
|
|
107
|
+
node,
|
|
108
|
+
name: getFunctionName$2(node),
|
|
109
|
+
returnsJsx: false
|
|
110
|
+
};
|
|
111
|
+
functionStack.push(fnCtx);
|
|
112
|
+
if (node.type === "ArrowFunctionExpression" && node.body && node.body.type !== "BlockStatement") {
|
|
113
|
+
if (expressionContainsJsx$2(node.body)) fnCtx.returnsJsx = true;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const exitFunction = () => {
|
|
117
|
+
const fnCtx = functionStack.pop();
|
|
118
|
+
if (!fnCtx) return;
|
|
119
|
+
if (fnCtx.returnsJsx && fnCtx.name && !isPascalCase(fnCtx.name) && !isHOCName(fnCtx.name)) context.report({
|
|
120
|
+
node: fnCtx.node,
|
|
121
|
+
message: `Function '${fnCtx.name}' returns JSX and should use PascalCase naming (e.g., '${fnCtx.name.charAt(0).toUpperCase()}${fnCtx.name.slice(1)}').`
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
/** @param {ReturnStatementNode} node */
|
|
125
|
+
const handleReturnStatement = (node) => {
|
|
126
|
+
const fnCtx = currentFunction();
|
|
127
|
+
if (!fnCtx) return;
|
|
128
|
+
const argument = node.argument;
|
|
129
|
+
if (!argument || isFunctionLike$3(argument)) return;
|
|
130
|
+
if (expressionContainsJsx$2(argument)) fnCtx.returnsJsx = true;
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
FunctionDeclaration(node) {
|
|
134
|
+
if (isFunctionLike$3(node)) enterFunction(node);
|
|
135
|
+
},
|
|
136
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
137
|
+
FunctionExpression(node) {
|
|
138
|
+
if (isFunctionLike$3(node)) enterFunction(node);
|
|
139
|
+
},
|
|
140
|
+
"FunctionExpression:exit": exitFunction,
|
|
141
|
+
ArrowFunctionExpression(node) {
|
|
142
|
+
if (isFunctionLike$3(node)) enterFunction(node);
|
|
143
|
+
},
|
|
144
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
145
|
+
ReturnStatement(node) {
|
|
146
|
+
if (node.type === "ReturnStatement") handleReturnStatement(node);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const jsxComponentPascalCaseRule = rule$8;
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/oxlint-plugins/no-component-date-instantiation.js
|
|
155
|
+
/**
|
|
156
|
+
* @typedef {import("oxlint").Context} RuleContext
|
|
157
|
+
* @typedef {import("oxlint").ESTree.Node} ESTNode
|
|
158
|
+
* @typedef {import("oxlint").ESTree.NewExpression} NewExpressionNode
|
|
159
|
+
* @typedef {import("oxlint").ESTree.ReturnStatement} ReturnStatementNode
|
|
160
|
+
* @typedef {import("oxlint").ESTree.Function | import("oxlint").ESTree.ArrowFunctionExpression} FunctionLikeNode
|
|
161
|
+
*/
|
|
162
|
+
/**
|
|
163
|
+
* @typedef {object} FunctionContext
|
|
164
|
+
* @property {FunctionLikeNode} node
|
|
165
|
+
* @property {FunctionContext | null} parent
|
|
166
|
+
* @property {string} name
|
|
167
|
+
* @property {boolean} returnsJsx
|
|
168
|
+
* @property {NewExpressionNode[]} dateInstantiations
|
|
169
|
+
*/
|
|
170
|
+
const FUNCTION_NODE_TYPES$2 = new Set([
|
|
171
|
+
"FunctionDeclaration",
|
|
172
|
+
"FunctionExpression",
|
|
173
|
+
"ArrowFunctionExpression"
|
|
174
|
+
]);
|
|
175
|
+
/**
|
|
176
|
+
* @param {unknown} node
|
|
177
|
+
* @returns {node is ESTNode & { type: string }}
|
|
178
|
+
*/
|
|
179
|
+
const isNode$2 = (node) => Boolean(node && typeof node === "object" && "type" in node);
|
|
180
|
+
/**
|
|
181
|
+
* @param {unknown} node
|
|
182
|
+
* @returns {node is FunctionLikeNode}
|
|
183
|
+
*/
|
|
184
|
+
const isFunctionLike$2 = (node) => isNode$2(node) && FUNCTION_NODE_TYPES$2.has(node.type);
|
|
185
|
+
/**
|
|
186
|
+
* Check if a function name follows React component naming convention (PascalCase)
|
|
187
|
+
* @param {unknown} name
|
|
188
|
+
* @returns {name is string}
|
|
189
|
+
*/
|
|
190
|
+
const isComponentName = (name) => typeof name === "string" && /^[A-Z]/.test(name);
|
|
191
|
+
/**
|
|
192
|
+
* Get the name of a function node
|
|
193
|
+
* @param {FunctionLikeNode} node
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
const getFunctionName$1 = (node) => {
|
|
197
|
+
if (node.type === "FunctionDeclaration" && node.id && node.id.type === "Identifier") return node.id.name;
|
|
198
|
+
if ((node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.id) {
|
|
199
|
+
if (node.id.type === "Identifier") return node.id.name;
|
|
200
|
+
}
|
|
201
|
+
const parent = node.parent;
|
|
202
|
+
if (!parent || !isNode$2(parent)) return "";
|
|
203
|
+
if (parent.type === "VariableDeclarator") return parent.id && parent.id.type === "Identifier" ? parent.id.name : "";
|
|
204
|
+
if (parent.type === "AssignmentExpression") return parent.left && parent.left.type === "Identifier" ? parent.left.name : "";
|
|
205
|
+
if (parent.type === "Property" || parent.type === "MethodDefinition") return parent.key && parent.key.type === "Identifier" ? parent.key.name : "";
|
|
206
|
+
return "";
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Check if a node is a JSX element or fragment
|
|
210
|
+
* @param {ESTNode | null | undefined} node
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
const isJSXNode = (node) => {
|
|
214
|
+
if (!node || !isNode$2(node)) return false;
|
|
215
|
+
return node.type === "JSXElement" || node.type === "JSXFragment";
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Check if a NewExpression is creating a Date instance
|
|
219
|
+
* @param {NewExpressionNode} node
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
const isDateInstantiation = (node) => {
|
|
223
|
+
if (node.callee.type === "Identifier" && node.callee.name === "Date") return true;
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
const rule$7 = defineRule({
|
|
227
|
+
meta: {
|
|
228
|
+
type: "problem",
|
|
229
|
+
docs: {
|
|
230
|
+
description: "Disallow Date instantiation in the top scope of React components. Date instances declared on every render are bad because they change every render.",
|
|
231
|
+
recommended: true
|
|
232
|
+
},
|
|
233
|
+
schema: []
|
|
234
|
+
},
|
|
235
|
+
createOnce(context) {
|
|
236
|
+
/** @type {FunctionContext[]} */
|
|
237
|
+
const functionStack = [];
|
|
238
|
+
const currentFunction = () => functionStack[functionStack.length - 1] ?? null;
|
|
239
|
+
/**
|
|
240
|
+
* @param {FunctionLikeNode} node
|
|
241
|
+
*/
|
|
242
|
+
const enterFunction = (node) => {
|
|
243
|
+
/** @type {FunctionContext} */
|
|
244
|
+
const fnCtx = {
|
|
245
|
+
node,
|
|
246
|
+
parent: currentFunction(),
|
|
247
|
+
name: getFunctionName$1(node),
|
|
248
|
+
returnsJsx: false,
|
|
249
|
+
dateInstantiations: []
|
|
250
|
+
};
|
|
251
|
+
functionStack.push(fnCtx);
|
|
252
|
+
if (node.type === "ArrowFunctionExpression" && node.body && node.body.type !== "BlockStatement") {
|
|
253
|
+
if (isJSXNode(node.body)) fnCtx.returnsJsx = true;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const exitFunction = () => {
|
|
257
|
+
const fnCtx = functionStack.pop();
|
|
258
|
+
if (!fnCtx) return;
|
|
259
|
+
if (!fnCtx.returnsJsx) return;
|
|
260
|
+
if (!isComponentName(fnCtx.name)) return;
|
|
261
|
+
for (const dateNode of fnCtx.dateInstantiations) context.report({
|
|
262
|
+
node: dateNode,
|
|
263
|
+
message: `Avoid instantiating Date in the top scope of component '${fnCtx.name}'. Date instances change on every render. Move it inside an effect, event handler, or use useMemo/useCallback.`
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
/** @param {ReturnStatementNode} node */
|
|
267
|
+
const handleReturnStatement = (node) => {
|
|
268
|
+
const fnCtx = currentFunction();
|
|
269
|
+
if (!fnCtx) return;
|
|
270
|
+
const argument = node.argument;
|
|
271
|
+
if (!argument) return;
|
|
272
|
+
if (isJSXNode(argument)) fnCtx.returnsJsx = true;
|
|
273
|
+
};
|
|
274
|
+
/** @param {NewExpressionNode} node */
|
|
275
|
+
const handleNewExpression = (node) => {
|
|
276
|
+
if (!isDateInstantiation(node)) return;
|
|
277
|
+
const fnCtx = currentFunction();
|
|
278
|
+
if (!fnCtx) return;
|
|
279
|
+
fnCtx.dateInstantiations.push(node);
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
FunctionDeclaration(node) {
|
|
283
|
+
if (isFunctionLike$2(node)) enterFunction(node);
|
|
284
|
+
},
|
|
285
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
286
|
+
FunctionExpression(node) {
|
|
287
|
+
if (isFunctionLike$2(node)) enterFunction(node);
|
|
288
|
+
},
|
|
289
|
+
"FunctionExpression:exit": exitFunction,
|
|
290
|
+
ArrowFunctionExpression(node) {
|
|
291
|
+
if (isFunctionLike$2(node)) enterFunction(node);
|
|
292
|
+
},
|
|
293
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
294
|
+
ReturnStatement(node) {
|
|
295
|
+
if (node.type === "ReturnStatement") handleReturnStatement(node);
|
|
296
|
+
},
|
|
297
|
+
NewExpression(node) {
|
|
298
|
+
if (node.type === "NewExpression") handleNewExpression(node);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
const noComponentDateInstantiationRule = rule$7;
|
|
304
|
+
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/oxlint-plugins/no-emoji.js
|
|
307
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
308
|
+
/**
|
|
309
|
+
* Regex pattern to match emojis
|
|
310
|
+
* Covers most common emoji ranges in Unicode
|
|
311
|
+
*/
|
|
312
|
+
const EMOJI_REGEX = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{FE00}-\u{FE0F}\u{203C}\u{2049}\u{20E3}\u{2139}\u{2194}-\u{2199}\u{21A9}-\u{21AA}\u{231A}-\u{231B}\u{2328}\u{23CF}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{24C2}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2600}-\u{2604}\u{260E}\u{2611}\u{2614}-\u{2615}\u{2618}\u{261D}\u{2620}\u{2622}-\u{2623}\u{2626}\u{262A}\u{262E}-\u{262F}\u{2638}-\u{263A}\u{2640}\u{2642}\u{2648}-\u{2653}\u{265F}-\u{2660}\u{2663}\u{2665}-\u{2666}\u{2668}\u{267B}\u{267E}-\u{267F}\u{2692}-\u{2697}\u{2699}\u{269B}-\u{269C}\u{26A0}-\u{26A1}\u{26A7}\u{26AA}-\u{26AB}\u{26B0}-\u{26B1}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26C8}\u{26CE}\u{26CF}\u{26D1}\u{26D3}-\u{26D4}\u{26E9}-\u{26EA}\u{26F0}-\u{26F5}\u{26F7}-\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270D}\u{270F}\u{2712}\u{2714}\u{2716}\u{271D}\u{2721}\u{2728}\u{2733}-\u{2734}\u{2744}\u{2747}\u{274C}\u{274E}\u{2753}-\u{2755}\u{2757}\u{2763}-\u{2764}\u{2795}-\u{2797}\u{27A1}\u{27B0}\u{27BF}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu;
|
|
313
|
+
/**
|
|
314
|
+
* Find emojis in a string
|
|
315
|
+
* @param {string} text
|
|
316
|
+
* @returns {RegExpMatchArray | null}
|
|
317
|
+
*/
|
|
318
|
+
const findEmojis = (text) => {
|
|
319
|
+
return text.match(EMOJI_REGEX);
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Get a preview of the emoji found
|
|
323
|
+
* @param {string} text
|
|
324
|
+
* @returns {string}
|
|
325
|
+
*/
|
|
326
|
+
const getEmojiPreview = (text) => {
|
|
327
|
+
const emojis = findEmojis(text);
|
|
328
|
+
if (!emojis || emojis.length === 0) return "";
|
|
329
|
+
const uniqueEmojis = [...new Set(emojis)];
|
|
330
|
+
const preview = uniqueEmojis.slice(0, 3).join(" ");
|
|
331
|
+
return uniqueEmojis.length > 3 ? `${preview} ...` : preview;
|
|
332
|
+
};
|
|
333
|
+
const rule$6 = defineRule({
|
|
334
|
+
meta: {
|
|
335
|
+
type: "problem",
|
|
336
|
+
docs: {
|
|
337
|
+
description: "Disallow the use of emojis in code. Use icons from a component library instead.",
|
|
338
|
+
recommended: true
|
|
339
|
+
},
|
|
340
|
+
schema: []
|
|
341
|
+
},
|
|
342
|
+
createOnce(context) {
|
|
343
|
+
return {
|
|
344
|
+
StringLiteral(node) {
|
|
345
|
+
if (node.type !== "StringLiteral") return;
|
|
346
|
+
const emojis = findEmojis(node.value);
|
|
347
|
+
if (emojis && emojis.length > 0) {
|
|
348
|
+
const preview = getEmojiPreview(node.value);
|
|
349
|
+
context.report({
|
|
350
|
+
node,
|
|
351
|
+
message: `Emojis are not allowed in code. Found: ${preview}. Use icons from a component library instead.`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
TemplateLiteral(node) {
|
|
356
|
+
if (node.type !== "TemplateLiteral") return;
|
|
357
|
+
for (const quasi of node.quasis) {
|
|
358
|
+
if (quasi.type !== "TemplateElement") continue;
|
|
359
|
+
const text = quasi.value.raw;
|
|
360
|
+
const emojis = findEmojis(text);
|
|
361
|
+
if (emojis && emojis.length > 0) {
|
|
362
|
+
const preview = getEmojiPreview(text);
|
|
363
|
+
context.report({
|
|
364
|
+
node: quasi,
|
|
365
|
+
message: `Emojis are not allowed in code. Found: ${preview}. Use icons from a component library instead.`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
JSXText(node) {
|
|
371
|
+
if (node.type !== "JSXText") return;
|
|
372
|
+
const emojis = findEmojis(node.value);
|
|
373
|
+
if (emojis && emojis.length > 0) {
|
|
374
|
+
const preview = getEmojiPreview(node.value);
|
|
375
|
+
context.report({
|
|
376
|
+
node,
|
|
377
|
+
message: `Emojis are not allowed in code. Found: ${preview}. Use icons from a component library instead.`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const noEmojiRule = rule$6;
|
|
385
|
+
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/oxlint-plugins/no-finally.js
|
|
388
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
389
|
+
const rule$5 = defineRule({
|
|
390
|
+
meta: {
|
|
391
|
+
type: "problem",
|
|
392
|
+
docs: {
|
|
393
|
+
description: "Disallow 'finally' blocks in try/catch/finally statements",
|
|
394
|
+
recommended: true
|
|
395
|
+
},
|
|
396
|
+
schema: []
|
|
397
|
+
},
|
|
398
|
+
createOnce(context) {
|
|
399
|
+
return { TryStatement(node) {
|
|
400
|
+
if (node.type !== "TryStatement") return;
|
|
401
|
+
if (node.finalizer) context.report({
|
|
402
|
+
node: node.finalizer,
|
|
403
|
+
message: "Use of 'finally' blocks is disallowed. Handle cleanup explicitly in try/catch blocks instead."
|
|
404
|
+
});
|
|
405
|
+
} };
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
const noFinallyRule = rule$5;
|
|
409
|
+
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/oxlint-plugins/no-inline-components.js
|
|
412
|
+
/**
|
|
413
|
+
* @typedef {import("oxlint").Context} RuleContext
|
|
414
|
+
* @typedef {import("oxlint").ESTree.Node} ESTNode
|
|
415
|
+
* @typedef {import("oxlint").ESTree.Expression} ESTExpression
|
|
416
|
+
* @typedef {import("oxlint").ESTree.Pattern} ESTPattern
|
|
417
|
+
* @typedef {import("oxlint").ESTree.ReturnStatement} ReturnStatementNode
|
|
418
|
+
* @typedef {import("oxlint").ESTree.VariableDeclarator} VariableDeclaratorNode
|
|
419
|
+
* @typedef {import("oxlint").ESTree.AssignmentExpression} AssignmentExpressionNode
|
|
420
|
+
* @typedef {import("oxlint").ESTree.Function | import("oxlint").ESTree.ArrowFunctionExpression} FunctionLikeNode
|
|
421
|
+
*/
|
|
422
|
+
/**
|
|
423
|
+
* @typedef {object} RecordedAssignment
|
|
424
|
+
* @property {ESTExpression} node
|
|
425
|
+
* @property {string[]} names
|
|
426
|
+
*/
|
|
427
|
+
/**
|
|
428
|
+
* @typedef {object} NestedFunctionRecord
|
|
429
|
+
* @property {FunctionLikeNode} node
|
|
430
|
+
* @property {string} name
|
|
431
|
+
*/
|
|
432
|
+
/**
|
|
433
|
+
* @typedef {object} FunctionContext
|
|
434
|
+
* @property {FunctionLikeNode} node
|
|
435
|
+
* @property {FunctionContext | null} parent
|
|
436
|
+
* @property {string} name
|
|
437
|
+
* @property {boolean} returnsJsx
|
|
438
|
+
* @property {Set<string>} jsxBindingNames
|
|
439
|
+
* @property {RecordedAssignment[]} jsxAssignments
|
|
440
|
+
* @property {NestedFunctionRecord[]} nestedJsxChildren
|
|
441
|
+
*/
|
|
442
|
+
const JSX_NODE_TYPES$1 = new Set(["JSXElement", "JSXFragment"]);
|
|
443
|
+
const FUNCTION_NODE_TYPES$1 = new Set([
|
|
444
|
+
"FunctionDeclaration",
|
|
445
|
+
"FunctionExpression",
|
|
446
|
+
"ArrowFunctionExpression"
|
|
447
|
+
]);
|
|
448
|
+
/**
|
|
449
|
+
* @param {unknown} node
|
|
450
|
+
* @returns {node is ESTNode & { type: string }}
|
|
451
|
+
*/
|
|
452
|
+
const isNode$1 = (node) => Boolean(node && typeof node === "object" && "type" in node);
|
|
453
|
+
/**
|
|
454
|
+
* @param {unknown} node
|
|
455
|
+
* @returns {node is FunctionLikeNode}
|
|
456
|
+
*/
|
|
457
|
+
const isFunctionLike$1 = (node) => isNode$1(node) && FUNCTION_NODE_TYPES$1.has(node.type);
|
|
458
|
+
/**
|
|
459
|
+
* @param {FunctionLikeNode} node
|
|
460
|
+
*/
|
|
461
|
+
const isFunctionUsedAsJsxProp = (node) => {
|
|
462
|
+
const checkJsxProp = (current) => {
|
|
463
|
+
if (!current) return false;
|
|
464
|
+
if (current.type === "JSXAttribute") return true;
|
|
465
|
+
if (isFunctionLike$1(current)) return false;
|
|
466
|
+
return checkJsxProp(isNode$1(current) ? current.parent ?? null : null);
|
|
467
|
+
};
|
|
468
|
+
return checkJsxProp(isNode$1(node) ? node.parent ?? null : null);
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* @param {FunctionLikeNode} node
|
|
472
|
+
*/
|
|
473
|
+
const isFunctionImmediatelyInvoked = (node) => {
|
|
474
|
+
const parent = isNode$1(node) ? node.parent ?? null : null;
|
|
475
|
+
if (!parent) return false;
|
|
476
|
+
if (parent.type === "CallExpression" && parent.callee === node) return true;
|
|
477
|
+
return false;
|
|
478
|
+
};
|
|
479
|
+
/**
|
|
480
|
+
* @param {ESTExpression | null | undefined} root
|
|
481
|
+
*/
|
|
482
|
+
const expressionContainsJsx$1 = (root) => {
|
|
483
|
+
if (!root || !isNode$1(root)) return false;
|
|
484
|
+
const stack = [root];
|
|
485
|
+
while (stack.length > 0) {
|
|
486
|
+
const current = stack.pop();
|
|
487
|
+
if (!current || !isNode$1(current)) continue;
|
|
488
|
+
if (JSX_NODE_TYPES$1.has(current.type)) return true;
|
|
489
|
+
if (FUNCTION_NODE_TYPES$1.has(current.type) && current !== root) continue;
|
|
490
|
+
for (const key of Object.keys(current)) {
|
|
491
|
+
if (key === "parent") continue;
|
|
492
|
+
const value = current[key];
|
|
493
|
+
if (!value) continue;
|
|
494
|
+
if (Array.isArray(value)) {
|
|
495
|
+
for (const element of value) if (isNode$1(element)) stack.push(element);
|
|
496
|
+
} else if (isNode$1(value)) stack.push(value);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* @param {ESTExpression | null | undefined} root
|
|
503
|
+
* @param {Set<string>} bindingNames
|
|
504
|
+
*/
|
|
505
|
+
const expressionProducesJsx = (root, bindingNames) => {
|
|
506
|
+
if (!root) return false;
|
|
507
|
+
if (expressionContainsJsx$1(root)) return true;
|
|
508
|
+
if (!isNode$1(root)) return false;
|
|
509
|
+
const type = root.type;
|
|
510
|
+
if (type === "Identifier") return bindingNames.has(root.name);
|
|
511
|
+
if (type === "ConditionalExpression") return expressionProducesJsx(root.consequent, bindingNames) || expressionProducesJsx(root.alternate, bindingNames);
|
|
512
|
+
if (type === "LogicalExpression") return expressionProducesJsx(root.left, bindingNames) || expressionProducesJsx(root.right, bindingNames);
|
|
513
|
+
if (type === "SequenceExpression") {
|
|
514
|
+
const expressions = root.expressions ?? [];
|
|
515
|
+
return expressionProducesJsx(expressions[expressions.length - 1] ?? null, bindingNames);
|
|
516
|
+
}
|
|
517
|
+
if (type === "ArrayExpression") return root.elements.some((element) => {
|
|
518
|
+
if (!element) return false;
|
|
519
|
+
if (element.type === "SpreadElement") return expressionProducesJsx(element.argument, bindingNames);
|
|
520
|
+
return expressionProducesJsx(element, bindingNames);
|
|
521
|
+
});
|
|
522
|
+
if (type === "ParenthesizedExpression") return expressionProducesJsx(root.expression, bindingNames);
|
|
523
|
+
if (type === "AwaitExpression" || type === "UnaryExpression" || type === "UpdateExpression") return expressionProducesJsx(root.argument, bindingNames);
|
|
524
|
+
if (type === "TSAsExpression" || type === "TSTypeAssertion" || type === "TSNonNullExpression" || type === "ChainExpression") return expressionProducesJsx(root.expression, bindingNames);
|
|
525
|
+
if (type === "CallExpression") return expressionProducesJsx(root.callee, bindingNames);
|
|
526
|
+
if (type === "MemberExpression") return expressionProducesJsx(root.object, bindingNames);
|
|
527
|
+
return false;
|
|
528
|
+
};
|
|
529
|
+
/**
|
|
530
|
+
* @param {ESTPattern | null | undefined} pattern
|
|
531
|
+
* @param {string[]} names
|
|
532
|
+
*/
|
|
533
|
+
const collectBindingNames = (pattern, names) => {
|
|
534
|
+
if (!pattern || !isNode$1(pattern)) return;
|
|
535
|
+
const type = pattern.type;
|
|
536
|
+
if (type === "Identifier") {
|
|
537
|
+
names.push(pattern.name);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (type === "ArrayPattern") {
|
|
541
|
+
for (const element of pattern.elements) {
|
|
542
|
+
if (!element) continue;
|
|
543
|
+
if (element.type === "RestElement") collectBindingNames(element.argument, names);
|
|
544
|
+
else collectBindingNames(element, names);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (type === "ObjectPattern") {
|
|
549
|
+
for (const property of pattern.properties) {
|
|
550
|
+
if (!property) continue;
|
|
551
|
+
if (property.type === "Property") collectBindingNames(property.value, names);
|
|
552
|
+
else if (property.type === "RestElement") collectBindingNames(property.argument, names);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (type === "AssignmentPattern") {
|
|
557
|
+
collectBindingNames(pattern.left, names);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (type === "RestElement") collectBindingNames(pattern.argument, names);
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* @param {FunctionLikeNode} node
|
|
564
|
+
*/
|
|
565
|
+
const getFunctionName = (node) => {
|
|
566
|
+
if (node.type === "FunctionDeclaration" && node.id && node.id.type === "Identifier") return node.id.name;
|
|
567
|
+
if ((node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.id) {
|
|
568
|
+
if (node.id.type === "Identifier") return node.id.name;
|
|
569
|
+
}
|
|
570
|
+
const parent = node.parent;
|
|
571
|
+
if (!parent || !isNode$1(parent)) return "";
|
|
572
|
+
if (parent.type === "VariableDeclarator") return parent.id && parent.id.type === "Identifier" ? parent.id.name : "";
|
|
573
|
+
if (parent.type === "AssignmentExpression") return parent.left && parent.left.type === "Identifier" ? parent.left.name : "";
|
|
574
|
+
if (parent.type === "Property" || parent.type === "MethodDefinition") return parent.key && parent.key.type === "Identifier" ? parent.key.name : "";
|
|
575
|
+
if (parent.type === "CallExpression") {
|
|
576
|
+
const callParent = parent.parent;
|
|
577
|
+
if (callParent && isNode$1(callParent)) {
|
|
578
|
+
if (callParent.type === "VariableDeclarator") return callParent.id && callParent.id.type === "Identifier" ? callParent.id.name : "";
|
|
579
|
+
if (callParent.type === "AssignmentExpression") return callParent.left && callParent.left.type === "Identifier" ? callParent.left.name : "";
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return "";
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* @param {string} name
|
|
586
|
+
*/
|
|
587
|
+
const describeFunction = (name) => name ? `function '${name}'` : "this function";
|
|
588
|
+
/**
|
|
589
|
+
* @param {string} name
|
|
590
|
+
*/
|
|
591
|
+
const describeNested = (name) => name ? `function '${name}'` : "an anonymous function";
|
|
592
|
+
/**
|
|
593
|
+
* @param {string[]} names
|
|
594
|
+
* @param {string} fnName
|
|
595
|
+
*/
|
|
596
|
+
const createAssignmentMessage = (names, fnName) => {
|
|
597
|
+
return `Avoid storing JSX in ${names.length === 0 ? "local variables" : names.length === 1 ? `local '${names[0]}'` : `locals ${names.map((name) => `'${name}'`).join(", ")}`} inside ${describeFunction(fnName)}; return the JSX directly instead.`;
|
|
598
|
+
};
|
|
599
|
+
/**
|
|
600
|
+
* @param {string} childName
|
|
601
|
+
* @param {string} parentName
|
|
602
|
+
*/
|
|
603
|
+
const createNestedFunctionMessage = (childName, parentName) => `JSX-returning ${describeNested(childName)} should not be declared inside ${describeFunction(parentName)}. Extract it to module scope.`;
|
|
604
|
+
/**
|
|
605
|
+
* @param {string} name
|
|
606
|
+
*/
|
|
607
|
+
const createIIFEMessage = (name) => `JSX-returning ${describeNested(name)} should not be declared as an immediately invoked function expression (IIFE). Extract it to a named function at module scope.`;
|
|
608
|
+
const rule$4 = defineRule({
|
|
609
|
+
meta: {
|
|
610
|
+
type: "problem",
|
|
611
|
+
docs: {
|
|
612
|
+
description: "Disallow JSX-returning functions and JSX-valued assignments within other functions that also return JSX.",
|
|
613
|
+
recommended: false
|
|
614
|
+
},
|
|
615
|
+
schema: []
|
|
616
|
+
},
|
|
617
|
+
createOnce(context) {
|
|
618
|
+
/** @type {FunctionContext[]} */
|
|
619
|
+
const functionStack = [];
|
|
620
|
+
const currentFunction = () => functionStack[functionStack.length - 1] ?? null;
|
|
621
|
+
/**
|
|
622
|
+
* @param {FunctionLikeNode} node
|
|
623
|
+
*/
|
|
624
|
+
const enterFunction = (node) => {
|
|
625
|
+
/** @type {FunctionContext} */
|
|
626
|
+
const fnCtx = {
|
|
627
|
+
node,
|
|
628
|
+
parent: currentFunction(),
|
|
629
|
+
name: getFunctionName(node),
|
|
630
|
+
returnsJsx: false,
|
|
631
|
+
jsxBindingNames: /* @__PURE__ */ new Set(),
|
|
632
|
+
jsxAssignments: [],
|
|
633
|
+
nestedJsxChildren: []
|
|
634
|
+
};
|
|
635
|
+
functionStack.push(fnCtx);
|
|
636
|
+
if (node.type === "ArrowFunctionExpression" && node.body && node.body.type !== "BlockStatement") {
|
|
637
|
+
if (expressionProducesJsx(node.body, fnCtx.jsxBindingNames)) fnCtx.returnsJsx = true;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const exitFunction = () => {
|
|
641
|
+
const fnCtx = functionStack.pop();
|
|
642
|
+
if (!fnCtx) return;
|
|
643
|
+
if (fnCtx.returnsJsx && isFunctionImmediatelyInvoked(fnCtx.node)) {
|
|
644
|
+
context.report({
|
|
645
|
+
node: fnCtx.node,
|
|
646
|
+
message: createIIFEMessage(fnCtx.name)
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (fnCtx.parent && fnCtx.returnsJsx && fnCtx.name && !isFunctionUsedAsJsxProp(fnCtx.node)) fnCtx.parent.nestedJsxChildren.push({
|
|
651
|
+
node: fnCtx.node,
|
|
652
|
+
name: fnCtx.name
|
|
653
|
+
});
|
|
654
|
+
if (!fnCtx.returnsJsx) return;
|
|
655
|
+
for (const assignment of fnCtx.jsxAssignments) context.report({
|
|
656
|
+
node: assignment.node,
|
|
657
|
+
message: createAssignmentMessage(assignment.names, fnCtx.name)
|
|
658
|
+
});
|
|
659
|
+
for (const nested of fnCtx.nestedJsxChildren) context.report({
|
|
660
|
+
node: nested.node,
|
|
661
|
+
message: createNestedFunctionMessage(nested.name, fnCtx.name)
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
/** @param {ReturnStatementNode} node */
|
|
665
|
+
const handleReturnStatement = (node) => {
|
|
666
|
+
const fnCtx = currentFunction();
|
|
667
|
+
if (!fnCtx) return;
|
|
668
|
+
const argument = node.argument;
|
|
669
|
+
if (!argument || isFunctionLike$1(argument)) return;
|
|
670
|
+
if (expressionProducesJsx(argument, fnCtx.jsxBindingNames)) fnCtx.returnsJsx = true;
|
|
671
|
+
};
|
|
672
|
+
/** @param {VariableDeclaratorNode} node */
|
|
673
|
+
const handleVariableDeclarator = (node) => {
|
|
674
|
+
const fnCtx = currentFunction();
|
|
675
|
+
if (!fnCtx) return;
|
|
676
|
+
const init = node.init;
|
|
677
|
+
if (!init || isFunctionLike$1(init)) return;
|
|
678
|
+
if (!expressionContainsJsx$1(init)) return;
|
|
679
|
+
const names = [];
|
|
680
|
+
collectBindingNames(node.id, names);
|
|
681
|
+
for (const name of names) fnCtx.jsxBindingNames.add(name);
|
|
682
|
+
fnCtx.jsxAssignments.push({
|
|
683
|
+
node: init,
|
|
684
|
+
names
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
/** @param {AssignmentExpressionNode} node */
|
|
688
|
+
const handleAssignmentExpression = (node) => {
|
|
689
|
+
if (node.operator !== "=") return;
|
|
690
|
+
const fnCtx = currentFunction();
|
|
691
|
+
if (!fnCtx) return;
|
|
692
|
+
const right = node.right;
|
|
693
|
+
if (!right || isFunctionLike$1(right)) return;
|
|
694
|
+
if (!expressionContainsJsx$1(right)) return;
|
|
695
|
+
const names = [];
|
|
696
|
+
if (node.left && node.left.type === "Identifier") {
|
|
697
|
+
names.push(node.left.name);
|
|
698
|
+
fnCtx.jsxBindingNames.add(node.left.name);
|
|
699
|
+
}
|
|
700
|
+
fnCtx.jsxAssignments.push({
|
|
701
|
+
node: right,
|
|
702
|
+
names
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
/**
|
|
706
|
+
* @param {import("oxlint").ESTree.CallExpression} node
|
|
707
|
+
*/
|
|
708
|
+
const handleCallExpression = (node) => {
|
|
709
|
+
const fnCtx = currentFunction();
|
|
710
|
+
if (!fnCtx) return;
|
|
711
|
+
if (node.callee && node.callee.type === "MemberExpression" && node.callee.property && node.callee.property.type === "Identifier" && node.callee.property.name === "push") {
|
|
712
|
+
const arrayObject = node.callee.object;
|
|
713
|
+
if (arrayObject && arrayObject.type === "Identifier") {
|
|
714
|
+
const arrayName = arrayObject.name;
|
|
715
|
+
if (node.arguments.some((arg) => {
|
|
716
|
+
if (!arg || arg.type === "SpreadElement") return false;
|
|
717
|
+
return expressionContainsJsx$1(arg);
|
|
718
|
+
})) {
|
|
719
|
+
fnCtx.jsxBindingNames.add(arrayName);
|
|
720
|
+
fnCtx.jsxAssignments.push({
|
|
721
|
+
node,
|
|
722
|
+
names: [arrayName]
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return {
|
|
729
|
+
FunctionDeclaration(node) {
|
|
730
|
+
if (isFunctionLike$1(node)) enterFunction(node);
|
|
731
|
+
},
|
|
732
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
733
|
+
FunctionExpression(node) {
|
|
734
|
+
if (isFunctionLike$1(node)) enterFunction(node);
|
|
735
|
+
},
|
|
736
|
+
"FunctionExpression:exit": exitFunction,
|
|
737
|
+
ArrowFunctionExpression(node) {
|
|
738
|
+
if (isFunctionLike$1(node)) enterFunction(node);
|
|
739
|
+
},
|
|
740
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
741
|
+
ReturnStatement(node) {
|
|
742
|
+
if (node.type === "ReturnStatement") handleReturnStatement(node);
|
|
743
|
+
},
|
|
744
|
+
VariableDeclarator(node) {
|
|
745
|
+
if (node.type === "VariableDeclarator") handleVariableDeclarator(node);
|
|
746
|
+
},
|
|
747
|
+
AssignmentExpression(node) {
|
|
748
|
+
if (node.type === "AssignmentExpression") handleAssignmentExpression(node);
|
|
749
|
+
},
|
|
750
|
+
CallExpression(node) {
|
|
751
|
+
if (node.type === "CallExpression") handleCallExpression(node);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
const noInlineComponentsRule = rule$4;
|
|
757
|
+
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/oxlint-plugins/no-react-namespace.js
|
|
760
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
761
|
+
/**
|
|
762
|
+
* Check if a MemberExpression is accessing the React namespace
|
|
763
|
+
* @param {ESTNode} node
|
|
764
|
+
* @returns {boolean}
|
|
765
|
+
*/
|
|
766
|
+
const isReactNamespaceAccess = (node) => {
|
|
767
|
+
if (node.type !== "MemberExpression") return false;
|
|
768
|
+
const object = node.object;
|
|
769
|
+
if (!object || object.type !== "Identifier" || object.name !== "React") return false;
|
|
770
|
+
return true;
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* Check if this is a type annotation context (TypeScript)
|
|
774
|
+
* @param {ESTNode} node
|
|
775
|
+
* @returns {boolean}
|
|
776
|
+
*/
|
|
777
|
+
const isTypeContext = (node) => {
|
|
778
|
+
const checkParent = (current) => {
|
|
779
|
+
if (!current) return false;
|
|
780
|
+
if (new Set([
|
|
781
|
+
"TSTypeReference",
|
|
782
|
+
"TSTypeAnnotation",
|
|
783
|
+
"TSTypeParameterInstantiation",
|
|
784
|
+
"TSInterfaceHeritage",
|
|
785
|
+
"TSTypeQuery",
|
|
786
|
+
"TSTypeAliasDeclaration",
|
|
787
|
+
"TSInterfaceDeclaration",
|
|
788
|
+
"TSTypeLiteral",
|
|
789
|
+
"TSPropertySignature",
|
|
790
|
+
"TSIndexSignature",
|
|
791
|
+
"TSMethodSignature",
|
|
792
|
+
"TSCallSignatureDeclaration",
|
|
793
|
+
"TSConstructSignatureDeclaration",
|
|
794
|
+
"TSExpressionWithTypeArguments"
|
|
795
|
+
]).has(current.type)) return true;
|
|
796
|
+
if (current.type === "ExpressionStatement" || current.type === "VariableDeclarator" || current.type === "CallExpression" || current.type === "ReturnStatement") return false;
|
|
797
|
+
return checkParent(current.parent);
|
|
798
|
+
};
|
|
799
|
+
return checkParent(node.parent);
|
|
800
|
+
};
|
|
801
|
+
const rule$3 = defineRule({
|
|
802
|
+
meta: {
|
|
803
|
+
type: "problem",
|
|
804
|
+
docs: {
|
|
805
|
+
description: "Disallow using the React namespace for accessing React APIs. Use destructured imports instead (e.g., import { useState } from 'react').",
|
|
806
|
+
recommended: true
|
|
807
|
+
},
|
|
808
|
+
schema: []
|
|
809
|
+
},
|
|
810
|
+
createOnce(context) {
|
|
811
|
+
return { MemberExpression(node) {
|
|
812
|
+
if (node.type !== "MemberExpression") return;
|
|
813
|
+
if (!isReactNamespaceAccess(node)) return;
|
|
814
|
+
if (isTypeContext(node)) return;
|
|
815
|
+
const propertyName = node.property && node.property.type === "Identifier" ? node.property.name : "property";
|
|
816
|
+
context.report({
|
|
817
|
+
node,
|
|
818
|
+
message: `Avoid using 'React.${propertyName}'. Import '${propertyName}' directly from 'react' instead (e.g., import { ${propertyName} } from 'react').`
|
|
819
|
+
});
|
|
820
|
+
} };
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
const noReactNamespaceRule = rule$3;
|
|
824
|
+
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/oxlint-plugins/no-switch-plugin.js
|
|
827
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
828
|
+
const noSwitchRule = defineRule({
|
|
829
|
+
meta: {
|
|
830
|
+
type: "problem",
|
|
831
|
+
docs: {
|
|
832
|
+
description: "Disallow switch/case statements",
|
|
833
|
+
recommended: true
|
|
834
|
+
},
|
|
835
|
+
schema: []
|
|
836
|
+
},
|
|
837
|
+
createOnce(context) {
|
|
838
|
+
return { SwitchStatement(node) {
|
|
839
|
+
if (node.type !== "SwitchStatement") return;
|
|
840
|
+
context.report({
|
|
841
|
+
node,
|
|
842
|
+
message: "Use of switch/case is disallowed. Use object map or if/else instead."
|
|
843
|
+
});
|
|
844
|
+
} };
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/oxlint-plugins/no-top-level-let.js
|
|
850
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
851
|
+
/**
|
|
852
|
+
* Get the enclosing function node for a given node
|
|
853
|
+
* @param {ESTNode} node
|
|
854
|
+
* @returns {ESTNode | null}
|
|
855
|
+
*/
|
|
856
|
+
const getEnclosingFunction = (node) => {
|
|
857
|
+
const findFunction = (current) => {
|
|
858
|
+
if (!current) return null;
|
|
859
|
+
if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") return current;
|
|
860
|
+
return findFunction(current.parent);
|
|
861
|
+
};
|
|
862
|
+
return findFunction(node.parent);
|
|
863
|
+
};
|
|
864
|
+
/**
|
|
865
|
+
* Check if a node is inside a loop
|
|
866
|
+
* @param {ESTNode} node
|
|
867
|
+
* @param {ESTNode} stopAt - Stop searching when we reach this node
|
|
868
|
+
* @returns {boolean}
|
|
869
|
+
*/
|
|
870
|
+
const isInsideLoop = (node, stopAt) => {
|
|
871
|
+
const checkLoop = (current) => {
|
|
872
|
+
if (!current || current === stopAt) return false;
|
|
873
|
+
if (current.type === "ForStatement" || current.type === "ForInStatement" || current.type === "ForOfStatement" || current.type === "WhileStatement" || current.type === "DoWhileStatement") return true;
|
|
874
|
+
return checkLoop(current.parent);
|
|
875
|
+
};
|
|
876
|
+
return checkLoop(node.parent);
|
|
877
|
+
};
|
|
878
|
+
const rule$2 = defineRule({
|
|
879
|
+
meta: {
|
|
880
|
+
type: "problem",
|
|
881
|
+
docs: {
|
|
882
|
+
description: "Disallow top-level `let` declarations inside functions to prevent conditional reassignment.",
|
|
883
|
+
recommended: false
|
|
884
|
+
},
|
|
885
|
+
schema: []
|
|
886
|
+
},
|
|
887
|
+
createOnce(context) {
|
|
888
|
+
return { VariableDeclaration(rawNode) {
|
|
889
|
+
if (rawNode.type !== "VariableDeclaration") return;
|
|
890
|
+
const node = rawNode;
|
|
891
|
+
if (node.kind !== "let") return;
|
|
892
|
+
const fn = getEnclosingFunction(node);
|
|
893
|
+
if (!fn || fn.type !== "FunctionDeclaration" && fn.type !== "FunctionExpression" && fn.type !== "ArrowFunctionExpression") return;
|
|
894
|
+
const parent = node.parent;
|
|
895
|
+
if (!parent || parent.type !== "BlockStatement" || parent.parent !== fn) return;
|
|
896
|
+
if (isInsideLoop(node, fn)) return;
|
|
897
|
+
context.report({
|
|
898
|
+
node,
|
|
899
|
+
message: "Avoid using `let` at the top level of functions; prefer `const` with extracted functions to avoid conditional reassignment. Extract conditional logic into a separate function that returns the appropriate value."
|
|
900
|
+
});
|
|
901
|
+
} };
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
const noTopLevelLetRule = rule$2;
|
|
905
|
+
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/oxlint-plugins/no-type-cast.js
|
|
908
|
+
/** @typedef {import("oxlint").ESTree.Node} ESTNode */
|
|
909
|
+
const rule$1 = defineRule({
|
|
910
|
+
meta: {
|
|
911
|
+
type: "problem",
|
|
912
|
+
docs: {
|
|
913
|
+
description: "Disallow TypeScript type assertions (`as` and angle-bracket syntax) to prevent unsafe type casting.",
|
|
914
|
+
recommended: false
|
|
915
|
+
},
|
|
916
|
+
schema: []
|
|
917
|
+
},
|
|
918
|
+
createOnce(context) {
|
|
919
|
+
return {
|
|
920
|
+
TSAsExpression(rawNode) {
|
|
921
|
+
if (rawNode.type !== "TSAsExpression") return;
|
|
922
|
+
if (rawNode.typeAnnotation.type === "TSTypeReference" && rawNode.typeAnnotation.typeName.type === "Identifier" && rawNode.typeAnnotation.typeName.name === "const") return;
|
|
923
|
+
context.report({
|
|
924
|
+
node: rawNode,
|
|
925
|
+
message: "Type casting with `as` is not permitted. Use runtime validation with valibot or refactor to avoid type casting."
|
|
926
|
+
});
|
|
927
|
+
},
|
|
928
|
+
TSTypeAssertion(rawNode) {
|
|
929
|
+
if (rawNode.type !== "TSTypeAssertion") return;
|
|
930
|
+
context.report({
|
|
931
|
+
node: rawNode,
|
|
932
|
+
message: "Type casting with angle brackets `<Type>` is not permitted. Use runtime validation with valibot or refactor to avoid type casting."
|
|
933
|
+
});
|
|
934
|
+
},
|
|
935
|
+
TSNonNullExpression(rawNode) {
|
|
936
|
+
if (rawNode.type !== "TSNonNullExpression") return;
|
|
937
|
+
context.report({
|
|
938
|
+
node: rawNode,
|
|
939
|
+
message: "Non-null assertion operator `!` is not permitted. Handle null/undefined cases explicitly or use optional chaining."
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
const noTypeCastRule = rule$1;
|
|
946
|
+
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region src/oxlint-plugins/pretty-props.js
|
|
949
|
+
/**
|
|
950
|
+
* @typedef {import("oxlint").Context} RuleContext
|
|
951
|
+
* @typedef {import("oxlint").ESTree.Node} ESTNode
|
|
952
|
+
* @typedef {import("oxlint").ESTree.Expression} ESTExpression
|
|
953
|
+
* @typedef {import("oxlint").ESTree.Function | import("oxlint").ESTree.ArrowFunctionExpression} FunctionLikeNode
|
|
954
|
+
*/
|
|
955
|
+
const JSX_NODE_TYPES = new Set(["JSXElement", "JSXFragment"]);
|
|
956
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
957
|
+
"FunctionDeclaration",
|
|
958
|
+
"FunctionExpression",
|
|
959
|
+
"ArrowFunctionExpression"
|
|
960
|
+
]);
|
|
961
|
+
/**
|
|
962
|
+
* @param {unknown} node
|
|
963
|
+
* @returns {node is ESTNode & { type: string }}
|
|
964
|
+
*/
|
|
965
|
+
const isNode = (node) => Boolean(node && typeof node === "object" && "type" in node);
|
|
966
|
+
/**
|
|
967
|
+
* @param {unknown} node
|
|
968
|
+
* @returns {node is FunctionLikeNode}
|
|
969
|
+
*/
|
|
970
|
+
const isFunctionLike = (node) => isNode(node) && FUNCTION_NODE_TYPES.has(node.type);
|
|
971
|
+
/**
|
|
972
|
+
* Check if an expression contains JSX
|
|
973
|
+
* @param {ESTExpression | null | undefined} root
|
|
974
|
+
*/
|
|
975
|
+
const expressionContainsJsx = (root) => {
|
|
976
|
+
if (!root || !isNode(root)) return false;
|
|
977
|
+
const stack = [root];
|
|
978
|
+
while (stack.length > 0) {
|
|
979
|
+
const current = stack.pop();
|
|
980
|
+
if (!current || !isNode(current)) continue;
|
|
981
|
+
if (JSX_NODE_TYPES.has(current.type)) return true;
|
|
982
|
+
if (FUNCTION_NODE_TYPES.has(current.type) && current !== root) continue;
|
|
983
|
+
for (const key of Object.keys(current)) {
|
|
984
|
+
if (key === "parent") continue;
|
|
985
|
+
const value = current[key];
|
|
986
|
+
if (!value) continue;
|
|
987
|
+
if (Array.isArray(value)) {
|
|
988
|
+
for (const element of value) if (isNode(element)) stack.push(element);
|
|
989
|
+
} else if (isNode(value)) stack.push(value);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
};
|
|
994
|
+
/**
|
|
995
|
+
* Check if a function returns JSX
|
|
996
|
+
* @param {FunctionLikeNode} node
|
|
997
|
+
*/
|
|
998
|
+
const functionReturnsJsx = (node) => {
|
|
999
|
+
if (node.type === "ArrowFunctionExpression" && node.body && node.body.type !== "BlockStatement") return expressionContainsJsx(node.body);
|
|
1000
|
+
const body = node.body;
|
|
1001
|
+
if (!body || body.type !== "BlockStatement") return false;
|
|
1002
|
+
const stack = [body];
|
|
1003
|
+
while (stack.length > 0) {
|
|
1004
|
+
const current = stack.pop();
|
|
1005
|
+
if (!current || !isNode(current)) continue;
|
|
1006
|
+
if (current.type === "ReturnStatement") {
|
|
1007
|
+
const argument = current.argument;
|
|
1008
|
+
if (argument && expressionContainsJsx(argument)) return true;
|
|
1009
|
+
}
|
|
1010
|
+
if (FUNCTION_NODE_TYPES.has(current.type) && current !== body) continue;
|
|
1011
|
+
for (const key of Object.keys(current)) {
|
|
1012
|
+
if (key === "parent") continue;
|
|
1013
|
+
const value = current[key];
|
|
1014
|
+
if (!value) continue;
|
|
1015
|
+
if (Array.isArray(value)) {
|
|
1016
|
+
for (const element of value) if (isNode(element)) stack.push(element);
|
|
1017
|
+
} else if (isNode(value)) stack.push(value);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return false;
|
|
1021
|
+
};
|
|
1022
|
+
const rule = defineRule({
|
|
1023
|
+
meta: {
|
|
1024
|
+
type: "problem",
|
|
1025
|
+
docs: {
|
|
1026
|
+
description: "Enforce consistent props parameter naming and disallow destructuring in component parameters.",
|
|
1027
|
+
recommended: false
|
|
1028
|
+
},
|
|
1029
|
+
schema: []
|
|
1030
|
+
},
|
|
1031
|
+
createOnce(context) {
|
|
1032
|
+
/**
|
|
1033
|
+
* Check if a node is at the top level of the file (module scope)
|
|
1034
|
+
* @param {FunctionLikeNode} node
|
|
1035
|
+
* @returns {boolean}
|
|
1036
|
+
*/
|
|
1037
|
+
const isTopLevel = (node) => {
|
|
1038
|
+
let current = node.parent;
|
|
1039
|
+
while (current && isNode(current)) {
|
|
1040
|
+
if (current.type === "Program") return true;
|
|
1041
|
+
if (node.type === "FunctionDeclaration" && current.type === "Program") return true;
|
|
1042
|
+
if (current.type === "VariableDeclaration") {
|
|
1043
|
+
const parent = current.parent;
|
|
1044
|
+
if (parent && isNode(parent) && parent.type === "Program") return true;
|
|
1045
|
+
}
|
|
1046
|
+
if (FUNCTION_NODE_TYPES.has(current.type)) return false;
|
|
1047
|
+
current = current.parent;
|
|
1048
|
+
}
|
|
1049
|
+
return false;
|
|
1050
|
+
};
|
|
1051
|
+
/**
|
|
1052
|
+
* Check if a function is a top-level React component (PascalCase naming at top level)
|
|
1053
|
+
* @param {FunctionLikeNode} node
|
|
1054
|
+
* @returns {boolean}
|
|
1055
|
+
*/
|
|
1056
|
+
const isTopLevelReactComponent = (node) => {
|
|
1057
|
+
if (!isTopLevel(node)) return false;
|
|
1058
|
+
if (node.id?.name) {
|
|
1059
|
+
const firstChar = node.id.name.charAt(0);
|
|
1060
|
+
return firstChar === firstChar.toUpperCase();
|
|
1061
|
+
}
|
|
1062
|
+
const parent = node.parent;
|
|
1063
|
+
if (!parent || !isNode(parent)) return false;
|
|
1064
|
+
if (parent.type === "VariableDeclarator" && parent.id && "name" in parent.id) {
|
|
1065
|
+
const name = parent.id.name;
|
|
1066
|
+
if (typeof name === "string") {
|
|
1067
|
+
const firstChar = name.charAt(0);
|
|
1068
|
+
return firstChar === firstChar.toUpperCase();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return false;
|
|
1072
|
+
};
|
|
1073
|
+
/**
|
|
1074
|
+
* @param {FunctionLikeNode} node
|
|
1075
|
+
*/
|
|
1076
|
+
const checkFunction = (node) => {
|
|
1077
|
+
if (!functionReturnsJsx(node)) return;
|
|
1078
|
+
if (!isTopLevelReactComponent(node)) return;
|
|
1079
|
+
const params = node.params;
|
|
1080
|
+
if (!params || params.length === 0) return;
|
|
1081
|
+
const firstParam = params[0];
|
|
1082
|
+
if (!firstParam || !isNode(firstParam)) return;
|
|
1083
|
+
if (firstParam.type === "ObjectPattern" || firstParam.type === "ArrayPattern") {
|
|
1084
|
+
context.report({
|
|
1085
|
+
node: firstParam,
|
|
1086
|
+
message: "Props should not be destructured in the component parameter. Use 'props' instead and destructure inside the component body."
|
|
1087
|
+
});
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (firstParam.type === "Identifier") {
|
|
1091
|
+
if (firstParam.name !== "props") context.report({
|
|
1092
|
+
node: firstParam,
|
|
1093
|
+
message: `Props parameter should be named 'props', not '${firstParam.name}'.`
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
return {
|
|
1098
|
+
FunctionDeclaration(node) {
|
|
1099
|
+
if (isFunctionLike(node)) checkFunction(node);
|
|
1100
|
+
},
|
|
1101
|
+
FunctionExpression(node) {
|
|
1102
|
+
if (isFunctionLike(node)) checkFunction(node);
|
|
1103
|
+
},
|
|
1104
|
+
ArrowFunctionExpression(node) {
|
|
1105
|
+
if (isFunctionLike(node)) checkFunction(node);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
const prettyPropsRule = rule;
|
|
1111
|
+
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region src/oxlint-plugins/index.js
|
|
1114
|
+
const plugin = definePlugin({
|
|
1115
|
+
meta: { name: "conorroberts" },
|
|
1116
|
+
rules: {
|
|
1117
|
+
"jsx-component-pascal-case": jsxComponentPascalCaseRule,
|
|
1118
|
+
"no-component-date-instantiation": noComponentDateInstantiationRule,
|
|
1119
|
+
"no-emoji": noEmojiRule,
|
|
1120
|
+
"no-finally": noFinallyRule,
|
|
1121
|
+
"no-inline-components": noInlineComponentsRule,
|
|
1122
|
+
"no-react-namespace": noReactNamespaceRule,
|
|
1123
|
+
"no-switch": noSwitchRule,
|
|
1124
|
+
"no-top-level-let": noTopLevelLetRule,
|
|
1125
|
+
"no-type-cast": noTypeCastRule,
|
|
1126
|
+
"pretty-props": prettyPropsRule
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
var oxlint_plugins_default = plugin;
|
|
1130
|
+
|
|
1131
|
+
//#endregion
|
|
1132
|
+
export { oxlint_plugins_default as default };
|
|
1133
|
+
//# sourceMappingURL=index.mjs.map
|