@appthen/cli 1.2.2 → 1.2.3

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.
@@ -1,683 +0,0 @@
1
- "use strict";
2
- /**
3
- * TSX 规范检查器 - 共享版本
4
- * 用于检查 TSX 文件是否符合平台特定的语法规范
5
- * 注意:此文件与 packages/cli/src/services/TSXComplianceChecker.ts 保持同步
6
- */
7
- Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.TSXComplianceChecker = void 0;
9
- class TSXComplianceChecker {
10
- /**
11
- * 检查 TSX 文件内容是否符合规范
12
- */
13
- static checkTSXCompliance(content, filePath) {
14
- const issues = [];
15
- const suggestions = [];
16
- // 检查是否是 TSX 文件
17
- if (filePath && !filePath.endsWith('.tsx')) {
18
- return { isCompliant: true, issues: [], suggestions: [] };
19
- }
20
- // 1. 检查文件结构完整性
21
- this.checkFileStructure(content, issues, suggestions);
22
- // 2. 检查 render() 方法约束
23
- this.checkRenderMethod(content, issues, suggestions);
24
- // 3. 检查类方法中的 TypeScript 语法
25
- this.checkMethodTypeScript(content, issues, suggestions);
26
- // 4. 检查类方法中的 JSX 语法
27
- this.checkMethodJSX(content, issues, suggestions);
28
- // 5. 检查对象安全访问
29
- this.checkObjectAccess(content, issues, suggestions);
30
- // 6. 检查回调函数中的变量声明
31
- this.checkCallbackVariableDeclarations(content, issues, suggestions);
32
- const isCompliant = issues.filter((issue) => issue.type === 'error').length === 0;
33
- return {
34
- isCompliant,
35
- issues,
36
- suggestions,
37
- };
38
- }
39
- /**
40
- * 检查文件结构完整性
41
- */
42
- static checkFileStructure(content, issues, suggestions) {
43
- // 检查 JSDoc 元数据注释
44
- if (!content.includes('@type Page') &&
45
- !content.includes('@type Component')) {
46
- issues.push({
47
- type: 'error',
48
- code: 'MISSING_METADATA',
49
- message: '缺少必需的 JSDoc 元数据注释 (@type Page|Component)',
50
- suggestion: '在文件顶部添加: /**\n * 组件标题\n * @type Page|Component\n */',
51
- });
52
- }
53
- // 检查 IProps 类
54
- if (!content.includes('class IProps')) {
55
- issues.push({
56
- type: 'error',
57
- code: 'MISSING_IPROPS',
58
- message: '缺少 IProps 类定义',
59
- suggestion: '添加: class IProps { /* 属性定义 */ }',
60
- });
61
- }
62
- // 检查 IState 类
63
- if (!content.includes('class IState')) {
64
- issues.push({
65
- type: 'error',
66
- code: 'MISSING_ISTATE',
67
- message: '缺少 IState 类定义',
68
- suggestion: '添加: class IState { /* 状态定义 */ }',
69
- });
70
- }
71
- // 检查 Document 类
72
- if (!content.includes('class Document extends React.Component')) {
73
- issues.push({
74
- type: 'error',
75
- code: 'MISSING_DOCUMENT_CLASS',
76
- message: '缺少 Document 类定义',
77
- suggestion: '添加: class Document extends React.Component<IProps, IState> { /* 组件实现 */ }',
78
- });
79
- }
80
- // 检查 export default
81
- if (!content.includes('export default Document')) {
82
- issues.push({
83
- type: 'error',
84
- code: 'MISSING_EXPORT',
85
- message: '缺少 export default Document',
86
- suggestion: '在文件末尾添加: export default Document;',
87
- });
88
- }
89
- }
90
- /**
91
- * 检查 render() 方法约束 - 使用 AST 方式
92
- */
93
- static checkRenderMethod(content, issues, suggestions) {
94
- // 先找到 class Document 部分
95
- const documentCodeIndex = content.search(/class\s+(\w+)\s+extends\s+React\.Component/);
96
- if (documentCodeIndex === -1) {
97
- issues.push({
98
- type: 'error',
99
- code: 'MISSING_DOCUMENT_CLASS',
100
- message: '缺少 Document 类定义',
101
- suggestion: '添加: class Document extends React.Component<IProps, IState> { /* 组件实现 */ }',
102
- });
103
- return;
104
- }
105
- // 只处理 class Document 部分,避免解析非常规的 IProps/IState 语法
106
- const documentCode = content.slice(documentCodeIndex);
107
- try {
108
- // 使用 AST 检查 render() 方法
109
- this.checkRenderMethodWithAST(documentCode, issues, suggestions);
110
- }
111
- catch (error) {
112
- // 如果 AST 解析失败,回退到原来的正则表达式方法
113
- console.warn('AST 解析失败,回退到正则表达式方法:', error);
114
- this.checkRenderMethodWithRegex(content, issues, suggestions);
115
- }
116
- }
117
- /**
118
- * 使用 AST 检查 render() 方法
119
- */
120
- static checkRenderMethodWithAST(documentCode, issues, suggestions) {
121
- // 这里先用简化的方法,后续可以集成真正的 AST 解析
122
- // 目前先实现基本的逻辑,确保不会破坏现有功能
123
- this.checkRenderMethodWithRegex(documentCode, issues, suggestions);
124
- }
125
- /**
126
- * 使用正则表达式检查 render() 方法(回退方法)
127
- */
128
- static checkRenderMethodWithRegex(content, issues, suggestions) {
129
- // 更精确的 render 方法匹配
130
- const renderBody = this.extractRenderMethodBody(content);
131
- if (!renderBody) {
132
- issues.push({
133
- type: 'error',
134
- code: 'MISSING_RENDER_METHOD',
135
- message: '缺少 render() 方法',
136
- suggestion: '添加: render() { return (<div>内容</div>); }',
137
- });
138
- return;
139
- }
140
- // 检查变量声明(排除事件处理函数内部,但包含.map()回调)
141
- const renderBodyWithoutEventHandlers = this.removeEventHandlers(renderBody);
142
- const varDeclMatch = renderBodyWithoutEventHandlers.match(/\b(const|let|var)\s+\w+/);
143
- if (varDeclMatch) {
144
- // 如果在处理后的内容中仍然找到变量声明,说明这个变量声明不在事件处理函数中
145
- // 现在需要在原始内容中找到对应的位置来报告错误
146
- const varPattern = varDeclMatch[0];
147
- const processedMatchIndex = renderBodyWithoutEventHandlers.indexOf(varPattern);
148
- // 在原始renderBody中找到相同的变量声明模式
149
- const originalMatchIndex = renderBody.indexOf(varPattern);
150
- if (originalMatchIndex !== -1) {
151
- // 检查这个变量声明是否在.map()回调中
152
- const isInMapCallback = this.isVariableInMapCallback(renderBody, originalMatchIndex);
153
- if (isInMapCallback) {
154
- // 这个错误会在checkCallbackVariableDeclarations中处理
155
- return;
156
- }
157
- // 报告错误
158
- const renderStartIndex = content.indexOf(renderBody);
159
- const absoluteIndex = renderStartIndex + originalMatchIndex;
160
- const lineNumber = this.getLineNumber(content, absoluteIndex);
161
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
162
- issues.push({
163
- type: 'error',
164
- code: 'RENDER_VARIABLE_DECLARATION',
165
- message: 'render() 方法中不能包含变量声明',
166
- suggestion: '移除所有 const/let/var 声明,直接使用 this.state.xxx',
167
- line: lineNumber,
168
- codeSnippet: snippet,
169
- snippetStartLine: startLine,
170
- snippetEndLine: endLine,
171
- });
172
- }
173
- }
174
- // 检查解构赋值(在移除事件处理函数后的内容中检查)
175
- const destructMatch = renderBodyWithoutEventHandlers.match(/\b(const|let|var)\s*\{/);
176
- if (destructMatch) {
177
- // 在原始renderBody中找到对应的解构赋值
178
- const originalDestructMatch = renderBody.match(/\b(const|let|var)\s*\{/);
179
- if (originalDestructMatch) {
180
- const matchIndex = renderBody.indexOf(originalDestructMatch[0]);
181
- const renderStartIndex = content.indexOf(renderBody);
182
- const absoluteIndex = renderStartIndex + matchIndex;
183
- const lineNumber = this.getLineNumber(content, absoluteIndex);
184
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
185
- issues.push({
186
- type: 'error',
187
- code: 'RENDER_DESTRUCTURING',
188
- message: 'render() 方法中不能包含解构赋值',
189
- suggestion: '使用 this.state.xxx 直接访问,而不是解构',
190
- line: lineNumber,
191
- codeSnippet: snippet,
192
- snippetStartLine: startLine,
193
- snippetEndLine: endLine,
194
- });
195
- }
196
- }
197
- // 检查逻辑语句(排除事件处理函数内部)
198
- if (/\b(if|for|while|switch)\s*\(/.test(renderBodyWithoutEventHandlers)) {
199
- issues.push({
200
- type: 'error',
201
- code: 'RENDER_LOGIC_STATEMENTS',
202
- message: 'render() 方法中不能包含 if/for/while/switch 等逻辑语句',
203
- suggestion: '使用条件渲染: {!!(condition) && <JSX>}',
204
- });
205
- }
206
- // 检查 return 是否在第一行
207
- const firstLine = renderBody.trim().split('\n')[0];
208
- if (!firstLine.includes('return')) {
209
- issues.push({
210
- type: 'error',
211
- code: 'RENDER_RETURN_NOT_FIRST',
212
- message: 'render() 方法第一行必须是 return (',
213
- suggestion: '确保 render() 方法第一行是: return (',
214
- });
215
- }
216
- // 三元表达式检查已移除 - 代码转换器会自动处理
217
- }
218
- /**
219
- * 提取 render 方法体
220
- */
221
- static extractRenderMethodBody(content) {
222
- // 查找 render() 方法的开始位置
223
- const renderMatch = content.match(/render\s*\(\s*\)\s*\{/);
224
- if (!renderMatch) {
225
- return null;
226
- }
227
- const startIndex = renderMatch.index + renderMatch[0].length;
228
- let braceCount = 1;
229
- let endIndex = startIndex;
230
- // 找到匹配的闭合大括号
231
- for (let i = startIndex; i < content.length && braceCount > 0; i++) {
232
- const char = content[i];
233
- if (char === '{') {
234
- braceCount++;
235
- }
236
- else if (char === '}') {
237
- braceCount--;
238
- }
239
- endIndex = i;
240
- }
241
- if (braceCount !== 0) {
242
- return null; // 大括号不匹配
243
- }
244
- return content.substring(startIndex, endIndex);
245
- }
246
- /**
247
- * 提取代码片段,用于错误展示
248
- */
249
- static extractCodeSnippet(content, targetLine, contextLines = 3) {
250
- const lines = content.split('\n');
251
- const startLine = Math.max(1, targetLine - contextLines);
252
- const endLine = Math.min(lines.length, targetLine + contextLines);
253
- const snippetLines = lines.slice(startLine - 1, endLine);
254
- const snippet = snippetLines
255
- .map((line, index) => {
256
- const lineNumber = startLine + index;
257
- const marker = lineNumber === targetLine ? '>>> ' : ' ';
258
- return `${marker}${lineNumber.toString().padStart(3)}: ${line}`;
259
- })
260
- .join('\n');
261
- return { snippet, startLine, endLine };
262
- }
263
- /**
264
- * 根据正则匹配结果获取行号
265
- */
266
- static getLineNumber(content, matchIndex) {
267
- const beforeMatch = content.substring(0, matchIndex);
268
- return (beforeMatch.match(/\n/g) || []).length + 1;
269
- }
270
- /**
271
- * 检查变量声明是否在.map()回调函数中
272
- */
273
- static isVariableInMapCallback(renderBody, varIndex) {
274
- // 查找所有.map()回调函数的位置
275
- const mapRegex = /\.map\s*\(\s*\([^)]*\)\s*=>\s*\{/g;
276
- let match;
277
- while ((match = mapRegex.exec(renderBody)) !== null) {
278
- const callbackStart = match.index + match[0].length;
279
- // 找到对应的结束大括号
280
- let braceCount = 1;
281
- let callbackEnd = callbackStart;
282
- for (let i = callbackStart; i < renderBody.length && braceCount > 0; i++) {
283
- if (renderBody[i] === '{')
284
- braceCount++;
285
- if (renderBody[i] === '}')
286
- braceCount--;
287
- callbackEnd = i;
288
- }
289
- // 检查变量声明是否在这个回调函数范围内
290
- if (varIndex >= callbackStart && varIndex <= callbackEnd) {
291
- return true;
292
- }
293
- }
294
- return false;
295
- }
296
- /**
297
- * 移除事件处理函数内容,但保留.map()等数组方法回调
298
- */
299
- static removeEventHandlers(renderBody) {
300
- let result = renderBody;
301
- // 注意:不移除.map()等数组方法回调,因为需要单独检查它们
302
- // 移除事件处理函数(但保留.map()等数组方法)
303
- const lines = result.split('\n');
304
- const processedLines = [];
305
- let inEventHandler = false;
306
- let braceCount = 0;
307
- for (let i = 0; i < lines.length; i++) {
308
- const line = lines[i];
309
- // 检查事件处理函数和其他非JSX返回的回调函数
310
- const eventHandlerMatch = line.match(/on\w+\s*=\s*\{/) || // onXxx={
311
- line.match(/on\w+\s*=\s*\([^)]*\)\s*=>\s*\{/) || // onXxx={(params) => {
312
- line.match(/on\w+\s*=\s*\{\s*\([^)]*\)\s*=>\s*\{/) || // onXxx={ (params) => {
313
- line.match(/on\w+\s*=\s*\{\s*function\s*\([^)]*\)\s*\{/) || // onXxx={ function() {
314
- line.match(/on\w+\s*=\s*function\s*\([^)]*\)\s*\{/) ||
315
- // 添加其他非JSX返回的回调函数
316
- line.match(/request\s*=\s*\{/) ||
317
- line.match(/request\s*=\s*\([^)]*\)\s*=>\s*\{/) ||
318
- line.match(/request\s*=\s*\{\s*\([^)]*\)\s*=>\s*\{/) ||
319
- line.match(/request\s*=\s*\{\s*async\s*\([^)]*\)\s*=>\s*\{/) ||
320
- line.match(/request\s*=\s*async\s*\([^)]*\)\s*=>\s*\{/) ||
321
- // 添加addEventListener等事件监听器
322
- line.match(/addEventListener\s*\(\s*['"][^'"]*['"]\s*,\s*\([^)]*\)\s*=>\s*\{/) ||
323
- line.match(/addEventListener\s*\(\s*['"][^'"]*['"]\s*,\s*function\s*\([^)]*\)\s*\{/) ||
324
- line.match(/addEventListener\s*\(\s*['"][^'"]*['"]\s*,\s*\{/) ||
325
- // 添加其他常见的回调函数模式
326
- line.match(/\.\w+\s*\(\s*\([^)]*\)\s*=>\s*\{/) || // .method((params) => {
327
- line.match(/\.\w+\s*\(\s*function\s*\([^)]*\)\s*\{/); // .method(function(params) {
328
- if (eventHandlerMatch) {
329
- inEventHandler = true;
330
- braceCount = 0;
331
- // 计算这一行的大括号
332
- for (const char of line) {
333
- if (char === '{')
334
- braceCount++;
335
- if (char === '}')
336
- braceCount--;
337
- }
338
- if (braceCount <= 0) {
339
- // 单行事件处理函数
340
- processedLines.push(line.replace(/on\w+\s*=\s*\{[^}]*\}/, 'onEvent={() => {}}'));
341
- inEventHandler = false;
342
- }
343
- else {
344
- // 多行事件处理函数开始
345
- processedLines.push(line.replace(/on\w+\s*=\s*\{.*/, 'onEvent={() => {'));
346
- }
347
- continue;
348
- }
349
- if (inEventHandler) {
350
- // 在事件处理函数内部,计算大括号
351
- for (const char of line) {
352
- if (char === '{')
353
- braceCount++;
354
- if (char === '}')
355
- braceCount--;
356
- }
357
- if (braceCount <= 0) {
358
- // 事件处理函数结束
359
- processedLines.push('}}');
360
- inEventHandler = false;
361
- }
362
- else {
363
- // 仍在事件处理函数内部,跳过这一行(移除变量声明)
364
- processedLines.push('// event handler content removed');
365
- }
366
- }
367
- else {
368
- // 正常行,保持不变(包括.map()等数组方法)
369
- processedLines.push(line);
370
- }
371
- }
372
- return processedLines.join('\n');
373
- }
374
- /**
375
- * 检查类方法中的 TypeScript 语法
376
- */
377
- static checkMethodTypeScript(content, issues, suggestions) {
378
- // 检查方法参数类型注解(更精确的匹配)
379
- // 只匹配真正的类方法,不匹配函数调用或其他语法
380
- const methodParamTypeRegex = /^\s*(\w+)\s*\([^)]*:\s*[^)]+\)\s*\{/gm;
381
- let match;
382
- while ((match = methodParamTypeRegex.exec(content)) !== null) {
383
- const methodName = match[1];
384
- // 跳过一些常见的非方法名称
385
- const skipNames = ['if', 'for', 'while', 'switch', 'catch', 'function'];
386
- if (skipNames.includes(methodName))
387
- continue;
388
- if (methodName !== 'render') {
389
- const lineNumber = this.getLineNumber(content, match.index);
390
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
391
- issues.push({
392
- type: 'error',
393
- code: 'METHOD_PARAM_TYPE_ANNOTATION',
394
- message: `方法 ${methodName} 不能包含参数类型注解`,
395
- suggestion: `将 ${methodName}(param: Type) 改为 ${methodName}(param)`,
396
- line: lineNumber,
397
- codeSnippet: snippet,
398
- snippetStartLine: startLine,
399
- snippetEndLine: endLine,
400
- });
401
- }
402
- }
403
- // 检查方法返回类型注解(只检查类方法,不检查数组方法调用)
404
- const methodReturnTypeRegex = /^\s*(\w+)\s*\([^)]*\)\s*:\s*\w+/gm;
405
- while ((match = methodReturnTypeRegex.exec(content)) !== null) {
406
- const methodName = match[1];
407
- // 排除数组方法调用(如 .filter, .map 等)和常见的非方法名称
408
- const skipNames = [
409
- 'filter',
410
- 'map',
411
- 'forEach',
412
- 'reduce',
413
- 'find',
414
- 'some',
415
- 'every',
416
- 'if',
417
- 'for',
418
- 'while',
419
- 'switch',
420
- 'catch',
421
- 'function',
422
- ];
423
- if (!skipNames.includes(methodName)) {
424
- const lineNumber = this.getLineNumber(content, match.index);
425
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
426
- issues.push({
427
- type: 'error',
428
- code: 'METHOD_RETURN_TYPE_ANNOTATION',
429
- message: `方法 ${methodName} 不能包含返回类型注解`,
430
- suggestion: `将 ${methodName}(): Type 改为 ${methodName}()`,
431
- line: lineNumber,
432
- codeSnippet: snippet,
433
- snippetStartLine: startLine,
434
- snippetEndLine: endLine,
435
- });
436
- }
437
- }
438
- // 检查类型断言
439
- if (/\w+\s+as\s+\w+/.test(content)) {
440
- issues.push({
441
- type: 'error',
442
- code: 'TYPE_ASSERTION',
443
- message: '不能使用类型断言 (as)',
444
- suggestion: '直接使用变量,移除 as 断言',
445
- });
446
- }
447
- // 检查泛型语法(除了 React.Component<IProps, IState>)
448
- // 更精确的泛型匹配,避免匹配JSX标签
449
- const genericRegex = /\b(\w+)<([^<>]+)>/g;
450
- while ((match = genericRegex.exec(content)) !== null) {
451
- const fullMatch = match[0];
452
- const typeName = match[1];
453
- const typeParams = match[2];
454
- // 跳过JSX标签(JSX标签通常以大写字母开头,且在<>内包含属性或文本)
455
- // 检查是否是JSX标签的结束部分(如 "detected</div>")
456
- if (typeParams.includes('/') ||
457
- typeParams.includes(' ') ||
458
- /^[A-Z]/.test(typeName)) {
459
- continue;
460
- }
461
- // 允许的泛型语法
462
- const allowedGenerics = [
463
- 'React.Component<IProps, IState>',
464
- 'React.PureComponent<IProps, IState>',
465
- 'Component<IProps, IState>',
466
- 'PureComponent<IProps, IState>',
467
- ];
468
- const isAllowed = allowedGenerics.some((allowed) => fullMatch.includes(allowed) || content.includes(`extends ${allowed}`));
469
- if (!isAllowed) {
470
- const lineNumber = this.getLineNumber(content, match.index);
471
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
472
- issues.push({
473
- type: 'error',
474
- code: 'GENERIC_SYNTAX',
475
- message: `不能使用泛型语法: ${fullMatch}`,
476
- suggestion: '使用基础类型,如 any[] 代替 Array<T>。注意:React.Component<IProps, IState> 是允许的',
477
- line: lineNumber,
478
- codeSnippet: snippet,
479
- snippetStartLine: startLine,
480
- snippetEndLine: endLine,
481
- });
482
- }
483
- }
484
- }
485
- /**
486
- * 检查类方法中的 JSX 语法
487
- */
488
- static checkMethodJSX(content, issues, suggestions) {
489
- // 更精确的方法匹配正则表达式,只匹配真正的类方法
490
- // 匹配类方法:methodName(params) { ... }
491
- const classMethodRegex = /^\s*(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/gm;
492
- const checkMethod = (methodName, methodBody, matchIndex) => {
493
- // 跳过 render 方法和一些常见的渲染相关方法
494
- const renderMethods = [
495
- 'render',
496
- 'renderItem',
497
- 'renderContent',
498
- 'renderHeader',
499
- 'renderFooter',
500
- ];
501
- if (renderMethods.includes(methodName))
502
- return;
503
- // 检查方法中是否包含 JSX(更精确的检查)
504
- // 匹配真正的JSX标签,而不是比较操作符
505
- const jsxRegex = /<[A-Z][a-zA-Z0-9]*[\s\S]*?>/;
506
- if (jsxRegex.test(methodBody)) {
507
- const lineNumber = this.getLineNumber(content, matchIndex);
508
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
509
- issues.push({
510
- type: 'error',
511
- code: 'METHOD_CONTAINS_JSX',
512
- message: `方法 ${methodName} 不能包含 JSX 语法`,
513
- suggestion: '只有 render() 方法可以包含 JSX,其他方法只处理逻辑',
514
- line: lineNumber,
515
- codeSnippet: snippet,
516
- snippetStartLine: startLine,
517
- snippetEndLine: endLine,
518
- });
519
- }
520
- };
521
- // 检查类方法
522
- let match;
523
- while ((match = classMethodRegex.exec(content)) !== null) {
524
- checkMethod(match[1], match[2], match.index);
525
- }
526
- // 检查箭头函数属性中的 JSX(更精确的检查)
527
- const arrowFunctionRegex = /^\s*(\w+)\s*=\s*\([^)]*\)\s*=>\s*([\s\S]*?)(?=\n\s*\w+\s*[=:]|\n\s*\}|\n\s*$)/gm;
528
- while ((match = arrowFunctionRegex.exec(content)) !== null) {
529
- const propName = match[1];
530
- const functionBody = match[2];
531
- // 检查是否包含JSX
532
- const jsxRegex = /<[A-Z][a-zA-Z0-9]*[\s\S]*?>/;
533
- if (jsxRegex.test(functionBody)) {
534
- const lineNumber = this.getLineNumber(content, match.index);
535
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
536
- issues.push({
537
- type: 'error',
538
- code: 'PROPERTY_CONTAINS_JSX',
539
- message: `属性 ${propName} 不能包含 JSX 语法`,
540
- suggestion: '移除属性中的 JSX,只在 render() 方法中使用 JSX',
541
- line: lineNumber,
542
- codeSnippet: snippet,
543
- snippetStartLine: startLine,
544
- snippetEndLine: endLine,
545
- });
546
- }
547
- }
548
- }
549
- /**
550
- * 检查对象安全访问
551
- */
552
- static checkObjectAccess(content, issues, suggestions) {
553
- // 提取 state 初始化信息
554
- const stateInitMatch = content.match(/state\s*=\s*\{([\s\S]*?)\}/);
555
- const initializedProperties = new Set();
556
- if (stateInitMatch) {
557
- const stateInit = stateInitMatch[1];
558
- // 匹配已初始化的属性(有默认值的)
559
- const propertyMatches = stateInit.matchAll(/(\w+):\s*([^,\n}]+)/g);
560
- for (const propMatch of propertyMatches) {
561
- const propName = propMatch[1];
562
- const propValue = propMatch[2].trim();
563
- // 如果有非空默认值,认为是安全的
564
- if (propValue !== 'null' &&
565
- propValue !== 'undefined' &&
566
- propValue !== '' &&
567
- propValue !== "''") {
568
- initializedProperties.add(propName);
569
- }
570
- }
571
- }
572
- // 检查嵌套对象访问是否使用可选链
573
- const unsafeAccessRegex = /this\.state\.(\w+)\.(\w+)/g;
574
- let match;
575
- while ((match = unsafeAccessRegex.exec(content)) !== null) {
576
- const objectName = match[1];
577
- const propertyName = match[2];
578
- // 如果属性已经初始化为非空值,跳过检查
579
- if (initializedProperties.has(objectName)) {
580
- continue;
581
- }
582
- // 检查是否已经使用了可选链
583
- const fullMatch = match[0];
584
- const beforeMatch = content.substring(0, match.index);
585
- if (!beforeMatch.endsWith('?.') && !fullMatch.includes('?.')) {
586
- issues.push({
587
- type: 'warning',
588
- code: 'UNSAFE_OBJECT_ACCESS',
589
- message: `不安全的对象访问: ${fullMatch}`,
590
- suggestion: `使用可选链: this.state.${objectName}?.${propertyName}`,
591
- });
592
- }
593
- }
594
- }
595
- /**
596
- * 生成检查报告
597
- */
598
- static generateReport(result, filePath) {
599
- const { isCompliant, issues, suggestions } = result;
600
- let report = `\n=== TSX 规范检查报告 ===\n`;
601
- if (filePath) {
602
- report += `文件: ${filePath}\n`;
603
- }
604
- if (isCompliant) {
605
- report += `✅ 检查通过:文件符合 TSX 规范\n`;
606
- }
607
- else {
608
- report += `❌ 检查失败:发现 ${issues.filter((i) => i.type === 'error').length} 个错误\n`;
609
- }
610
- if (issues.length > 0) {
611
- report += `\n--- 问题详情 ---\n`;
612
- issues.forEach((issue, index) => {
613
- const icon = issue.type === 'error' ? '❌' : '⚠️';
614
- report += `${index + 1}. ${icon} [${issue.code}] ${issue.message}\n`;
615
- if (issue.line) {
616
- report += ` 📍 位置: 第 ${issue.line} 行\n`;
617
- }
618
- if (issue.suggestion) {
619
- report += ` 💡 建议: ${issue.suggestion}\n`;
620
- }
621
- if (issue.codeSnippet) {
622
- report += ` 📝 代码片段:\n${issue.codeSnippet}\n`;
623
- }
624
- });
625
- }
626
- if (suggestions.length > 0) {
627
- report += `\n--- 改进建议 ---\n`;
628
- suggestions.forEach((suggestion, index) => {
629
- report += `${index + 1}. ${suggestion}\n`;
630
- });
631
- }
632
- return report;
633
- }
634
- /**
635
- * 检查返回JSX的回调函数中的变量声明
636
- */
637
- static checkCallbackVariableDeclarations(content, issues, suggestions) {
638
- // 先提取render方法体
639
- const renderBody = this.extractRenderMethodBody(content);
640
- if (!renderBody) {
641
- return; // 没有render方法,跳过检查
642
- }
643
- // 检查所有数组方法的回调函数,但只针对返回JSX的情况
644
- const arrayMethodRegex = /\.(map|filter|forEach|reduce|find|some|every)\s*\(\s*\([^)]*\)\s*=>\s*\{([\s\S]*?)\}\s*\)/g;
645
- let match;
646
- while ((match = arrayMethodRegex.exec(renderBody)) !== null) {
647
- const methodName = match[1];
648
- const callbackBody = match[2];
649
- // 检查回调函数是否返回JSX(包含return <xxx>或直接返回JSX)
650
- const returnsJSX = /return\s*</.test(callbackBody) || // return <div>
651
- /return\s*\(\s*</.test(callbackBody) || // return (<div>
652
- /=>\s*</.test(match[0]) || // () => <div>
653
- /=>\s*\(\s*</.test(match[0]); // () => (<div>
654
- if (!returnsJSX) {
655
- continue; // 不返回JSX,跳过检查
656
- }
657
- // 检查回调函数体中是否有变量声明
658
- const variableDeclarationRegex = /^\s*(const|let|var)\s+\w+/gm;
659
- let varMatch;
660
- while ((varMatch = variableDeclarationRegex.exec(callbackBody)) !== null) {
661
- const declarationType = varMatch[1];
662
- // 计算在整个文件中的行号
663
- const renderStartIndex = content.indexOf(renderBody);
664
- const callbackStartIndex = renderStartIndex + match.index + match[0].indexOf('{') + 1;
665
- const varMatchIndex = callbackStartIndex + varMatch.index;
666
- const beforeMatch = content.substring(0, varMatchIndex);
667
- const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
668
- const { snippet, startLine, endLine } = this.extractCodeSnippet(content, lineNumber);
669
- issues.push({
670
- type: 'error',
671
- code: 'JSX_CALLBACK_VARIABLE_DECLARATION',
672
- message: `返回JSX的 .${methodName}() 回调函数中不能声明变量 (${declarationType})`,
673
- line: lineNumber,
674
- suggestion: '使用内联表达式,或将逻辑移到其他方法中处理数据',
675
- codeSnippet: snippet,
676
- snippetStartLine: startLine,
677
- snippetEndLine: endLine,
678
- });
679
- }
680
- }
681
- }
682
- }
683
- exports.TSXComplianceChecker = TSXComplianceChecker;