@appthen/cli 1.1.33 → 1.2.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/bin/main.js +230 -12
- package/dist/TSXComplianceChecker-AST.js +321 -0
- package/dist/TSXComplianceChecker.js +683 -0
- package/dist/index.js +6937 -1405
- package/package.json +3 -1
package/bin/main.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 导入包信息,用于获取版本号
|
|
8
8
|
*/
|
|
9
9
|
var pkg = require('../package.json');
|
|
10
|
+
var path = require('path');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 导入commander用于构建CLI命令行工具
|
|
@@ -14,8 +15,7 @@ var pkg = require('../package.json');
|
|
|
14
15
|
var program = require('commander');
|
|
15
16
|
|
|
16
17
|
// 设置版本信息命令
|
|
17
|
-
program
|
|
18
|
-
.version(pkg.version, '-v, --version', 'display version information');
|
|
18
|
+
program.version(pkg.version, '-v, --version', 'display version information');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* generate命令 - 默认命令
|
|
@@ -208,12 +208,20 @@ program
|
|
|
208
208
|
.description('Sync backend code from remote')
|
|
209
209
|
.requiredOption('-p, --id <id>', 'project id')
|
|
210
210
|
.requiredOption('-a, --auth <auth>', 'auth token')
|
|
211
|
-
.option(
|
|
211
|
+
.option(
|
|
212
|
+
'-f, --framework <framework>',
|
|
213
|
+
'backend language framework',
|
|
214
|
+
'fastify'
|
|
215
|
+
)
|
|
212
216
|
.option('-o, --output <output>', 'specify the output directory', 'src/types')
|
|
213
217
|
.option('-m, --mapping <mapping>', 'path mapping configuration')
|
|
214
218
|
.option('-cc, --clear <clear>', 'clear cache')
|
|
215
219
|
.option('-c, --cwd <cwd>', 'specify the working directory', '.')
|
|
216
|
-
.option(
|
|
220
|
+
.option(
|
|
221
|
+
'-q, --quiet',
|
|
222
|
+
'be quiet, do not output anything unless get error',
|
|
223
|
+
false
|
|
224
|
+
)
|
|
217
225
|
.option('-vb, --verbose', 'be verbose, output more information', false)
|
|
218
226
|
.option('--nocheck', 'add @ts-nocheck to cloud function files', false)
|
|
219
227
|
.action(function doSync(command) {
|
|
@@ -261,7 +269,7 @@ program
|
|
|
261
269
|
// 处理环境变量
|
|
262
270
|
const env = {};
|
|
263
271
|
if (options.env) {
|
|
264
|
-
options.env.forEach(item => {
|
|
272
|
+
options.env.forEach((item) => {
|
|
265
273
|
const [key, value] = item.split('=');
|
|
266
274
|
if (key && value) {
|
|
267
275
|
env[key] = value;
|
|
@@ -276,7 +284,7 @@ program
|
|
|
276
284
|
name: options.projectName,
|
|
277
285
|
target: options.prod ? 'production' : 'preview',
|
|
278
286
|
outputDir: options.output,
|
|
279
|
-
env
|
|
287
|
+
env,
|
|
280
288
|
};
|
|
281
289
|
|
|
282
290
|
require('../dist/index.js')
|
|
@@ -313,6 +321,7 @@ program
|
|
|
313
321
|
.option('-vb, --verbose', 'be verbose, output more information', false)
|
|
314
322
|
.option('--daemon', 'run as daemon process', false)
|
|
315
323
|
.option('--debug', 'debug mode, process will not exit automatically', false)
|
|
324
|
+
.option('--workspace', 'enable shadow space workspace features', false)
|
|
316
325
|
.action(async function doConnect(command) {
|
|
317
326
|
var options = command.opts();
|
|
318
327
|
if (options.cwd) {
|
|
@@ -323,7 +332,8 @@ program
|
|
|
323
332
|
const retCode = await require('../dist/index.js').startConnecting({
|
|
324
333
|
...options,
|
|
325
334
|
daemon: options.daemon,
|
|
326
|
-
debug: options.debug
|
|
335
|
+
debug: options.debug,
|
|
336
|
+
workspace: options.workspace,
|
|
327
337
|
});
|
|
328
338
|
if (!options.debug) {
|
|
329
339
|
process.exit(retCode);
|
|
@@ -372,8 +382,13 @@ program
|
|
|
372
382
|
*/
|
|
373
383
|
program
|
|
374
384
|
.command('proxy')
|
|
375
|
-
.description(
|
|
376
|
-
|
|
385
|
+
.description(
|
|
386
|
+
'start local proxy server to solve CORS and private network access'
|
|
387
|
+
)
|
|
388
|
+
.requiredOption(
|
|
389
|
+
'--target <target>',
|
|
390
|
+
'target server address, e.g. http://localhost:3000'
|
|
391
|
+
)
|
|
377
392
|
.requiredOption('--port <port>', 'local listen port, e.g. 9000')
|
|
378
393
|
.option('--daemon', 'run as daemon process', false)
|
|
379
394
|
.option('--debug', 'debug mode, process will not exit automatically', false)
|
|
@@ -381,9 +396,13 @@ program
|
|
|
381
396
|
.on('--help', function () {
|
|
382
397
|
console.log('\n用法示例:');
|
|
383
398
|
console.log(' # 启动本地代理,将9000端口转发到目标服务');
|
|
384
|
-
console.log(
|
|
399
|
+
console.log(
|
|
400
|
+
' $ yourcli proxy --target http://192.168.1.100:8080 --port 9000'
|
|
401
|
+
);
|
|
385
402
|
console.log('\n # 以守护进程方式运行');
|
|
386
|
-
console.log(
|
|
403
|
+
console.log(
|
|
404
|
+
' $ yourcli proxy --target http://192.168.1.100:8080 --port 9000 --daemon'
|
|
405
|
+
);
|
|
387
406
|
console.log('\n # 停止代理服务');
|
|
388
407
|
console.log(' $ yourcli proxy --stop');
|
|
389
408
|
console.log('\n参数说明:');
|
|
@@ -416,5 +435,204 @@ program
|
|
|
416
435
|
});
|
|
417
436
|
});
|
|
418
437
|
|
|
419
|
-
|
|
438
|
+
/**
|
|
439
|
+
* shadow-space命令
|
|
440
|
+
* 影子空间调试工具
|
|
441
|
+
*/
|
|
442
|
+
program
|
|
443
|
+
.command('shadow-space')
|
|
444
|
+
.description('shadow space debugging tool')
|
|
445
|
+
.option(
|
|
446
|
+
'-a, --operation <operation>',
|
|
447
|
+
'operation to perform: init|info|changes|sync|reset|cleanup',
|
|
448
|
+
'info'
|
|
449
|
+
)
|
|
450
|
+
.option('-r, --project-root <path>', 'project root directory', '.')
|
|
451
|
+
.option('-i, --project-id <id>', 'project ID', 'debug-project')
|
|
452
|
+
.option('-u, --user-id <id>', 'user ID', 'debug-user')
|
|
453
|
+
.option('-s, --space-id <id>', 'space ID', 'debug')
|
|
454
|
+
.option('-f, --files <files>', 'comma-separated list of files to sync')
|
|
455
|
+
.option('-v, --verbose', 'verbose output', false)
|
|
456
|
+
.on('--help', function () {
|
|
457
|
+
console.log('\n用法示例:');
|
|
458
|
+
console.log(' # 初始化影子空间');
|
|
459
|
+
console.log(
|
|
460
|
+
' $ appthen shadow-space --operation init --project-id my-project --user-id user123'
|
|
461
|
+
);
|
|
462
|
+
console.log('\n # 查看空间信息');
|
|
463
|
+
console.log(' $ appthen shadow-space --operation info');
|
|
464
|
+
console.log('\n # 检测文件变更');
|
|
465
|
+
console.log(' $ appthen shadow-space --operation changes --verbose');
|
|
466
|
+
console.log('\n # 同步指定文件');
|
|
467
|
+
console.log(
|
|
468
|
+
' $ appthen shadow-space --operation sync --files "src/index.ts,package.json"'
|
|
469
|
+
);
|
|
470
|
+
console.log('\n # 重置影子空间');
|
|
471
|
+
console.log(' $ appthen shadow-space --operation reset');
|
|
472
|
+
console.log('\n # 清理影子空间');
|
|
473
|
+
console.log(' $ appthen shadow-space --operation cleanup');
|
|
474
|
+
console.log('\n支持的操作:');
|
|
475
|
+
console.log(' init - 初始化影子空间');
|
|
476
|
+
console.log(' info - 显示空间信息');
|
|
477
|
+
console.log(' changes - 检测文件变更');
|
|
478
|
+
console.log(' sync - 同步文件到影子空间');
|
|
479
|
+
console.log(' reset - 重置影子空间');
|
|
480
|
+
console.log(' cleanup - 清理影子空间');
|
|
481
|
+
})
|
|
482
|
+
.action(async function doShadowSpace(command) {
|
|
483
|
+
const options = command.opts();
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const { executeShadowSpaceDebug } = require('../dist/index.js');
|
|
487
|
+
|
|
488
|
+
await executeShadowSpaceDebug({
|
|
489
|
+
projectRoot: path.resolve(options.projectRoot),
|
|
490
|
+
projectId: options.projectId,
|
|
491
|
+
userId: options.userId,
|
|
492
|
+
spaceId: options.spaceId,
|
|
493
|
+
action: options.operation,
|
|
494
|
+
files: options.files
|
|
495
|
+
? options.files.split(',').map((f) => f.trim())
|
|
496
|
+
: undefined,
|
|
497
|
+
verbose: options.verbose,
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error('Shadow space command failed:', error);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* check-tsx命令
|
|
507
|
+
* 检查 TSX 文件是否符合平台规范
|
|
508
|
+
* 可选参数:
|
|
509
|
+
* -f, --files: 要检查的文件模式,默认为所有 tsx 文件
|
|
510
|
+
* -c, --cwd: 指定工作目录
|
|
511
|
+
* -r, --recursive: 递归检查子目录
|
|
512
|
+
* -e, --errors-only: 只显示错误
|
|
513
|
+
* -v, --verbose: 详细输出
|
|
514
|
+
* --format: 输出格式 (text|json)
|
|
515
|
+
* --exit-on-error: 发现错误时退出
|
|
516
|
+
*/
|
|
517
|
+
program
|
|
518
|
+
.command('check-tsx')
|
|
519
|
+
.description('Check TSX files compliance with platform standards')
|
|
520
|
+
.option('-f, --files <files...>', 'file patterns to check')
|
|
521
|
+
.option('-c, --cwd <cwd>', 'specify the working directory', '.')
|
|
522
|
+
.option('-r, --recursive', 'recursively check subdirectories', true)
|
|
523
|
+
.option('-e, --errors-only', 'only show errors', false)
|
|
524
|
+
.option('-v, --verbose', 'verbose output', false)
|
|
525
|
+
.option('--format <format>', 'output format (text|json)', 'text')
|
|
526
|
+
.option('--exit-on-error', 'exit with error code if issues found', false)
|
|
527
|
+
.on('--help', function () {
|
|
528
|
+
console.log('\n用法示例:');
|
|
529
|
+
console.log(' # 检查当前目录下所有 TSX 文件');
|
|
530
|
+
console.log(' $ appthen check-tsx');
|
|
531
|
+
console.log('\n # 检查指定文件');
|
|
532
|
+
console.log(' $ appthen check-tsx -f "src/**/*.tsx"');
|
|
533
|
+
console.log('\n # 只显示错误,不显示警告');
|
|
534
|
+
console.log(' $ appthen check-tsx --errors-only');
|
|
535
|
+
console.log('\n # 输出 JSON 格式结果');
|
|
536
|
+
console.log(' $ appthen check-tsx --format json');
|
|
537
|
+
console.log('\n # 发现错误时退出(用于 CI/CD)');
|
|
538
|
+
console.log(' $ appthen check-tsx --exit-on-error');
|
|
539
|
+
console.log('\n检查规则:');
|
|
540
|
+
console.log(' - 文件结构完整性(JSDoc、IProps、IState、Document 类)');
|
|
541
|
+
console.log(' - render() 方法约束(无变量声明、无逻辑语句)');
|
|
542
|
+
console.log(' - TypeScript 语法限制(方法中禁用类型注解)');
|
|
543
|
+
console.log(' - JSX 语法限制(只有 render() 方法可以包含 JSX)');
|
|
544
|
+
console.log(' - 对象安全访问(推荐使用可选链)');
|
|
545
|
+
})
|
|
546
|
+
.action(async function doCheckTSX(command) {
|
|
547
|
+
const options = command.opts();
|
|
548
|
+
if (options.cwd) {
|
|
549
|
+
process.chdir(options.cwd);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const { checkTSX } = require('../dist/index.js');
|
|
554
|
+
|
|
555
|
+
const result = await checkTSX({
|
|
556
|
+
files: options.files || ['**/*.tsx'], // 设置默认值
|
|
557
|
+
cwd: options.cwd,
|
|
558
|
+
recursive: options.recursive,
|
|
559
|
+
errorsOnly: options.errorsOnly,
|
|
560
|
+
verbose: options.verbose,
|
|
561
|
+
format: options.format,
|
|
562
|
+
exitOnError: options.exitOnError,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// 如果是 JSON 格式,不需要额外处理,checkTSX 已经输出了
|
|
566
|
+
if (options.format !== 'json') {
|
|
567
|
+
if (result.nonCompliantFiles > 0 && options.exitOnError) {
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('TSX 检查失败:', error);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// init-claude-hooks 命令
|
|
578
|
+
program
|
|
579
|
+
.command('init-claude-hooks')
|
|
580
|
+
.description('初始化 Claude Code TSX 检查器配置')
|
|
581
|
+
.option('-f, --force', '覆盖已存在的配置文件')
|
|
582
|
+
.option('-p, --project-only', '只初始化项目配置,不修改全局配置')
|
|
583
|
+
.option('-v, --verbose', '显示详细输出')
|
|
584
|
+
.option('-c, --cwd <cwd>', '工作目录', process.cwd())
|
|
585
|
+
.action(async function doInitClaudeHooks(command) {
|
|
586
|
+
const options = command.opts();
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const { initClaudeHooks } = require('../dist/index.js');
|
|
590
|
+
|
|
591
|
+
const result = await initClaudeHooks({
|
|
592
|
+
force: options.force,
|
|
593
|
+
projectOnly: options.projectOnly,
|
|
594
|
+
verbose: options.verbose,
|
|
595
|
+
cwd: options.cwd,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (!result.success) {
|
|
599
|
+
console.error('❌ 初始化失败:', result.error);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error('❌ 初始化失败:', error.message);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// analyze-tsx-hooks 命令
|
|
609
|
+
program
|
|
610
|
+
.command('analyze-tsx-hooks')
|
|
611
|
+
.description('分析 TSX Hook 日志,排查误判问题')
|
|
612
|
+
.option('-r, --recent <number>', '显示最近的 N 条记录', '50')
|
|
613
|
+
.option('-b, --blocked-only', '只显示被阻止的文件')
|
|
614
|
+
.option('-p, --passed-only', '只显示通过的文件')
|
|
615
|
+
.option('-v, --verbose', '显示详细信息')
|
|
616
|
+
.option('-f, --file <filename>', '分析特定文件')
|
|
617
|
+
.option('-c, --cleanup', '清理旧日志')
|
|
618
|
+
.action(async function doAnalyzeTSXHooks(command) {
|
|
619
|
+
const options = command.opts();
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const { analyzeTSXHooks } = require('../dist/index.js');
|
|
623
|
+
|
|
624
|
+
await analyzeTSXHooks({
|
|
625
|
+
recent: parseInt(options.recent),
|
|
626
|
+
blockedOnly: options.blockedOnly,
|
|
627
|
+
passedOnly: options.passedOnly,
|
|
628
|
+
verbose: options.verbose,
|
|
629
|
+
file: options.file,
|
|
630
|
+
cleanup: options.cleanup,
|
|
631
|
+
});
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error('❌ 分析失败:', error.message);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
420
638
|
program.parse(process.argv);
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.TSXComplianceChecker = void 0;
|
|
30
|
+
const parser_1 = require("@babel/parser");
|
|
31
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
32
|
+
const t = __importStar(require("@babel/types"));
|
|
33
|
+
class TSXComplianceChecker {
|
|
34
|
+
/**
|
|
35
|
+
* 使用 AST 检查 TSX 规范
|
|
36
|
+
*/
|
|
37
|
+
static checkTSXCompliance(content, filename) {
|
|
38
|
+
const issues = [];
|
|
39
|
+
try {
|
|
40
|
+
// 使用 Babel 解析 TSX 代码
|
|
41
|
+
const ast = (0, parser_1.parse)(content, {
|
|
42
|
+
sourceType: 'module',
|
|
43
|
+
plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties'],
|
|
44
|
+
errorRecovery: true,
|
|
45
|
+
});
|
|
46
|
+
// 遍历 AST 进行检查
|
|
47
|
+
(0, traverse_1.default)(ast, {
|
|
48
|
+
// 检查变量声明
|
|
49
|
+
VariableDeclarator(path) {
|
|
50
|
+
TSXComplianceChecker.checkVariableDeclaration(path, content, issues);
|
|
51
|
+
},
|
|
52
|
+
// 检查方法定义
|
|
53
|
+
ClassMethod(path) {
|
|
54
|
+
TSXComplianceChecker.checkMethodDefinition(path, content, issues);
|
|
55
|
+
},
|
|
56
|
+
// 检查 JSX 使用
|
|
57
|
+
JSXElement(path) {
|
|
58
|
+
TSXComplianceChecker.checkJSXUsage(path, content, issues);
|
|
59
|
+
},
|
|
60
|
+
// 检查类型注解
|
|
61
|
+
TSTypeAnnotation(path) {
|
|
62
|
+
TSXComplianceChecker.checkTypeAnnotation(path, content, issues);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// 检查必需的结构
|
|
66
|
+
this.checkRequiredStructures(ast, content, issues);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
issues.push({
|
|
70
|
+
type: 'error',
|
|
71
|
+
code: 'PARSE_ERROR',
|
|
72
|
+
message: `代码解析失败: ${error.message}`,
|
|
73
|
+
suggestion: '检查代码语法是否正确',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
isCompliant: issues.filter((issue) => issue.type === 'error').length === 0,
|
|
78
|
+
issues,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 检查变量声明
|
|
83
|
+
*/
|
|
84
|
+
static checkVariableDeclaration(path, content, issues) {
|
|
85
|
+
const node = path.node;
|
|
86
|
+
// 检查是否在 render 方法中
|
|
87
|
+
const renderMethod = path.findParent((p) => p.isClassMethod() &&
|
|
88
|
+
t.isIdentifier(p.node.key) &&
|
|
89
|
+
p.node.key.name === 'render');
|
|
90
|
+
if (!renderMethod)
|
|
91
|
+
return;
|
|
92
|
+
// 检查是否在事件处理函数中
|
|
93
|
+
const isInEventHandler = this.isInEventHandler(path);
|
|
94
|
+
if (isInEventHandler)
|
|
95
|
+
return;
|
|
96
|
+
// 检查是否在非 JSX 返回的回调函数中
|
|
97
|
+
const isInNonJSXCallback = this.isInNonJSXCallback(path);
|
|
98
|
+
if (isInNonJSXCallback)
|
|
99
|
+
return;
|
|
100
|
+
// 检查是否在返回 JSX 的回调函数中(如 .map())
|
|
101
|
+
const isInJSXCallback = this.isInJSXCallback(path);
|
|
102
|
+
if (isInJSXCallback) {
|
|
103
|
+
// 这种情况需要报错
|
|
104
|
+
const { line, column } = this.getLocation(path, content);
|
|
105
|
+
const snippet = this.extractCodeSnippet(content, line);
|
|
106
|
+
issues.push({
|
|
107
|
+
type: 'error',
|
|
108
|
+
code: 'CALLBACK_VARIABLE_DECLARATION',
|
|
109
|
+
message: '返回 JSX 的回调函数中不能包含变量声明',
|
|
110
|
+
suggestion: '将变量声明移到回调函数外部,或使用内联表达式',
|
|
111
|
+
line,
|
|
112
|
+
column,
|
|
113
|
+
codeSnippet: snippet.snippet,
|
|
114
|
+
snippetStartLine: snippet.startLine,
|
|
115
|
+
snippetEndLine: snippet.endLine,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// 其他在 render 方法中的变量声明都是错误的
|
|
120
|
+
const { line, column } = this.getLocation(path, content);
|
|
121
|
+
const snippet = this.extractCodeSnippet(content, line);
|
|
122
|
+
issues.push({
|
|
123
|
+
type: 'error',
|
|
124
|
+
code: 'RENDER_VARIABLE_DECLARATION',
|
|
125
|
+
message: 'render() 方法中不能包含变量声明',
|
|
126
|
+
suggestion: '移除所有 const/let/var 声明,直接使用 this.state.xxx',
|
|
127
|
+
line,
|
|
128
|
+
column,
|
|
129
|
+
codeSnippet: snippet.snippet,
|
|
130
|
+
snippetStartLine: snippet.startLine,
|
|
131
|
+
snippetEndLine: snippet.endLine,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 检查是否在事件处理函数中
|
|
136
|
+
*/
|
|
137
|
+
static isInEventHandler(path) {
|
|
138
|
+
// 查找父级 JSX 属性
|
|
139
|
+
const jsxAttribute = path.findParent((p) => p.isJSXAttribute());
|
|
140
|
+
if (jsxAttribute && t.isJSXIdentifier(jsxAttribute.node.name)) {
|
|
141
|
+
const attrName = jsxAttribute.node.name.name;
|
|
142
|
+
// 检查是否是事件处理属性(onXxx)
|
|
143
|
+
if (/^on[A-Z]/.test(attrName)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 检查是否在 addEventListener 回调中
|
|
148
|
+
const callExpression = path.findParent((p) => p.isCallExpression());
|
|
149
|
+
if (callExpression && t.isMemberExpression(callExpression.node.callee)) {
|
|
150
|
+
const memberExpr = callExpression.node.callee;
|
|
151
|
+
if (t.isIdentifier(memberExpr.property) &&
|
|
152
|
+
memberExpr.property.name === 'addEventListener') {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 检查是否在非 JSX 返回的回调函数中
|
|
160
|
+
*/
|
|
161
|
+
static isInNonJSXCallback(path) {
|
|
162
|
+
// 查找父级函数
|
|
163
|
+
const parentFunction = path.findParent((p) => p.isArrowFunctionExpression() || p.isFunctionExpression());
|
|
164
|
+
if (!parentFunction)
|
|
165
|
+
return false;
|
|
166
|
+
// 检查函数是否返回 JSX
|
|
167
|
+
const returnsJSX = this.functionReturnsJSX(parentFunction);
|
|
168
|
+
// 如果不返回 JSX,则认为是非 JSX 回调
|
|
169
|
+
return !returnsJSX;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 检查是否在返回 JSX 的回调函数中
|
|
173
|
+
*/
|
|
174
|
+
static isInJSXCallback(path) {
|
|
175
|
+
// 查找父级函数
|
|
176
|
+
const parentFunction = path.findParent((p) => p.isArrowFunctionExpression() || p.isFunctionExpression());
|
|
177
|
+
if (!parentFunction)
|
|
178
|
+
return false;
|
|
179
|
+
// 检查函数是否返回 JSX
|
|
180
|
+
return this.functionReturnsJSX(parentFunction);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 检查函数是否返回 JSX
|
|
184
|
+
*/
|
|
185
|
+
static functionReturnsJSX(functionPath) {
|
|
186
|
+
let returnsJSX = false;
|
|
187
|
+
functionPath.traverse({
|
|
188
|
+
ReturnStatement(path) {
|
|
189
|
+
if (path.node.argument && t.isJSXElement(path.node.argument)) {
|
|
190
|
+
returnsJSX = true;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
JSXElement(path) {
|
|
194
|
+
// 如果函数体直接包含 JSX(箭头函数的隐式返回)
|
|
195
|
+
if (path.parent === functionPath.node.body) {
|
|
196
|
+
returnsJSX = true;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
return returnsJSX;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 检查方法定义
|
|
204
|
+
*/
|
|
205
|
+
static checkMethodDefinition(path, content, issues) {
|
|
206
|
+
const node = path.node;
|
|
207
|
+
// 跳过 render 方法和渲染相关方法
|
|
208
|
+
if (t.isIdentifier(node.key)) {
|
|
209
|
+
const methodName = node.key.name;
|
|
210
|
+
const renderMethods = [
|
|
211
|
+
'render',
|
|
212
|
+
'renderItem',
|
|
213
|
+
'renderContent',
|
|
214
|
+
'renderHeader',
|
|
215
|
+
'renderFooter',
|
|
216
|
+
];
|
|
217
|
+
if (renderMethods.includes(methodName))
|
|
218
|
+
return;
|
|
219
|
+
// 检查方法中是否包含 JSX
|
|
220
|
+
let containsJSX = false;
|
|
221
|
+
path.traverse({
|
|
222
|
+
JSXElement() {
|
|
223
|
+
containsJSX = true;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
if (containsJSX) {
|
|
227
|
+
const { line, column } = this.getLocation(path, content);
|
|
228
|
+
const snippet = this.extractCodeSnippet(content, line);
|
|
229
|
+
issues.push({
|
|
230
|
+
type: 'error',
|
|
231
|
+
code: 'METHOD_CONTAINS_JSX',
|
|
232
|
+
message: `方法 ${methodName} 不能包含 JSX 语法`,
|
|
233
|
+
suggestion: '只有 render() 方法可以包含 JSX,其他方法只处理逻辑',
|
|
234
|
+
line,
|
|
235
|
+
column,
|
|
236
|
+
codeSnippet: snippet.snippet,
|
|
237
|
+
snippetStartLine: snippet.startLine,
|
|
238
|
+
snippetEndLine: snippet.endLine,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 检查 JSX 使用
|
|
245
|
+
*/
|
|
246
|
+
static checkJSXUsage(path, content, issues) {
|
|
247
|
+
// 暂时不实现,专注于变量声明检查
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 检查类型注解
|
|
251
|
+
*/
|
|
252
|
+
static checkTypeAnnotation(path, content, issues) {
|
|
253
|
+
// 暂时不实现,专注于变量声明检查
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 检查必需的结构
|
|
257
|
+
*/
|
|
258
|
+
static checkRequiredStructures(ast, content, issues) {
|
|
259
|
+
// 这里可以添加对必需结构的检查
|
|
260
|
+
// 如 IProps, IState, Document 类等
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 获取节点在源码中的位置
|
|
264
|
+
*/
|
|
265
|
+
static getLocation(path, content) {
|
|
266
|
+
const loc = path.node.loc;
|
|
267
|
+
return {
|
|
268
|
+
line: loc ? loc.start.line : 1,
|
|
269
|
+
column: loc ? loc.start.column : 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 提取代码片段
|
|
274
|
+
*/
|
|
275
|
+
static extractCodeSnippet(content, lineNumber, contextLines = 3) {
|
|
276
|
+
const lines = content.split('\n');
|
|
277
|
+
const startLine = Math.max(1, lineNumber - contextLines);
|
|
278
|
+
const endLine = Math.min(lines.length, lineNumber + contextLines);
|
|
279
|
+
const snippetLines = [];
|
|
280
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
281
|
+
const line = lines[i - 1] || '';
|
|
282
|
+
const prefix = i === lineNumber ? '>>> ' : ' ';
|
|
283
|
+
snippetLines.push(`${prefix}${i.toString().padStart(3)}: ${line}`);
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
snippet: snippetLines.join('\n'),
|
|
287
|
+
startLine,
|
|
288
|
+
endLine,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 生成检查报告
|
|
293
|
+
*/
|
|
294
|
+
static generateReport(result, filename) {
|
|
295
|
+
const { isCompliant, issues } = result;
|
|
296
|
+
let report = '=== TSX 规范检查报告 ===\n';
|
|
297
|
+
report += `文件: ${filename}\n`;
|
|
298
|
+
if (isCompliant) {
|
|
299
|
+
report += '✅ 检查通过:文件符合 TSX 规范\n';
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const errorCount = issues.filter((issue) => issue.type === 'error').length;
|
|
303
|
+
report += `❌ 检查失败:发现 ${errorCount} 个错误\n\n`;
|
|
304
|
+
report += '--- 问题详情 ---\n';
|
|
305
|
+
issues.forEach((issue, index) => {
|
|
306
|
+
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
307
|
+
report += `${index + 1}. ${icon} [${issue.code}] ${issue.message}\n`;
|
|
308
|
+
if (issue.line) {
|
|
309
|
+
report += ` 📍 位置: 第 ${issue.line} 行\n`;
|
|
310
|
+
}
|
|
311
|
+
report += ` 💡 建议: ${issue.suggestion}\n`;
|
|
312
|
+
if (issue.codeSnippet) {
|
|
313
|
+
report += ` 📝 代码片段:\n${issue.codeSnippet}\n`;
|
|
314
|
+
}
|
|
315
|
+
report += '\n';
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return report;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.TSXComplianceChecker = TSXComplianceChecker;
|