frontend-guardian-core 2.6.0
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 +21 -0
- package/bin/fg-core.js +1238 -0
- package/bin/watch-mode.js +123 -0
- package/dist/engine/cache.d.ts +68 -0
- package/dist/engine/cache.d.ts.map +1 -0
- package/dist/engine/cache.js +164 -0
- package/dist/engine/cache.js.map +1 -0
- package/dist/engine/rule-engine.d.ts +135 -0
- package/dist/engine/rule-engine.d.ts.map +1 -0
- package/dist/engine/rule-engine.js +716 -0
- package/dist/engine/rule-engine.js.map +1 -0
- package/dist/formatters/github-annotation.d.ts +36 -0
- package/dist/formatters/github-annotation.d.ts.map +1 -0
- package/dist/formatters/github-annotation.js +122 -0
- package/dist/formatters/github-annotation.js.map +1 -0
- package/dist/formatters/pr-comment.d.ts +43 -0
- package/dist/formatters/pr-comment.d.ts.map +1 -0
- package/dist/formatters/pr-comment.js +171 -0
- package/dist/formatters/pr-comment.js.map +1 -0
- package/dist/formatters/sarif.d.ts +104 -0
- package/dist/formatters/sarif.d.ts.map +1 -0
- package/dist/formatters/sarif.js +130 -0
- package/dist/formatters/sarif.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/base.d.ts +44 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +104 -0
- package/dist/integrations/base.js.map +1 -0
- package/dist/integrations/eslint.d.ts +8 -0
- package/dist/integrations/eslint.d.ts.map +1 -0
- package/dist/integrations/eslint.js +67 -0
- package/dist/integrations/eslint.js.map +1 -0
- package/dist/integrations/formatter.d.ts +35 -0
- package/dist/integrations/formatter.d.ts.map +1 -0
- package/dist/integrations/formatter.js +182 -0
- package/dist/integrations/formatter.js.map +1 -0
- package/dist/integrations/index.d.ts +17 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +25 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/stylelint.d.ts +8 -0
- package/dist/integrations/stylelint.d.ts.map +1 -0
- package/dist/integrations/stylelint.js +59 -0
- package/dist/integrations/stylelint.js.map +1 -0
- package/dist/integrations/typescript.d.ts +8 -0
- package/dist/integrations/typescript.d.ts.map +1 -0
- package/dist/integrations/typescript.js +92 -0
- package/dist/integrations/typescript.js.map +1 -0
- package/dist/rules/registry.d.ts +83 -0
- package/dist/rules/registry.d.ts.map +1 -0
- package/dist/rules/registry.js +205 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/scanners/a11y-scanner.d.ts +14 -0
- package/dist/scanners/a11y-scanner.d.ts.map +1 -0
- package/dist/scanners/a11y-scanner.js +781 -0
- package/dist/scanners/a11y-scanner.js.map +1 -0
- package/dist/scanners/component-scanner.d.ts +12 -0
- package/dist/scanners/component-scanner.d.ts.map +1 -0
- package/dist/scanners/component-scanner.js +304 -0
- package/dist/scanners/component-scanner.js.map +1 -0
- package/dist/scanners/cross-file-scanner.d.ts +18 -0
- package/dist/scanners/cross-file-scanner.d.ts.map +1 -0
- package/dist/scanners/cross-file-scanner.js +684 -0
- package/dist/scanners/cross-file-scanner.js.map +1 -0
- package/dist/scanners/hooks-scanner.d.ts +15 -0
- package/dist/scanners/hooks-scanner.d.ts.map +1 -0
- package/dist/scanners/hooks-scanner.js +670 -0
- package/dist/scanners/hooks-scanner.js.map +1 -0
- package/dist/scanners/i18n-scanner.d.ts +13 -0
- package/dist/scanners/i18n-scanner.d.ts.map +1 -0
- package/dist/scanners/i18n-scanner.js +535 -0
- package/dist/scanners/i18n-scanner.js.map +1 -0
- package/dist/scanners/naming-scanner.d.ts +19 -0
- package/dist/scanners/naming-scanner.d.ts.map +1 -0
- package/dist/scanners/naming-scanner.js +746 -0
- package/dist/scanners/naming-scanner.js.map +1 -0
- package/dist/scanners/performance-scanner.d.ts +7 -0
- package/dist/scanners/performance-scanner.d.ts.map +1 -0
- package/dist/scanners/performance-scanner.js +402 -0
- package/dist/scanners/performance-scanner.js.map +1 -0
- package/dist/scanners/platform-scanner.d.ts +15 -0
- package/dist/scanners/platform-scanner.d.ts.map +1 -0
- package/dist/scanners/platform-scanner.js +320 -0
- package/dist/scanners/platform-scanner.js.map +1 -0
- package/dist/scanners/security-scanner.d.ts +7 -0
- package/dist/scanners/security-scanner.d.ts.map +1 -0
- package/dist/scanners/security-scanner.js +349 -0
- package/dist/scanners/security-scanner.js.map +1 -0
- package/dist/scanners/svelte-scanner.d.ts +14 -0
- package/dist/scanners/svelte-scanner.d.ts.map +1 -0
- package/dist/scanners/svelte-scanner.js +228 -0
- package/dist/scanners/svelte-scanner.js.map +1 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/ast-parser.d.ts +21 -0
- package/dist/utils/ast-parser.d.ts.map +1 -0
- package/dist/utils/ast-parser.js +119 -0
- package/dist/utils/ast-parser.js.map +1 -0
- package/dist/utils/baseline.d.ts +89 -0
- package/dist/utils/baseline.d.ts.map +1 -0
- package/dist/utils/baseline.js +156 -0
- package/dist/utils/baseline.js.map +1 -0
- package/dist/utils/ci-generator.d.ts +34 -0
- package/dist/utils/ci-generator.d.ts.map +1 -0
- package/dist/utils/ci-generator.js +194 -0
- package/dist/utils/ci-generator.js.map +1 -0
- package/dist/utils/common.d.ts +8 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +38 -0
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/concurrent.d.ts +16 -0
- package/dist/utils/concurrent.d.ts.map +1 -0
- package/dist/utils/concurrent.js +49 -0
- package/dist/utils/concurrent.js.map +1 -0
- package/dist/utils/config-loader.d.ts +8 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +154 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/fix-bot.d.ts +36 -0
- package/dist/utils/fix-bot.d.ts.map +1 -0
- package/dist/utils/fix-bot.js +274 -0
- package/dist/utils/fix-bot.js.map +1 -0
- package/dist/utils/git-hooks.d.ts +55 -0
- package/dist/utils/git-hooks.d.ts.map +1 -0
- package/dist/utils/git-hooks.js +318 -0
- package/dist/utils/git-hooks.js.map +1 -0
- package/dist/utils/history-report.d.ts +72 -0
- package/dist/utils/history-report.d.ts.map +1 -0
- package/dist/utils/history-report.js +144 -0
- package/dist/utils/history-report.js.map +1 -0
- package/dist/utils/init-config.d.ts +23 -0
- package/dist/utils/init-config.d.ts.map +1 -0
- package/dist/utils/init-config.js +146 -0
- package/dist/utils/init-config.js.map +1 -0
- package/dist/utils/pr-publisher.d.ts +64 -0
- package/dist/utils/pr-publisher.d.ts.map +1 -0
- package/dist/utils/pr-publisher.js +265 -0
- package/dist/utils/pr-publisher.js.map +1 -0
- package/dist/utils/project-detector.d.ts +20 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js +342 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/report-uploader.d.ts +35 -0
- package/dist/utils/report-uploader.d.ts.map +1 -0
- package/dist/utils/report-uploader.js +106 -0
- package/dist/utils/report-uploader.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 跨文件公共部分 Scanner
|
|
4
|
+
*
|
|
5
|
+
* 检测范围:
|
|
6
|
+
* 1. 父子组件 — 未使用的 props、缺失的 props、props 类型不匹配
|
|
7
|
+
* 2. 爷孙组件 — Context 过度使用(浅层传递应使用 props)
|
|
8
|
+
* 3. 兄弟组件 — 重复代码检测、公共逻辑提取建议
|
|
9
|
+
* 4. 跨文件公共 — 相似函数/组件建议提取到公共模块
|
|
10
|
+
*
|
|
11
|
+
* 实现方式:
|
|
12
|
+
* - 先解析所有文件的 AST
|
|
13
|
+
* - 提取组件信息(props、context、函数签名)
|
|
14
|
+
* - 建立文件依赖图(import/export 关系)
|
|
15
|
+
* - 分析跨文件问题
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.crossFileRules = void 0;
|
|
22
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
23
|
+
const node_fs_1 = require("node:fs");
|
|
24
|
+
const node_path_1 = require("node:path");
|
|
25
|
+
const ast_parser_js_1 = require("../utils/ast-parser.js");
|
|
26
|
+
const common_js_1 = require("../utils/common.js");
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// 规则定义
|
|
29
|
+
// ============================================================================
|
|
30
|
+
exports.crossFileRules = [
|
|
31
|
+
{
|
|
32
|
+
id: "cross-unused-props",
|
|
33
|
+
name: "父组件传递了未使用的 props",
|
|
34
|
+
description: "父组件向子组件传递了 props,但子组件没有使用",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
category: "architecture",
|
|
37
|
+
defaultEnabled: true,
|
|
38
|
+
docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/cross-unused-props.md",
|
|
39
|
+
execute(context) {
|
|
40
|
+
const graph = buildFileGraph(context);
|
|
41
|
+
return analyzeUnusedProps(graph, context);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "cross-missing-props",
|
|
46
|
+
name: "子组件缺少必要的 props",
|
|
47
|
+
description: "子组件声明了必传的 props,但父组件没有传递",
|
|
48
|
+
severity: "warning",
|
|
49
|
+
category: "architecture",
|
|
50
|
+
defaultEnabled: true,
|
|
51
|
+
docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/cross-missing-props.md",
|
|
52
|
+
execute(context) {
|
|
53
|
+
const graph = buildFileGraph(context);
|
|
54
|
+
return analyzeMissingProps(graph, context);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "cross-context-overuse",
|
|
59
|
+
name: "Context 过度使用",
|
|
60
|
+
description: "爷孙组件间只有单层嵌套时使用 Context 是不必要的,应使用 props 传递",
|
|
61
|
+
severity: "suggestion",
|
|
62
|
+
category: "architecture",
|
|
63
|
+
defaultEnabled: true,
|
|
64
|
+
docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/cross-context-overuse.md",
|
|
65
|
+
execute(context) {
|
|
66
|
+
const graph = buildFileGraph(context);
|
|
67
|
+
return analyzeContextOveruse(graph, context);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "cross-duplicate-code",
|
|
72
|
+
name: "兄弟组件存在重复代码",
|
|
73
|
+
description: "同级目录下的文件存在相似的函数或逻辑,建议提取公共模块",
|
|
74
|
+
severity: "suggestion",
|
|
75
|
+
category: "architecture",
|
|
76
|
+
defaultEnabled: true,
|
|
77
|
+
docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/cross-duplicate-code.md",
|
|
78
|
+
execute(context) {
|
|
79
|
+
const graph = buildFileGraph(context);
|
|
80
|
+
return analyzeDuplicateCode(graph, context);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "cross-extract-common",
|
|
85
|
+
name: "建议提取公共逻辑",
|
|
86
|
+
description: "多个兄弟文件使用相似的逻辑,建议提取到 hooks/utils",
|
|
87
|
+
severity: "suggestion",
|
|
88
|
+
category: "architecture",
|
|
89
|
+
defaultEnabled: true,
|
|
90
|
+
docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/cross-extract-common.md",
|
|
91
|
+
execute(context) {
|
|
92
|
+
const graph = buildFileGraph(context);
|
|
93
|
+
return analyzeExtractCommon(graph, context);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// 文件图构建
|
|
99
|
+
// ============================================================================
|
|
100
|
+
const FILE_GRAPH_CACHE_KEY = "__crossFileGraph";
|
|
101
|
+
/** 构建项目文件图(带 sharedCache 复用) */
|
|
102
|
+
function buildFileGraph(context) {
|
|
103
|
+
// v2.1.1: 优先从 sharedCache 复用文件图(同目录同轮扫描)
|
|
104
|
+
if (context.sharedCache) {
|
|
105
|
+
const cached = context.sharedCache.get(FILE_GRAPH_CACHE_KEY);
|
|
106
|
+
if (cached)
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
const graph = {
|
|
110
|
+
components: new Map(),
|
|
111
|
+
imports: new Map(),
|
|
112
|
+
componentIndex: new Map(),
|
|
113
|
+
};
|
|
114
|
+
// 获取项目中的源文件(限制为当前目录下的文件,避免全项目扫描太慢)
|
|
115
|
+
const currentDir = (0, node_path_1.dirname)(context.filePath);
|
|
116
|
+
const siblingFiles = getSiblingFiles(currentDir, context.filePath);
|
|
117
|
+
// 解析当前文件和兄弟文件
|
|
118
|
+
const filesToParse = [context.filePath, ...siblingFiles];
|
|
119
|
+
for (const filePath of filesToParse) {
|
|
120
|
+
try {
|
|
121
|
+
// 当前文件使用 context.source(避免文件不存在时读取失败)
|
|
122
|
+
let source;
|
|
123
|
+
if (filePath === context.filePath) {
|
|
124
|
+
source = context.source;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
source = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
128
|
+
}
|
|
129
|
+
const ext = (0, common_js_1.getFileExt)(filePath);
|
|
130
|
+
const ast = (0, ast_parser_js_1.parseAST)(source, { ext });
|
|
131
|
+
if (!ast)
|
|
132
|
+
continue;
|
|
133
|
+
// 提取组件信息
|
|
134
|
+
const components = extractComponents(filePath, ast, source);
|
|
135
|
+
graph.components.set(filePath, components);
|
|
136
|
+
// 建立组件索引
|
|
137
|
+
for (const comp of components) {
|
|
138
|
+
graph.componentIndex.set(comp.name, comp);
|
|
139
|
+
}
|
|
140
|
+
// 提取导入关系
|
|
141
|
+
const imports = (0, ast_parser_js_1.getImports)(ast);
|
|
142
|
+
const importedFiles = [];
|
|
143
|
+
for (const imp of imports) {
|
|
144
|
+
// 解析相对路径为绝对路径
|
|
145
|
+
if (imp.source.startsWith(".")) {
|
|
146
|
+
const resolved = (0, node_path_1.resolve)((0, node_path_1.dirname)(filePath), imp.source);
|
|
147
|
+
// 尝试添加扩展名
|
|
148
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
149
|
+
try {
|
|
150
|
+
const fullPath = resolved + ext;
|
|
151
|
+
(0, node_fs_1.readFileSync)(fullPath, "utf-8");
|
|
152
|
+
importedFiles.push(fullPath);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// 尝试 index 文件
|
|
157
|
+
try {
|
|
158
|
+
const indexPath = (0, node_path_1.resolve)(resolved, "index" + ext);
|
|
159
|
+
(0, node_fs_1.readFileSync)(indexPath, "utf-8");
|
|
160
|
+
importedFiles.push(indexPath);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// 继续尝试
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
graph.imports.set(filePath, importedFiles);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// 文件读取失败,跳过
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// v2.1.1: 存入 sharedCache,供同轮扫描的其他规则复用
|
|
177
|
+
if (context.sharedCache) {
|
|
178
|
+
context.sharedCache.set(FILE_GRAPH_CACHE_KEY, graph);
|
|
179
|
+
}
|
|
180
|
+
return graph;
|
|
181
|
+
}
|
|
182
|
+
/** 获取兄弟文件(同目录下的其他文件) */
|
|
183
|
+
function getSiblingFiles(dir, excludeFile) {
|
|
184
|
+
const { readdirSync, statSync } = require("node:fs");
|
|
185
|
+
const { resolve, extname } = require("node:path");
|
|
186
|
+
const siblings = [];
|
|
187
|
+
try {
|
|
188
|
+
const entries = readdirSync(dir);
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const fullPath = resolve(dir, entry);
|
|
191
|
+
const stat = statSync(fullPath);
|
|
192
|
+
if (!stat.isFile())
|
|
193
|
+
continue;
|
|
194
|
+
if (fullPath === excludeFile)
|
|
195
|
+
continue;
|
|
196
|
+
const ext = extname(fullPath).toLowerCase();
|
|
197
|
+
if ([".js", ".ts", ".jsx", ".tsx", ".vue"].includes(ext)) {
|
|
198
|
+
siblings.push(fullPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// 目录读取失败
|
|
204
|
+
}
|
|
205
|
+
// 也检查父目录的兄弟(堂兄弟文件)
|
|
206
|
+
const parentDir = (0, node_path_1.dirname)(dir);
|
|
207
|
+
if (parentDir !== dir) {
|
|
208
|
+
try {
|
|
209
|
+
const parentEntries = readdirSync(parentDir);
|
|
210
|
+
for (const entry of parentEntries) {
|
|
211
|
+
const fullPath = resolve(parentDir, entry);
|
|
212
|
+
const stat = statSync(fullPath);
|
|
213
|
+
if (stat.isDirectory() && fullPath !== dir) {
|
|
214
|
+
// 子目录下的文件
|
|
215
|
+
try {
|
|
216
|
+
const subEntries = readdirSync(fullPath);
|
|
217
|
+
for (const sub of subEntries) {
|
|
218
|
+
const subPath = resolve(fullPath, sub);
|
|
219
|
+
const subStat = statSync(subPath);
|
|
220
|
+
if (subStat.isFile()) {
|
|
221
|
+
const ext = extname(subPath).toLowerCase();
|
|
222
|
+
if ([".js", ".ts", ".jsx", ".tsx"].includes(ext)) {
|
|
223
|
+
siblings.push(subPath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// 忽略子目录错误
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// 忽略父目录错误
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 限制数量,避免扫描太多文件
|
|
239
|
+
return siblings.slice(0, 20);
|
|
240
|
+
}
|
|
241
|
+
/** 从 AST 提取组件信息 */
|
|
242
|
+
function extractComponents(filePath, ast, source) {
|
|
243
|
+
const components = [];
|
|
244
|
+
(0, traverse_1.default)(ast, {
|
|
245
|
+
// 函数组件: function Foo() {}
|
|
246
|
+
FunctionDeclaration(path) {
|
|
247
|
+
const name = path.node.id?.name;
|
|
248
|
+
if (!name || !isComponentName(name))
|
|
249
|
+
return;
|
|
250
|
+
const info = {
|
|
251
|
+
name,
|
|
252
|
+
filePath,
|
|
253
|
+
type: "function",
|
|
254
|
+
declaredProps: extractFunctionProps(path.node),
|
|
255
|
+
usedProps: [],
|
|
256
|
+
contextConsumers: [],
|
|
257
|
+
contextProviders: [],
|
|
258
|
+
functions: [],
|
|
259
|
+
imports: [],
|
|
260
|
+
exports: [],
|
|
261
|
+
usedComponents: [],
|
|
262
|
+
line: path.node.loc?.start?.line || 0,
|
|
263
|
+
column: path.node.loc?.start?.column || 0,
|
|
264
|
+
};
|
|
265
|
+
// 提取函数体中的 props 使用和 context 使用
|
|
266
|
+
extractBodyInfo(path.node.body, info);
|
|
267
|
+
components.push(info);
|
|
268
|
+
},
|
|
269
|
+
// 类组件: class Foo extends Component {}
|
|
270
|
+
ClassDeclaration(path) {
|
|
271
|
+
const name = path.node.id?.name;
|
|
272
|
+
if (!name || !isComponentName(name))
|
|
273
|
+
return;
|
|
274
|
+
const info = {
|
|
275
|
+
name,
|
|
276
|
+
filePath,
|
|
277
|
+
type: "class",
|
|
278
|
+
declaredProps: [],
|
|
279
|
+
usedProps: [],
|
|
280
|
+
contextConsumers: [],
|
|
281
|
+
contextProviders: [],
|
|
282
|
+
functions: [],
|
|
283
|
+
imports: [],
|
|
284
|
+
exports: [],
|
|
285
|
+
usedComponents: [],
|
|
286
|
+
line: path.node.loc?.start?.line || 0,
|
|
287
|
+
column: path.node.loc?.start?.column || 0,
|
|
288
|
+
};
|
|
289
|
+
// 提取类中的 render 方法和 props 使用
|
|
290
|
+
path.node.body.body.forEach((member) => {
|
|
291
|
+
if (member.type === "ClassMethod" && member.key?.name === "render") {
|
|
292
|
+
extractBodyInfo(member.body, info);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
components.push(info);
|
|
296
|
+
},
|
|
297
|
+
// 箭头函数组件: const Foo = () => {}
|
|
298
|
+
VariableDeclarator(path) {
|
|
299
|
+
const id = path.node.id;
|
|
300
|
+
if (id.type !== "Identifier")
|
|
301
|
+
return;
|
|
302
|
+
const name = id.name;
|
|
303
|
+
if (!isComponentName(name))
|
|
304
|
+
return;
|
|
305
|
+
const init = path.node.init;
|
|
306
|
+
if (!init || init.type !== "ArrowFunctionExpression")
|
|
307
|
+
return;
|
|
308
|
+
const info = {
|
|
309
|
+
name,
|
|
310
|
+
filePath,
|
|
311
|
+
type: "arrow-function",
|
|
312
|
+
declaredProps: extractFunctionProps(init),
|
|
313
|
+
usedProps: [],
|
|
314
|
+
contextConsumers: [],
|
|
315
|
+
contextProviders: [],
|
|
316
|
+
functions: [],
|
|
317
|
+
imports: [],
|
|
318
|
+
exports: [],
|
|
319
|
+
usedComponents: [],
|
|
320
|
+
line: id.loc?.start?.line || 0,
|
|
321
|
+
column: id.loc?.start?.column || 0,
|
|
322
|
+
};
|
|
323
|
+
if (init.body.type === "BlockStatement") {
|
|
324
|
+
extractBodyInfo(init.body, info);
|
|
325
|
+
}
|
|
326
|
+
components.push(info);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
return components;
|
|
330
|
+
}
|
|
331
|
+
/** 判断是否是组件名(PascalCase) */
|
|
332
|
+
function isComponentName(name) {
|
|
333
|
+
// 排除常见的非组件名
|
|
334
|
+
const nonComponents = [
|
|
335
|
+
"describe",
|
|
336
|
+
"it",
|
|
337
|
+
"test",
|
|
338
|
+
"beforeEach",
|
|
339
|
+
"afterEach",
|
|
340
|
+
"beforeAll",
|
|
341
|
+
"afterAll",
|
|
342
|
+
"expect",
|
|
343
|
+
"jest",
|
|
344
|
+
];
|
|
345
|
+
if (nonComponents.includes(name))
|
|
346
|
+
return false;
|
|
347
|
+
// PascalCase
|
|
348
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name) && /[a-z]/.test(name);
|
|
349
|
+
}
|
|
350
|
+
/** 提取函数参数中的 props */
|
|
351
|
+
function extractFunctionProps(node) {
|
|
352
|
+
const props = [];
|
|
353
|
+
if (!node.params || node.params.length === 0)
|
|
354
|
+
return props;
|
|
355
|
+
const firstParam = node.params[0];
|
|
356
|
+
// 解构: ({ foo, bar }) => {}
|
|
357
|
+
if (firstParam.type === "ObjectPattern") {
|
|
358
|
+
for (const prop of firstParam.properties) {
|
|
359
|
+
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
|
|
360
|
+
props.push({
|
|
361
|
+
name: prop.key.name,
|
|
362
|
+
optional: prop.optional || false,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
|
|
366
|
+
props.push({
|
|
367
|
+
name: `...${prop.argument.name}`,
|
|
368
|
+
optional: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// 单个 props 参数: (props) => {}
|
|
374
|
+
if (firstParam.type === "Identifier") {
|
|
375
|
+
props.push({
|
|
376
|
+
name: firstParam.name,
|
|
377
|
+
optional: firstParam.optional || false,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return props;
|
|
381
|
+
}
|
|
382
|
+
/** 提取函数体中的信息 */
|
|
383
|
+
function extractBodyInfo(body, info) {
|
|
384
|
+
if (!body)
|
|
385
|
+
return;
|
|
386
|
+
// 使用 traverse 遍历函数体
|
|
387
|
+
(0, traverse_1.default)(body, {
|
|
388
|
+
// props 使用
|
|
389
|
+
MemberExpression(path) {
|
|
390
|
+
const node = path.node;
|
|
391
|
+
if (node.object.type === "Identifier" && node.object.name === "props") {
|
|
392
|
+
if (node.property.type === "Identifier") {
|
|
393
|
+
info.usedProps.push(node.property.name);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
// 解构中的 props 使用
|
|
398
|
+
VariableDeclarator(path) {
|
|
399
|
+
const init = path.node.init;
|
|
400
|
+
if (init?.type === "MemberExpression" &&
|
|
401
|
+
init.object.type === "Identifier" &&
|
|
402
|
+
init.object.name === "props") {
|
|
403
|
+
const id = path.node.id;
|
|
404
|
+
if (id.type === "Identifier") {
|
|
405
|
+
info.usedProps.push(id.name);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
// useContext 调用
|
|
410
|
+
CallExpression(path) {
|
|
411
|
+
const callee = path.node.callee;
|
|
412
|
+
if (callee.type === "Identifier" && callee.name === "useContext") {
|
|
413
|
+
const firstArg = path.node.arguments[0];
|
|
414
|
+
if (firstArg?.type === "Identifier") {
|
|
415
|
+
info.contextConsumers.push(firstArg.name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Context.Provider
|
|
419
|
+
if (callee.type === "MemberExpression" &&
|
|
420
|
+
callee.property.type === "Identifier" &&
|
|
421
|
+
callee.property.name === "Provider") {
|
|
422
|
+
const obj = callee.object;
|
|
423
|
+
if (obj.type === "Identifier") {
|
|
424
|
+
info.contextProviders.push(obj.name);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
// JSX 中使用的外部组件
|
|
429
|
+
JSXOpeningElement(path) {
|
|
430
|
+
const tagName = (0, common_js_1.getJSXTagName)(path.node.name);
|
|
431
|
+
if (!tagName)
|
|
432
|
+
return;
|
|
433
|
+
// 检测 Context.Provider: <UserContext.Provider>
|
|
434
|
+
if (tagName.endsWith(".Provider")) {
|
|
435
|
+
const ctxName = tagName.split(".")[0];
|
|
436
|
+
if (ctxName) {
|
|
437
|
+
info.contextProviders.push(ctxName);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!isComponentName(tagName))
|
|
442
|
+
return; // 只关心组件,不关心 HTML 标签
|
|
443
|
+
const usedProps = [];
|
|
444
|
+
for (const attr of path.node.attributes) {
|
|
445
|
+
if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
|
|
446
|
+
usedProps.push(attr.name.name);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
info.usedComponents.push({
|
|
450
|
+
name: tagName,
|
|
451
|
+
props: usedProps,
|
|
452
|
+
line: path.node.loc?.start?.line || 0,
|
|
453
|
+
column: path.node.loc?.start?.column || 0,
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
// 函数定义
|
|
457
|
+
FunctionDeclaration(innerPath) {
|
|
458
|
+
const name = innerPath.node.id?.name;
|
|
459
|
+
if (name) {
|
|
460
|
+
info.functions.push({
|
|
461
|
+
name,
|
|
462
|
+
body: "", // 简化,不存储完整函数体
|
|
463
|
+
params: innerPath.node.params.map((p) => p.name || ""),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
}, body);
|
|
468
|
+
}
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// 分析规则
|
|
471
|
+
// ============================================================================
|
|
472
|
+
/** 分析未使用的 props */
|
|
473
|
+
function analyzeUnusedProps(graph, context) {
|
|
474
|
+
const issues = [];
|
|
475
|
+
// 遍历所有组件
|
|
476
|
+
for (const [, components] of graph.components) {
|
|
477
|
+
for (const parent of components) {
|
|
478
|
+
// 检查父组件中使用的子组件
|
|
479
|
+
for (const childUsage of parent.usedComponents) {
|
|
480
|
+
const child = graph.componentIndex.get(childUsage.name);
|
|
481
|
+
if (!child)
|
|
482
|
+
continue; // 外部库组件,跳过
|
|
483
|
+
// 检查父传递的 props 中哪些是子组件未使用的
|
|
484
|
+
const childUsedProps = new Set(child.usedProps);
|
|
485
|
+
// 加上子组件 declaredProps 中的解构参数
|
|
486
|
+
for (const dp of child.declaredProps) {
|
|
487
|
+
childUsedProps.add(dp.name);
|
|
488
|
+
}
|
|
489
|
+
const unusedProps = childUsage.props.filter((p) => !childUsedProps.has(p) &&
|
|
490
|
+
p !== "key" &&
|
|
491
|
+
p !== "ref" &&
|
|
492
|
+
!p.startsWith("on") && // 事件处理器可能在子组件中通过 props 解构
|
|
493
|
+
!p.startsWith("data-") &&
|
|
494
|
+
!p.startsWith("aria-"));
|
|
495
|
+
if (unusedProps.length > 0) {
|
|
496
|
+
issues.push({
|
|
497
|
+
ruleId: "cross-unused-props",
|
|
498
|
+
title: `父组件 "${parent.name}" 向 "${child.name}" 传递了未使用的 props`,
|
|
499
|
+
description: `子组件 "${child.name}" 未使用以下 props: ${unusedProps.join(", ")}。建议移除父组件中的传递,或检查是否是拼写错误`,
|
|
500
|
+
severity: "warning",
|
|
501
|
+
file: context.filePath,
|
|
502
|
+
line: parent.line,
|
|
503
|
+
column: parent.column,
|
|
504
|
+
source: `<${child.name} ${unusedProps.map((p) => `${p}=...`).join(" ")} />`,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return issues;
|
|
511
|
+
}
|
|
512
|
+
/** 分析缺失的 props */
|
|
513
|
+
function analyzeMissingProps(graph, context) {
|
|
514
|
+
const issues = [];
|
|
515
|
+
for (const [, components] of graph.components) {
|
|
516
|
+
for (const parent of components) {
|
|
517
|
+
for (const childUsage of parent.usedComponents) {
|
|
518
|
+
const child = graph.componentIndex.get(childUsage.name);
|
|
519
|
+
if (!child)
|
|
520
|
+
continue;
|
|
521
|
+
// 检查子组件声明的必传 props 是否都被传递了
|
|
522
|
+
const passedProps = new Set(childUsage.props);
|
|
523
|
+
const missingProps = child.declaredProps.filter((dp) => !dp.optional &&
|
|
524
|
+
!passedProps.has(dp.name) &&
|
|
525
|
+
dp.name !== "children" &&
|
|
526
|
+
!dp.name.startsWith("..."));
|
|
527
|
+
if (missingProps.length > 0) {
|
|
528
|
+
issues.push({
|
|
529
|
+
ruleId: "cross-missing-props",
|
|
530
|
+
title: `"${parent.name}" 未传递 "${child.name}" 的必传 props`,
|
|
531
|
+
description: `子组件 "${child.name}" 需要以下必传 props: ${missingProps.map((p) => p.name).join(", ")}`,
|
|
532
|
+
severity: "warning",
|
|
533
|
+
file: context.filePath,
|
|
534
|
+
line: parent.line,
|
|
535
|
+
column: parent.column,
|
|
536
|
+
source: `<${child.name} />`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return issues;
|
|
543
|
+
}
|
|
544
|
+
/** 分析 Context 过度使用 */
|
|
545
|
+
function analyzeContextOveruse(graph, context) {
|
|
546
|
+
const issues = [];
|
|
547
|
+
for (const [, components] of graph.components) {
|
|
548
|
+
for (const comp of components) {
|
|
549
|
+
// 检测:如果组件只消费了一个 context,且它的父组件(在同一文件中)提供了这个 context
|
|
550
|
+
// 说明只有单层嵌套,用 props 更合适
|
|
551
|
+
if (comp.contextConsumers.length === 0)
|
|
552
|
+
continue;
|
|
553
|
+
for (const ctxName of comp.contextConsumers) {
|
|
554
|
+
// 检查是否在同文件中有 Provider
|
|
555
|
+
const sameFileComponents = graph.components.get(comp.filePath) || [];
|
|
556
|
+
const hasProviderInSameFile = sameFileComponents.some((c) => c.contextProviders.includes(`${ctxName}.Provider`) || c.contextProviders.includes(ctxName));
|
|
557
|
+
if (hasProviderInSameFile) {
|
|
558
|
+
issues.push({
|
|
559
|
+
ruleId: "cross-context-overuse",
|
|
560
|
+
title: `"${comp.name}" 的 Context 使用可改为 props 传递`,
|
|
561
|
+
description: `组件 "${comp.name}" 在同文件中消费了 "${ctxName}" Context。由于只有单层嵌套,建议直接用 props 传递数据,减少 Context 的复杂度`,
|
|
562
|
+
severity: "suggestion",
|
|
563
|
+
file: context.filePath,
|
|
564
|
+
line: comp.line,
|
|
565
|
+
column: comp.column,
|
|
566
|
+
source: `useContext(${ctxName})`,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return issues;
|
|
573
|
+
}
|
|
574
|
+
/** 分析重复代码 */
|
|
575
|
+
function analyzeDuplicateCode(graph, context) {
|
|
576
|
+
const issues = [];
|
|
577
|
+
// 获取当前文件所在目录的兄弟组件
|
|
578
|
+
const currentDir = (0, node_path_1.dirname)(context.filePath);
|
|
579
|
+
const currentComps = graph.components.get(context.filePath) || [];
|
|
580
|
+
const siblingComps = [];
|
|
581
|
+
for (const [filePath, comps] of graph.components) {
|
|
582
|
+
if (filePath === context.filePath)
|
|
583
|
+
continue;
|
|
584
|
+
const fileDir = (0, node_path_1.dirname)(filePath);
|
|
585
|
+
// 兄弟文件(同目录)
|
|
586
|
+
if (fileDir === currentDir) {
|
|
587
|
+
siblingComps.push(...comps);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// 检测函数签名相似的组件(跨文件 + 同文件内不同组件)
|
|
591
|
+
for (let i = 0; i < currentComps.length; i++) {
|
|
592
|
+
const current = currentComps[i];
|
|
593
|
+
// 同文件内的其他组件
|
|
594
|
+
for (let j = i + 1; j < currentComps.length; j++) {
|
|
595
|
+
checkSimilarity(current, currentComps[j], issues, context);
|
|
596
|
+
}
|
|
597
|
+
// 兄弟文件中的组件
|
|
598
|
+
for (const sibling of siblingComps) {
|
|
599
|
+
checkSimilarity(current, sibling, issues, context);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return issues;
|
|
603
|
+
}
|
|
604
|
+
/** 检查两个组件是否相似 */
|
|
605
|
+
function checkSimilarity(current, sibling, issues, context) {
|
|
606
|
+
if (current.name === sibling.name)
|
|
607
|
+
return; // 跳过同名
|
|
608
|
+
// 检测 props 结构是否相似(>50% 相同)
|
|
609
|
+
const currentPropNames = new Set(current.declaredProps.map((p) => p.name));
|
|
610
|
+
const siblingPropNames = new Set(sibling.declaredProps.map((p) => p.name));
|
|
611
|
+
const intersection = [...currentPropNames].filter((p) => siblingPropNames.has(p));
|
|
612
|
+
const union = new Set([...currentPropNames, ...siblingPropNames]);
|
|
613
|
+
if (union.size > 0 && intersection.length / union.size >= 0.5 && intersection.length >= 2) {
|
|
614
|
+
issues.push({
|
|
615
|
+
ruleId: "cross-duplicate-code",
|
|
616
|
+
title: `"${current.name}" 与 "${sibling.name}" 有相似的 props 结构`,
|
|
617
|
+
description: `两个组件有 ${intersection.length} 个相同的 props: ${intersection.join(", ")}。如果逻辑也相似,建议提取公共的 Base 组件或 HOC`,
|
|
618
|
+
severity: "suggestion",
|
|
619
|
+
file: context.filePath,
|
|
620
|
+
line: current.line,
|
|
621
|
+
column: current.column,
|
|
622
|
+
source: `${current.name} / ${sibling.name}`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// 检测函数体中是否有相同的事件处理函数名
|
|
626
|
+
const currentFns = new Set(current.functions.map((f) => f.name));
|
|
627
|
+
const siblingFns = new Set(sibling.functions.map((f) => f.name));
|
|
628
|
+
const sameFns = [...currentFns].filter((f) => siblingFns.has(f) && f.startsWith("handle") // 事件处理函数
|
|
629
|
+
);
|
|
630
|
+
if (sameFns.length >= 2) {
|
|
631
|
+
issues.push({
|
|
632
|
+
ruleId: "cross-duplicate-code",
|
|
633
|
+
title: `"${current.name}" 与 "${sibling.name}" 有重复的事件处理逻辑`,
|
|
634
|
+
description: `发现 ${sameFns.length} 个同名事件处理函数: ${sameFns.join(", ")}。建议提取到公共 hooks 中`,
|
|
635
|
+
severity: "suggestion",
|
|
636
|
+
file: context.filePath,
|
|
637
|
+
line: current.line,
|
|
638
|
+
column: current.column,
|
|
639
|
+
source: `${current.name} / ${sibling.name}`,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** 分析公共逻辑提取建议 */
|
|
644
|
+
function analyzeExtractCommon(graph, context) {
|
|
645
|
+
const issues = [];
|
|
646
|
+
// 检测当前文件的函数中是否有可以提取的公共逻辑
|
|
647
|
+
const currentComps = graph.components.get(context.filePath) || [];
|
|
648
|
+
for (const comp of currentComps) {
|
|
649
|
+
// 检测组件中是否有独立的工具函数(不依赖组件状态)
|
|
650
|
+
const utilityFns = comp.functions.filter((fn) => {
|
|
651
|
+
// 不是事件处理函数
|
|
652
|
+
if (fn.name.startsWith("handle") || fn.name.startsWith("on"))
|
|
653
|
+
return false;
|
|
654
|
+
// 不是生命周期方法
|
|
655
|
+
const lifecycle = [
|
|
656
|
+
"componentDidMount",
|
|
657
|
+
"componentWillUnmount",
|
|
658
|
+
"componentDidUpdate",
|
|
659
|
+
"useEffect",
|
|
660
|
+
"useState",
|
|
661
|
+
"useCallback",
|
|
662
|
+
"useMemo",
|
|
663
|
+
];
|
|
664
|
+
if (lifecycle.includes(fn.name))
|
|
665
|
+
return false;
|
|
666
|
+
// 参数列表简单(纯函数特征)
|
|
667
|
+
return fn.params.length > 0 && fn.params.length <= 3;
|
|
668
|
+
});
|
|
669
|
+
if (utilityFns.length >= 2) {
|
|
670
|
+
issues.push({
|
|
671
|
+
ruleId: "cross-extract-common",
|
|
672
|
+
title: `"${comp.name}" 中有 ${utilityFns.length} 个可提取的工具函数`,
|
|
673
|
+
description: `组件 "${comp.name}" 包含多个不依赖组件状态的工具函数(${utilityFns.map((f) => f.name).join(", ")})。建议提取到 utils/ 目录下的公共模块`,
|
|
674
|
+
severity: "suggestion",
|
|
675
|
+
file: context.filePath,
|
|
676
|
+
line: comp.line,
|
|
677
|
+
column: comp.column,
|
|
678
|
+
source: utilityFns.map((f) => f.name).join(", "),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return issues;
|
|
683
|
+
}
|
|
684
|
+
//# sourceMappingURL=cross-file-scanner.js.map
|