@wsxjs/eslint-plugin-wsx 0.0.16 → 0.0.18
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/LICENSE +2 -2
- package/README.md +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +197 -1
- package/dist/index.mjs +197 -1
- package/package.json +3 -3
- package/src/configs/flat.ts +1 -1
- package/src/configs/recommended.ts +3 -0
- package/src/index.ts +6 -0
- package/src/rules/no-inner-html.ts +64 -0
- package/src/rules/no-null-render.ts +171 -0
- package/src/rules/require-jsx-import-source.ts +64 -0
- package/src/types.ts +1 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 WSXJS Contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @wsxjs/eslint-plugin-wsx
|
|
2
2
|
|
|
3
|
-
ESLint plugin for
|
|
3
|
+
ESLint plugin for WSXJS - enforces best practices and framework-specific rules for Web Components with JSX.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -260,6 +260,196 @@ var stateRequiresInitialValue = {
|
|
|
260
260
|
}
|
|
261
261
|
};
|
|
262
262
|
|
|
263
|
+
// src/rules/require-jsx-import-source.ts
|
|
264
|
+
var JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
|
|
265
|
+
var requireJsxImportSource = {
|
|
266
|
+
meta: {
|
|
267
|
+
type: "problem",
|
|
268
|
+
docs: {
|
|
269
|
+
description: "require @jsxImportSource pragma in .wsx files",
|
|
270
|
+
category: "Best Practices",
|
|
271
|
+
recommended: true
|
|
272
|
+
},
|
|
273
|
+
fixable: "code",
|
|
274
|
+
messages: {
|
|
275
|
+
missingPragma: "WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support"
|
|
276
|
+
},
|
|
277
|
+
schema: []
|
|
278
|
+
// 无配置选项
|
|
279
|
+
},
|
|
280
|
+
defaultOptions: [],
|
|
281
|
+
// 无默认配置选项
|
|
282
|
+
create(context) {
|
|
283
|
+
const filename = context.getFilename();
|
|
284
|
+
if (!filename.endsWith(".wsx")) {
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
const sourceCode = context.getSourceCode();
|
|
288
|
+
const text = sourceCode.getText();
|
|
289
|
+
const hasPragma = text.includes("@jsxImportSource");
|
|
290
|
+
if (!hasPragma) {
|
|
291
|
+
const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
|
|
292
|
+
context.report({
|
|
293
|
+
node: firstNode,
|
|
294
|
+
messageId: "missingPragma",
|
|
295
|
+
fix(fixer) {
|
|
296
|
+
return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/rules/no-null-render.ts
|
|
306
|
+
var noNullRender = {
|
|
307
|
+
meta: {
|
|
308
|
+
type: "problem",
|
|
309
|
+
docs: {
|
|
310
|
+
description: "disallow returning null from render() method",
|
|
311
|
+
category: "Possible Errors",
|
|
312
|
+
recommended: true
|
|
313
|
+
},
|
|
314
|
+
fixable: "code",
|
|
315
|
+
messages: {
|
|
316
|
+
nullReturn: "render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead."
|
|
317
|
+
},
|
|
318
|
+
schema: []
|
|
319
|
+
// 无配置选项
|
|
320
|
+
},
|
|
321
|
+
defaultOptions: [],
|
|
322
|
+
create(context) {
|
|
323
|
+
function checkStatements(statements) {
|
|
324
|
+
statements.forEach((statement) => {
|
|
325
|
+
if (statement.type === "ReturnStatement" && statement.argument) {
|
|
326
|
+
const arg = statement.argument;
|
|
327
|
+
if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
|
|
328
|
+
context.report({
|
|
329
|
+
node: statement,
|
|
330
|
+
messageId: "nullReturn",
|
|
331
|
+
fix(fixer) {
|
|
332
|
+
const sourceCode = context.getSourceCode();
|
|
333
|
+
const returnToken = sourceCode.getFirstToken(statement);
|
|
334
|
+
const nullToken = sourceCode.getTokenAfter(returnToken);
|
|
335
|
+
const semicolonToken = sourceCode.getTokenAfter(nullToken);
|
|
336
|
+
if (!returnToken || !nullToken) return null;
|
|
337
|
+
const start = returnToken.range[0];
|
|
338
|
+
const end = semicolonToken ? semicolonToken.range[1] : nullToken.range[1];
|
|
339
|
+
return fixer.replaceTextRange([start, end], "return <div></div>;");
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (statement.type === "IfStatement") {
|
|
345
|
+
if (statement.consequent.type === "BlockStatement") {
|
|
346
|
+
checkStatements(statement.consequent.body);
|
|
347
|
+
}
|
|
348
|
+
if (statement.alternate) {
|
|
349
|
+
if (statement.alternate.type === "BlockStatement") {
|
|
350
|
+
checkStatements(statement.alternate.body);
|
|
351
|
+
} else if (statement.alternate.type === "IfStatement") {
|
|
352
|
+
checkStatements([statement.alternate]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (statement.type === "SwitchStatement") {
|
|
356
|
+
statement.cases.forEach((caseClause) => {
|
|
357
|
+
checkStatements(caseClause.consequent);
|
|
358
|
+
});
|
|
359
|
+
} else if (statement.type === "ForStatement" || statement.type === "WhileStatement" || statement.type === "DoWhileStatement") {
|
|
360
|
+
if (statement.body.type === "BlockStatement") {
|
|
361
|
+
checkStatements(statement.body.body);
|
|
362
|
+
}
|
|
363
|
+
} else if (statement.type === "ForInStatement" || statement.type === "ForOfStatement") {
|
|
364
|
+
if (statement.body.type === "BlockStatement") {
|
|
365
|
+
checkStatements(statement.body.body);
|
|
366
|
+
}
|
|
367
|
+
} else if (statement.type === "TryStatement") {
|
|
368
|
+
if (statement.block.type === "BlockStatement") {
|
|
369
|
+
checkStatements(statement.block.body);
|
|
370
|
+
}
|
|
371
|
+
if (statement.handler && statement.handler.body.type === "BlockStatement") {
|
|
372
|
+
checkStatements(statement.handler.body.body);
|
|
373
|
+
}
|
|
374
|
+
if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
|
|
375
|
+
checkStatements(statement.finalizer.body);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function checkReturnNull(node) {
|
|
381
|
+
const isRenderMethod = node.key.type === "Identifier" && node.key.name === "render" || node.key.type === "Literal" && node.key.value === "render";
|
|
382
|
+
if (!isRenderMethod) return;
|
|
383
|
+
const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
|
|
384
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") return;
|
|
385
|
+
const body = methodValue.body;
|
|
386
|
+
if (!body || body.type !== "BlockStatement") return;
|
|
387
|
+
checkStatements(body.body);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
// 检查类方法定义
|
|
391
|
+
MethodDefinition(node) {
|
|
392
|
+
const ancestors = context.getAncestors();
|
|
393
|
+
let classDeclaration = null;
|
|
394
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
395
|
+
const ancestor = ancestors[i];
|
|
396
|
+
if (ancestor && ancestor.type === "ClassDeclaration") {
|
|
397
|
+
classDeclaration = ancestor;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (classDeclaration && classDeclaration.superClass) {
|
|
402
|
+
const isWSXComponent = classDeclaration.superClass.type === "Identifier" && (classDeclaration.superClass.name === "WebComponent" || classDeclaration.superClass.name === "LightComponent");
|
|
403
|
+
if (isWSXComponent) {
|
|
404
|
+
checkReturnNull(node);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/rules/no-inner-html.ts
|
|
413
|
+
var noInnerHTML = {
|
|
414
|
+
meta: {
|
|
415
|
+
type: "problem",
|
|
416
|
+
docs: {
|
|
417
|
+
description: "disallow use of innerHTML in WSX code",
|
|
418
|
+
category: "Best Practices",
|
|
419
|
+
recommended: true
|
|
420
|
+
},
|
|
421
|
+
messages: {
|
|
422
|
+
noInnerHTML: "Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function."
|
|
423
|
+
},
|
|
424
|
+
schema: []
|
|
425
|
+
// 无配置选项
|
|
426
|
+
},
|
|
427
|
+
create(context) {
|
|
428
|
+
return {
|
|
429
|
+
// 检测 innerHTML 赋值:element.innerHTML = "..."
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
431
|
+
AssignmentExpression(node) {
|
|
432
|
+
if (node.left && node.left.type === "MemberExpression" && node.left.property && node.left.property.type === "Identifier" && node.left.property.name === "innerHTML") {
|
|
433
|
+
context.report({
|
|
434
|
+
node,
|
|
435
|
+
messageId: "noInnerHTML"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
// 检测 innerHTML 读取:const html = element.innerHTML
|
|
440
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
441
|
+
MemberExpression(node) {
|
|
442
|
+
if (node.property && node.property.type === "Identifier" && node.property.name === "innerHTML" && node.parent && node.parent.type !== "AssignmentExpression") {
|
|
443
|
+
context.report({
|
|
444
|
+
node,
|
|
445
|
+
messageId: "noInnerHTML"
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
263
453
|
// src/configs/recommended.ts
|
|
264
454
|
var recommendedConfig = {
|
|
265
455
|
parser: "@typescript-eslint/parser",
|
|
@@ -281,6 +471,9 @@ var recommendedConfig = {
|
|
|
281
471
|
"wsx/no-react-imports": "error",
|
|
282
472
|
"wsx/web-component-naming": "warn",
|
|
283
473
|
"wsx/state-requires-initial-value": "error",
|
|
474
|
+
"wsx/require-jsx-import-source": "error",
|
|
475
|
+
"wsx/no-null-render": "error",
|
|
476
|
+
"wsx/no-inner-html": "error",
|
|
284
477
|
// TypeScript 规则(推荐)
|
|
285
478
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
286
479
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -446,7 +639,10 @@ var plugin = {
|
|
|
446
639
|
"render-method-required": renderMethodRequired,
|
|
447
640
|
"no-react-imports": noReactImports,
|
|
448
641
|
"web-component-naming": webComponentNaming,
|
|
449
|
-
"state-requires-initial-value": stateRequiresInitialValue
|
|
642
|
+
"state-requires-initial-value": stateRequiresInitialValue,
|
|
643
|
+
"require-jsx-import-source": requireJsxImportSource,
|
|
644
|
+
"no-null-render": noNullRender,
|
|
645
|
+
"no-inner-html": noInnerHTML
|
|
450
646
|
},
|
|
451
647
|
// 配置预设
|
|
452
648
|
configs: {
|
package/dist/index.mjs
CHANGED
|
@@ -231,6 +231,196 @@ var stateRequiresInitialValue = {
|
|
|
231
231
|
}
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
// src/rules/require-jsx-import-source.ts
|
|
235
|
+
var JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
|
|
236
|
+
var requireJsxImportSource = {
|
|
237
|
+
meta: {
|
|
238
|
+
type: "problem",
|
|
239
|
+
docs: {
|
|
240
|
+
description: "require @jsxImportSource pragma in .wsx files",
|
|
241
|
+
category: "Best Practices",
|
|
242
|
+
recommended: true
|
|
243
|
+
},
|
|
244
|
+
fixable: "code",
|
|
245
|
+
messages: {
|
|
246
|
+
missingPragma: "WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support"
|
|
247
|
+
},
|
|
248
|
+
schema: []
|
|
249
|
+
// 无配置选项
|
|
250
|
+
},
|
|
251
|
+
defaultOptions: [],
|
|
252
|
+
// 无默认配置选项
|
|
253
|
+
create(context) {
|
|
254
|
+
const filename = context.getFilename();
|
|
255
|
+
if (!filename.endsWith(".wsx")) {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
const sourceCode = context.getSourceCode();
|
|
259
|
+
const text = sourceCode.getText();
|
|
260
|
+
const hasPragma = text.includes("@jsxImportSource");
|
|
261
|
+
if (!hasPragma) {
|
|
262
|
+
const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
|
|
263
|
+
context.report({
|
|
264
|
+
node: firstNode,
|
|
265
|
+
messageId: "missingPragma",
|
|
266
|
+
fix(fixer) {
|
|
267
|
+
return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}
|
|
268
|
+
`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/rules/no-null-render.ts
|
|
277
|
+
var noNullRender = {
|
|
278
|
+
meta: {
|
|
279
|
+
type: "problem",
|
|
280
|
+
docs: {
|
|
281
|
+
description: "disallow returning null from render() method",
|
|
282
|
+
category: "Possible Errors",
|
|
283
|
+
recommended: true
|
|
284
|
+
},
|
|
285
|
+
fixable: "code",
|
|
286
|
+
messages: {
|
|
287
|
+
nullReturn: "render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead."
|
|
288
|
+
},
|
|
289
|
+
schema: []
|
|
290
|
+
// 无配置选项
|
|
291
|
+
},
|
|
292
|
+
defaultOptions: [],
|
|
293
|
+
create(context) {
|
|
294
|
+
function checkStatements(statements) {
|
|
295
|
+
statements.forEach((statement) => {
|
|
296
|
+
if (statement.type === "ReturnStatement" && statement.argument) {
|
|
297
|
+
const arg = statement.argument;
|
|
298
|
+
if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
|
|
299
|
+
context.report({
|
|
300
|
+
node: statement,
|
|
301
|
+
messageId: "nullReturn",
|
|
302
|
+
fix(fixer) {
|
|
303
|
+
const sourceCode = context.getSourceCode();
|
|
304
|
+
const returnToken = sourceCode.getFirstToken(statement);
|
|
305
|
+
const nullToken = sourceCode.getTokenAfter(returnToken);
|
|
306
|
+
const semicolonToken = sourceCode.getTokenAfter(nullToken);
|
|
307
|
+
if (!returnToken || !nullToken) return null;
|
|
308
|
+
const start = returnToken.range[0];
|
|
309
|
+
const end = semicolonToken ? semicolonToken.range[1] : nullToken.range[1];
|
|
310
|
+
return fixer.replaceTextRange([start, end], "return <div></div>;");
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (statement.type === "IfStatement") {
|
|
316
|
+
if (statement.consequent.type === "BlockStatement") {
|
|
317
|
+
checkStatements(statement.consequent.body);
|
|
318
|
+
}
|
|
319
|
+
if (statement.alternate) {
|
|
320
|
+
if (statement.alternate.type === "BlockStatement") {
|
|
321
|
+
checkStatements(statement.alternate.body);
|
|
322
|
+
} else if (statement.alternate.type === "IfStatement") {
|
|
323
|
+
checkStatements([statement.alternate]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else if (statement.type === "SwitchStatement") {
|
|
327
|
+
statement.cases.forEach((caseClause) => {
|
|
328
|
+
checkStatements(caseClause.consequent);
|
|
329
|
+
});
|
|
330
|
+
} else if (statement.type === "ForStatement" || statement.type === "WhileStatement" || statement.type === "DoWhileStatement") {
|
|
331
|
+
if (statement.body.type === "BlockStatement") {
|
|
332
|
+
checkStatements(statement.body.body);
|
|
333
|
+
}
|
|
334
|
+
} else if (statement.type === "ForInStatement" || statement.type === "ForOfStatement") {
|
|
335
|
+
if (statement.body.type === "BlockStatement") {
|
|
336
|
+
checkStatements(statement.body.body);
|
|
337
|
+
}
|
|
338
|
+
} else if (statement.type === "TryStatement") {
|
|
339
|
+
if (statement.block.type === "BlockStatement") {
|
|
340
|
+
checkStatements(statement.block.body);
|
|
341
|
+
}
|
|
342
|
+
if (statement.handler && statement.handler.body.type === "BlockStatement") {
|
|
343
|
+
checkStatements(statement.handler.body.body);
|
|
344
|
+
}
|
|
345
|
+
if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
|
|
346
|
+
checkStatements(statement.finalizer.body);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function checkReturnNull(node) {
|
|
352
|
+
const isRenderMethod = node.key.type === "Identifier" && node.key.name === "render" || node.key.type === "Literal" && node.key.value === "render";
|
|
353
|
+
if (!isRenderMethod) return;
|
|
354
|
+
const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
|
|
355
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") return;
|
|
356
|
+
const body = methodValue.body;
|
|
357
|
+
if (!body || body.type !== "BlockStatement") return;
|
|
358
|
+
checkStatements(body.body);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
// 检查类方法定义
|
|
362
|
+
MethodDefinition(node) {
|
|
363
|
+
const ancestors = context.getAncestors();
|
|
364
|
+
let classDeclaration = null;
|
|
365
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
366
|
+
const ancestor = ancestors[i];
|
|
367
|
+
if (ancestor && ancestor.type === "ClassDeclaration") {
|
|
368
|
+
classDeclaration = ancestor;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (classDeclaration && classDeclaration.superClass) {
|
|
373
|
+
const isWSXComponent = classDeclaration.superClass.type === "Identifier" && (classDeclaration.superClass.name === "WebComponent" || classDeclaration.superClass.name === "LightComponent");
|
|
374
|
+
if (isWSXComponent) {
|
|
375
|
+
checkReturnNull(node);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/rules/no-inner-html.ts
|
|
384
|
+
var noInnerHTML = {
|
|
385
|
+
meta: {
|
|
386
|
+
type: "problem",
|
|
387
|
+
docs: {
|
|
388
|
+
description: "disallow use of innerHTML in WSX code",
|
|
389
|
+
category: "Best Practices",
|
|
390
|
+
recommended: true
|
|
391
|
+
},
|
|
392
|
+
messages: {
|
|
393
|
+
noInnerHTML: "Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function."
|
|
394
|
+
},
|
|
395
|
+
schema: []
|
|
396
|
+
// 无配置选项
|
|
397
|
+
},
|
|
398
|
+
create(context) {
|
|
399
|
+
return {
|
|
400
|
+
// 检测 innerHTML 赋值:element.innerHTML = "..."
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
402
|
+
AssignmentExpression(node) {
|
|
403
|
+
if (node.left && node.left.type === "MemberExpression" && node.left.property && node.left.property.type === "Identifier" && node.left.property.name === "innerHTML") {
|
|
404
|
+
context.report({
|
|
405
|
+
node,
|
|
406
|
+
messageId: "noInnerHTML"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
// 检测 innerHTML 读取:const html = element.innerHTML
|
|
411
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
412
|
+
MemberExpression(node) {
|
|
413
|
+
if (node.property && node.property.type === "Identifier" && node.property.name === "innerHTML" && node.parent && node.parent.type !== "AssignmentExpression") {
|
|
414
|
+
context.report({
|
|
415
|
+
node,
|
|
416
|
+
messageId: "noInnerHTML"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
234
424
|
// src/configs/recommended.ts
|
|
235
425
|
var recommendedConfig = {
|
|
236
426
|
parser: "@typescript-eslint/parser",
|
|
@@ -252,6 +442,9 @@ var recommendedConfig = {
|
|
|
252
442
|
"wsx/no-react-imports": "error",
|
|
253
443
|
"wsx/web-component-naming": "warn",
|
|
254
444
|
"wsx/state-requires-initial-value": "error",
|
|
445
|
+
"wsx/require-jsx-import-source": "error",
|
|
446
|
+
"wsx/no-null-render": "error",
|
|
447
|
+
"wsx/no-inner-html": "error",
|
|
255
448
|
// TypeScript 规则(推荐)
|
|
256
449
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
257
450
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -417,7 +610,10 @@ var plugin = {
|
|
|
417
610
|
"render-method-required": renderMethodRequired,
|
|
418
611
|
"no-react-imports": noReactImports,
|
|
419
612
|
"web-component-naming": webComponentNaming,
|
|
420
|
-
"state-requires-initial-value": stateRequiresInitialValue
|
|
613
|
+
"state-requires-initial-value": stateRequiresInitialValue,
|
|
614
|
+
"require-jsx-import-source": requireJsxImportSource,
|
|
615
|
+
"no-null-render": noNullRender,
|
|
616
|
+
"no-inner-html": noInnerHTML
|
|
421
617
|
},
|
|
422
618
|
// 配置预设
|
|
423
619
|
configs: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/eslint-plugin-wsx",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "ESLint plugin for
|
|
3
|
+
"version": "0.0.18",
|
|
4
|
+
"description": "ESLint plugin for WSXJS",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"web-components"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@wsxjs/wsx-core": "0.0.
|
|
28
|
+
"@wsxjs/wsx-core": "0.0.18"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"tsup": "^8.0.0",
|
package/src/configs/flat.ts
CHANGED
|
@@ -25,6 +25,9 @@ export const recommendedConfig: WSXConfig = {
|
|
|
25
25
|
"wsx/no-react-imports": "error",
|
|
26
26
|
"wsx/web-component-naming": "warn",
|
|
27
27
|
"wsx/state-requires-initial-value": "error",
|
|
28
|
+
"wsx/require-jsx-import-source": "error",
|
|
29
|
+
"wsx/no-null-render": "error",
|
|
30
|
+
"wsx/no-inner-html": "error",
|
|
28
31
|
|
|
29
32
|
// TypeScript 规则(推荐)
|
|
30
33
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { renderMethodRequired } from "./rules/render-method-required";
|
|
|
9
9
|
import { noReactImports } from "./rules/no-react-imports";
|
|
10
10
|
import { webComponentNaming } from "./rules/web-component-naming";
|
|
11
11
|
import { stateRequiresInitialValue } from "./rules/state-requires-initial-value";
|
|
12
|
+
import { requireJsxImportSource } from "./rules/require-jsx-import-source";
|
|
13
|
+
import { noNullRender } from "./rules/no-null-render";
|
|
14
|
+
import { noInnerHTML } from "./rules/no-inner-html";
|
|
12
15
|
import { recommendedConfig } from "./configs/recommended";
|
|
13
16
|
import { createFlatConfig } from "./configs/flat";
|
|
14
17
|
import { WSXPlugin } from "./types";
|
|
@@ -26,6 +29,9 @@ const plugin: WSXPlugin = {
|
|
|
26
29
|
"no-react-imports": noReactImports,
|
|
27
30
|
"web-component-naming": webComponentNaming,
|
|
28
31
|
"state-requires-initial-value": stateRequiresInitialValue,
|
|
32
|
+
"require-jsx-import-source": requireJsxImportSource,
|
|
33
|
+
"no-null-render": noNullRender,
|
|
34
|
+
"no-inner-html": noInnerHTML,
|
|
29
35
|
},
|
|
30
36
|
|
|
31
37
|
// 配置预设
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:no-inner-html
|
|
3
|
+
*
|
|
4
|
+
* 禁止在 WSX 代码中使用 innerHTML
|
|
5
|
+
* WSX 框架鼓励使用声明式 JSX 而不是手动 DOM 操作
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Rule } from "eslint";
|
|
9
|
+
import { WSXRuleModule } from "../types";
|
|
10
|
+
|
|
11
|
+
export const noInnerHTML: WSXRuleModule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "disallow use of innerHTML in WSX code",
|
|
16
|
+
category: "Best Practices",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
noInnerHTML:
|
|
21
|
+
"Do not use innerHTML. Use JSX and WSX's declarative rendering instead. If you need to parse HTML strings, use parseHTMLToNodes() utility function.",
|
|
22
|
+
},
|
|
23
|
+
schema: [], // 无配置选项
|
|
24
|
+
},
|
|
25
|
+
create(context: Rule.RuleContext) {
|
|
26
|
+
return {
|
|
27
|
+
// 检测 innerHTML 赋值:element.innerHTML = "..."
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
AssignmentExpression(node: any) {
|
|
30
|
+
if (
|
|
31
|
+
node.left &&
|
|
32
|
+
node.left.type === "MemberExpression" &&
|
|
33
|
+
node.left.property &&
|
|
34
|
+
node.left.property.type === "Identifier" &&
|
|
35
|
+
node.left.property.name === "innerHTML"
|
|
36
|
+
) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: "noInnerHTML",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// 检测 innerHTML 读取:const html = element.innerHTML
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
MemberExpression(node: any) {
|
|
46
|
+
if (
|
|
47
|
+
node.property &&
|
|
48
|
+
node.property.type === "Identifier" &&
|
|
49
|
+
node.property.name === "innerHTML" &&
|
|
50
|
+
node.parent &&
|
|
51
|
+
node.parent.type !== "AssignmentExpression"
|
|
52
|
+
) {
|
|
53
|
+
// 只报告读取操作(赋值已在上面处理)
|
|
54
|
+
// 但允许在特定上下文中读取(如调试或工具函数)
|
|
55
|
+
// 这里我们仍然报告,但用户可以通过注释禁用
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: "noInnerHTML",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:no-null-render
|
|
3
|
+
*
|
|
4
|
+
* 禁止 render() 方法返回 null
|
|
5
|
+
* WebComponent 和 LightComponent 的 connectedCallback 需要有效的 DOM 节点
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Rule } from "eslint";
|
|
9
|
+
import { WSXRuleModule } from "../types";
|
|
10
|
+
|
|
11
|
+
export const noNullRender: WSXRuleModule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "disallow returning null from render() method",
|
|
16
|
+
category: "Possible Errors",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
fixable: "code",
|
|
20
|
+
messages: {
|
|
21
|
+
nullReturn:
|
|
22
|
+
"render() method must not return null. WebComponent and LightComponent require a valid DOM node. Use an empty div as placeholder instead.",
|
|
23
|
+
},
|
|
24
|
+
schema: [], // 无配置选项
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context: Rule.RuleContext) {
|
|
28
|
+
/**
|
|
29
|
+
* 递归检查语句块中的所有 return null
|
|
30
|
+
*/
|
|
31
|
+
function checkStatements(statements: import("estree").Statement[]) {
|
|
32
|
+
statements.forEach((statement) => {
|
|
33
|
+
// 检查 return null
|
|
34
|
+
if (statement.type === "ReturnStatement" && statement.argument) {
|
|
35
|
+
const arg = statement.argument;
|
|
36
|
+
// 检查是否为 null 字面量
|
|
37
|
+
if (arg.type === "Literal" && (arg.value === null || arg.raw === "null")) {
|
|
38
|
+
context.report({
|
|
39
|
+
node: statement,
|
|
40
|
+
messageId: "nullReturn",
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
// 将 return null; 替换为 return <div></div>;
|
|
43
|
+
const sourceCode = context.getSourceCode();
|
|
44
|
+
const returnToken = sourceCode.getFirstToken(statement);
|
|
45
|
+
const nullToken = sourceCode.getTokenAfter(returnToken!);
|
|
46
|
+
const semicolonToken = sourceCode.getTokenAfter(nullToken!);
|
|
47
|
+
|
|
48
|
+
if (!returnToken || !nullToken) return null;
|
|
49
|
+
|
|
50
|
+
// 计算替换范围:从 return 到 null(或分号)
|
|
51
|
+
const start = returnToken.range[0];
|
|
52
|
+
const end = semicolonToken
|
|
53
|
+
? semicolonToken.range[1]
|
|
54
|
+
: nullToken.range[1];
|
|
55
|
+
|
|
56
|
+
// 替换为 return <div></div>;
|
|
57
|
+
return fixer.replaceTextRange([start, end], "return <div></div>;");
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 递归检查嵌套的语句块
|
|
64
|
+
if (statement.type === "IfStatement") {
|
|
65
|
+
// 检查 then 分支
|
|
66
|
+
if (statement.consequent.type === "BlockStatement") {
|
|
67
|
+
checkStatements(statement.consequent.body);
|
|
68
|
+
}
|
|
69
|
+
// 检查 else 分支
|
|
70
|
+
if (statement.alternate) {
|
|
71
|
+
if (statement.alternate.type === "BlockStatement") {
|
|
72
|
+
checkStatements(statement.alternate.body);
|
|
73
|
+
} else if (statement.alternate.type === "IfStatement") {
|
|
74
|
+
// else if 的情况
|
|
75
|
+
checkStatements([statement.alternate]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (statement.type === "SwitchStatement") {
|
|
79
|
+
// 检查 switch 的每个 case
|
|
80
|
+
statement.cases.forEach((caseClause) => {
|
|
81
|
+
checkStatements(caseClause.consequent);
|
|
82
|
+
});
|
|
83
|
+
} else if (
|
|
84
|
+
statement.type === "ForStatement" ||
|
|
85
|
+
statement.type === "WhileStatement" ||
|
|
86
|
+
statement.type === "DoWhileStatement"
|
|
87
|
+
) {
|
|
88
|
+
// 检查循环体
|
|
89
|
+
if (statement.body.type === "BlockStatement") {
|
|
90
|
+
checkStatements(statement.body.body);
|
|
91
|
+
}
|
|
92
|
+
} else if (
|
|
93
|
+
statement.type === "ForInStatement" ||
|
|
94
|
+
statement.type === "ForOfStatement"
|
|
95
|
+
) {
|
|
96
|
+
// 检查 for...in 和 for...of 循环体
|
|
97
|
+
if (statement.body.type === "BlockStatement") {
|
|
98
|
+
checkStatements(statement.body.body);
|
|
99
|
+
}
|
|
100
|
+
} else if (statement.type === "TryStatement") {
|
|
101
|
+
// 检查 try 块
|
|
102
|
+
if (statement.block.type === "BlockStatement") {
|
|
103
|
+
checkStatements(statement.block.body);
|
|
104
|
+
}
|
|
105
|
+
// 检查 catch 块
|
|
106
|
+
if (statement.handler && statement.handler.body.type === "BlockStatement") {
|
|
107
|
+
checkStatements(statement.handler.body.body);
|
|
108
|
+
}
|
|
109
|
+
// 检查 finally 块
|
|
110
|
+
if (statement.finalizer && statement.finalizer.type === "BlockStatement") {
|
|
111
|
+
checkStatements(statement.finalizer.body);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 检查方法体中是否有 return null
|
|
119
|
+
*/
|
|
120
|
+
function checkReturnNull(
|
|
121
|
+
node: import("estree").MethodDefinition | import("estree").Property
|
|
122
|
+
) {
|
|
123
|
+
// 只检查 render 方法
|
|
124
|
+
const isRenderMethod =
|
|
125
|
+
(node.key.type === "Identifier" && node.key.name === "render") ||
|
|
126
|
+
(node.key.type === "Literal" && node.key.value === "render");
|
|
127
|
+
|
|
128
|
+
if (!isRenderMethod) return;
|
|
129
|
+
|
|
130
|
+
// 获取方法体
|
|
131
|
+
const methodValue = node.type === "MethodDefinition" ? node.value : node.value;
|
|
132
|
+
if (!methodValue || methodValue.type !== "FunctionExpression") return;
|
|
133
|
+
|
|
134
|
+
const body = methodValue.body;
|
|
135
|
+
if (!body || body.type !== "BlockStatement") return;
|
|
136
|
+
|
|
137
|
+
// 递归检查所有语句(包括嵌套的)
|
|
138
|
+
checkStatements(body.body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
// 检查类方法定义
|
|
143
|
+
MethodDefinition(node: import("estree").MethodDefinition) {
|
|
144
|
+
// 检查是否继承自 WebComponent 或 LightComponent
|
|
145
|
+
// 使用 getAncestors 获取父节点链(不包括当前节点)
|
|
146
|
+
const ancestors = context.getAncestors();
|
|
147
|
+
|
|
148
|
+
// 查找最近的 ClassDeclaration 父节点(从后往前)
|
|
149
|
+
let classDeclaration: import("estree").ClassDeclaration | null = null;
|
|
150
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
151
|
+
const ancestor = ancestors[i];
|
|
152
|
+
if (ancestor && ancestor.type === "ClassDeclaration") {
|
|
153
|
+
classDeclaration = ancestor as import("estree").ClassDeclaration;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (classDeclaration && classDeclaration.superClass) {
|
|
159
|
+
const isWSXComponent =
|
|
160
|
+
classDeclaration.superClass.type === "Identifier" &&
|
|
161
|
+
(classDeclaration.superClass.name === "WebComponent" ||
|
|
162
|
+
classDeclaration.superClass.name === "LightComponent");
|
|
163
|
+
|
|
164
|
+
if (isWSXComponent) {
|
|
165
|
+
checkReturnNull(node);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:require-jsx-import-source
|
|
3
|
+
*
|
|
4
|
+
* 要求 .wsx 文件包含 @jsxImportSource pragma 注释
|
|
5
|
+
* 自动修复:如果缺失,自动在文件开头添加
|
|
6
|
+
*
|
|
7
|
+
* 为什么需要这个规则:
|
|
8
|
+
* - IDE 的 TypeScript 语言服务器读取源文件,需要 pragma 来正确进行类型检查
|
|
9
|
+
* - Vite 插件在构建时注入 pragma 对 IDE 无效(IDE 看不到构建后的代码)
|
|
10
|
+
* - ESLint 自动修复直接修改源文件,IDE 可以立即看到 pragma
|
|
11
|
+
* - 这是唯一能让 IDE 和构建工具都正常工作的方案
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Rule } from "eslint";
|
|
15
|
+
import { WSXRuleModule } from "../types";
|
|
16
|
+
|
|
17
|
+
const JSX_IMPORT_SOURCE_PRAGMA = "/** @jsxImportSource @wsxjs/wsx-core */";
|
|
18
|
+
|
|
19
|
+
export const requireJsxImportSource: WSXRuleModule = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "require @jsxImportSource pragma in .wsx files",
|
|
24
|
+
category: "Best Practices",
|
|
25
|
+
recommended: true,
|
|
26
|
+
},
|
|
27
|
+
fixable: "code",
|
|
28
|
+
messages: {
|
|
29
|
+
missingPragma:
|
|
30
|
+
"WSX files must include '@jsxImportSource @wsxjs/wsx-core' pragma comment at the top for IDE TypeScript language server support",
|
|
31
|
+
},
|
|
32
|
+
schema: [], // 无配置选项
|
|
33
|
+
},
|
|
34
|
+
defaultOptions: [], // 无默认配置选项
|
|
35
|
+
create(context: Rule.RuleContext) {
|
|
36
|
+
const filename = context.getFilename();
|
|
37
|
+
// 只检查 .wsx 文件
|
|
38
|
+
if (!filename.endsWith(".wsx")) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sourceCode = context.getSourceCode();
|
|
43
|
+
const text = sourceCode.getText();
|
|
44
|
+
|
|
45
|
+
// 检查是否已包含 pragma
|
|
46
|
+
const hasPragma = text.includes("@jsxImportSource");
|
|
47
|
+
|
|
48
|
+
if (!hasPragma) {
|
|
49
|
+
// 获取第一个节点(通常是文件开头的注释或 import)
|
|
50
|
+
const firstNode = sourceCode.ast.body[0] || sourceCode.ast;
|
|
51
|
+
|
|
52
|
+
context.report({
|
|
53
|
+
node: firstNode,
|
|
54
|
+
messageId: "missingPragma",
|
|
55
|
+
fix(fixer) {
|
|
56
|
+
// 在文件开头插入 pragma 注释
|
|
57
|
+
return fixer.insertTextBeforeRange([0, 0], `${JSX_IMPORT_SOURCE_PRAGMA}\n`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {};
|
|
63
|
+
},
|
|
64
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface WSXRuleContext extends Rule.RuleContext {
|
|
|
12
12
|
export interface WSXRuleModule extends Rule.RuleModule {
|
|
13
13
|
// WSX 特定的规则模块扩展
|
|
14
14
|
create: (context: WSXRuleContext) => Rule.RuleListener;
|
|
15
|
+
defaultOptions?: unknown[]; // 默认配置选项(可选)
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface WSXConfig {
|