jarvis-ai-assistant 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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.
Files changed (41) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +114 -6
  3. jarvis/jarvis_agent/agent_manager.py +3 -0
  4. jarvis/jarvis_agent/jarvis.py +45 -9
  5. jarvis/jarvis_agent/run_loop.py +6 -1
  6. jarvis/jarvis_agent/task_planner.py +219 -0
  7. jarvis/jarvis_c2rust/__init__.py +13 -0
  8. jarvis/jarvis_c2rust/cli.py +405 -0
  9. jarvis/jarvis_c2rust/collector.py +209 -0
  10. jarvis/jarvis_c2rust/library_replacer.py +933 -0
  11. jarvis/jarvis_c2rust/llm_module_agent.py +1265 -0
  12. jarvis/jarvis_c2rust/scanner.py +1671 -0
  13. jarvis/jarvis_c2rust/transpiler.py +1236 -0
  14. jarvis/jarvis_code_agent/code_agent.py +151 -18
  15. jarvis/jarvis_data/config_schema.json +13 -3
  16. jarvis/jarvis_sec/README.md +180 -0
  17. jarvis/jarvis_sec/__init__.py +674 -0
  18. jarvis/jarvis_sec/checkers/__init__.py +33 -0
  19. jarvis/jarvis_sec/checkers/c_checker.py +1269 -0
  20. jarvis/jarvis_sec/checkers/rust_checker.py +367 -0
  21. jarvis/jarvis_sec/cli.py +110 -0
  22. jarvis/jarvis_sec/prompts.py +324 -0
  23. jarvis/jarvis_sec/report.py +260 -0
  24. jarvis/jarvis_sec/types.py +20 -0
  25. jarvis/jarvis_sec/workflow.py +513 -0
  26. jarvis/jarvis_tools/cli/main.py +1 -0
  27. jarvis/jarvis_tools/execute_script.py +1 -1
  28. jarvis/jarvis_tools/read_code.py +11 -1
  29. jarvis/jarvis_tools/read_symbols.py +129 -0
  30. jarvis/jarvis_tools/registry.py +9 -1
  31. jarvis/jarvis_tools/sub_agent.py +4 -3
  32. jarvis/jarvis_tools/sub_code_agent.py +3 -3
  33. jarvis/jarvis_utils/config.py +28 -6
  34. jarvis/jarvis_utils/git_utils.py +39 -0
  35. jarvis/jarvis_utils/utils.py +150 -7
  36. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/METADATA +13 -1
  37. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/RECORD +41 -22
  38. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/entry_points.txt +4 -0
  39. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/WHEEL +0 -0
  40. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/licenses/LICENSE +0 -0
  41. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,405 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ C2Rust 独立命令行入口。
4
+
5
+ 提供分组式 CLI,将扫描能力作为子命令 scan 暴露:
6
+ - jarvis-c2rust scan --root <path> [--dot ...] [--only-dot] [--subgraphs-dir ...] [--only-subgraphs] [--png]
7
+
8
+ 实现策略:
9
+ - 复用 scanner.cli 的核心逻辑,避免重复代码。
10
+ - 使用 Typer 分组式结构,便于后续扩展更多子命令(如 analyze/export 等)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Optional, List
17
+
18
+ import typer
19
+ from jarvis.jarvis_c2rust.scanner import run_scan as _run_scan
20
+ from jarvis.jarvis_c2rust.scanner import (
21
+ compute_translation_order_jsonl as _compute_order,
22
+ )
23
+ from jarvis.jarvis_c2rust.library_replacer import (
24
+ apply_library_replacement as _apply_library_replacement,
25
+ )
26
+ from jarvis.jarvis_utils.utils import init_env
27
+ from jarvis.jarvis_c2rust.llm_module_agent import (
28
+ execute_llm_plan as _execute_llm_plan,
29
+ entries_to_yaml as _entries_to_yaml,
30
+ )
31
+
32
+ app = typer.Typer(help="C2Rust 命令行工具")
33
+
34
+ # 显式定义根回调,确保为命令组而非单函数入口
35
+ @app.callback()
36
+ def _root():
37
+ """
38
+ C2Rust 命令行工具
39
+ """
40
+ # 不做任何处理,仅作为命令组的占位,使 'scan' 作为子命令出现
41
+ init_env("欢迎使用 Jarvis C2Rust 工具")
42
+ pass
43
+
44
+
45
+ @app.command("scan")
46
+ def scan(
47
+ dot: Optional[Path] = typer.Option(
48
+ None,
49
+ "--dot",
50
+ help="扫描后将引用依赖图写入 DOT 文件(或与 --only-dot 一起使用)",
51
+ ),
52
+ only_dot: bool = typer.Option(
53
+ False,
54
+ "--only-dot",
55
+ help="不重新扫描。读取现有数据 (JSONL) 并仅生成 DOT(需要 --dot)",
56
+ ),
57
+ subgraphs_dir: Optional[Path] = typer.Option(
58
+ None,
59
+ "--subgraphs-dir",
60
+ help="用于写入每个根函数引用子图 DOT 文件的目录(每个根函数一个文件)",
61
+ ),
62
+ only_subgraphs: bool = typer.Option(
63
+ False,
64
+ "--only-subgraphs",
65
+ help="不重新扫描。仅生成每个根函数的引用子图 DOT 文件(需要 --subgraphs-dir)",
66
+ ),
67
+ ) -> None:
68
+ """
69
+ 进行 C/C++ 函数扫描并生成引用关系 DOT 图;PNG 渲染默认启用(无需参数)。
70
+ """
71
+ _run_scan(
72
+ dot=dot,
73
+ only_dot=only_dot,
74
+ subgraphs_dir=subgraphs_dir,
75
+ only_subgraphs=only_subgraphs,
76
+ png=True,
77
+ )
78
+
79
+ @app.command("prepare")
80
+ def prepare(
81
+ llm_group: Optional[str] = typer.Option(
82
+ None, "-g", "--llm-group", help="指定用于规划的 LLM 模型组(仅影响本次运行)"
83
+ ),
84
+ ) -> None:
85
+ """
86
+ 使用 LLM Agent 基于根函数子图规划 Rust crate 模块结构并直接应用到磁盘。
87
+ 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl)
88
+ 默认使用当前目录作为项目根,并从 <root>/.jarvis/c2rust/symbols.jsonl 读取数据
89
+ """
90
+ try:
91
+ _execute_llm_plan(apply=True, llm_group=llm_group)
92
+ except Exception as e:
93
+ typer.secho(f"[c2rust-llm-planner] 错误: {e}", fg=typer.colors.RED, err=True)
94
+ raise typer.Exit(code=1)
95
+
96
+
97
+ @app.command("transpile")
98
+ def transpile(
99
+ llm_group: Optional[str] = typer.Option(
100
+ None, "-g", "--llm-group", help="指定用于翻译的 LLM 模型组"
101
+ ),
102
+ only: Optional[str] = typer.Option(
103
+ None, "--only", help="仅翻译指定的函数(名称或限定名称),以逗号分隔"
104
+ ),
105
+ ) -> None:
106
+ """
107
+ 使用转译器按扫描顺序逐个函数转译并构建修复。
108
+ 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl 与 translation_order.jsonl)
109
+ 默认使用当前目录作为项目根,并从 <root>/.jarvis/c2rust/symbols.jsonl 读取数据。
110
+ 未指定目标 crate 时,使用默认 <cwd>/<cwd.name>_rs。
111
+ """
112
+ try:
113
+ # Lazy import to avoid hard dependency if not used
114
+ from jarvis.jarvis_c2rust.transpiler import run_transpile as _run_transpile
115
+ only_list = [s.strip() for s in str(only).split(",") if s.strip()] if only else None
116
+ _run_transpile(
117
+ project_root=Path("."),
118
+ crate_dir=None,
119
+ llm_group=llm_group,
120
+ only=only_list,
121
+ )
122
+ except Exception as e:
123
+ typer.secho(f"[c2rust-transpiler] 错误: {e}", fg=typer.colors.RED, err=True)
124
+ raise typer.Exit(code=1)
125
+
126
+
127
+ @app.command("lib-replace")
128
+ def lib_replace(
129
+ llm_group: Optional[str] = typer.Option(
130
+ None, "-g", "--llm-group", help="用于评估的 LLM 模型组"
131
+ ),
132
+ root_list_file: Optional[Path] = typer.Option(
133
+ None, "--root-list-file", help="根列表文件:按行列出要参与评估的根符号名称或限定名(忽略空行与以#开头的注释)"
134
+ ),
135
+ root_list_syms: Optional[str] = typer.Option(
136
+ None, "--root-list-syms", help="根列表内联:以逗号分隔的符号名称或限定名(仅评估这些根)"
137
+ ),
138
+ disabled_libs: Optional[str] = typer.Option(
139
+ None, "--disabled-libs", help="禁用库列表:逗号分隔的库名(评估时禁止使用这些库)"
140
+ ),
141
+ ) -> None:
142
+ """
143
+ Root-list 评估模式(必须走 LLM 评估):
144
+ - 必须提供根列表(--root-list-file 或 --root-list-syms,至少一种)
145
+ - 仅对根列表中的符号作为评估根执行 LLM 子树评估
146
+ - 若可替代:替换该根的 ref 为库占位,并剪除其所有子孙函数(根本身保留)
147
+ - 需先执行: jarvis-c2rust scan 以生成数据文件(symbols.jsonl)
148
+ - 默认库: std(仅用于对后续流程保持一致的默认上下文)
149
+ - 可选:--disabled-libs 指定评估时禁止使用的库列表(逗号分隔)
150
+ """
151
+ try:
152
+ data_dir = Path(".") / ".jarvis" / "c2rust"
153
+ curated_symbols = data_dir / "symbols.jsonl"
154
+ raw_symbols = data_dir / "symbols_raw.jsonl"
155
+ if not curated_symbols.exists() and not raw_symbols.exists():
156
+ typer.secho("[c2rust-lib-replace] 未找到符号数据(symbols.jsonl 或 symbols_raw.jsonl),正在执行扫描以生成数据...", fg=typer.colors.YELLOW)
157
+ _run_scan(dot=None, only_dot=False, subgraphs_dir=None, only_subgraphs=False, png=False)
158
+ if not curated_symbols.exists() and not raw_symbols.exists():
159
+ raise FileNotFoundError(f"未找到符号数据: {curated_symbols} 或 {raw_symbols}")
160
+
161
+ # 使用默认库: std
162
+ library = "std"
163
+
164
+ # 读取根列表(必填,至少提供一种来源)
165
+ root_names: List[str] = []
166
+ # 文件来源
167
+ if root_list_file is not None:
168
+ try:
169
+ txt = root_list_file.read_text(encoding="utf-8")
170
+ root_names.extend([ln.strip() for ln in txt.splitlines() if ln.strip() and not ln.strip().startswith("#")])
171
+ except Exception as _e:
172
+ typer.secho(f"[c2rust-lib-replace] 读取根列表失败: {root_list_file}: {_e}", fg=typer.colors.RED, err=True)
173
+ raise typer.Exit(code=1)
174
+ # 内联来源
175
+ if isinstance(root_list_syms, str) and root_list_syms.strip():
176
+ parts = [s.strip() for s in root_list_syms.replace("\n", ",").split(",") if s.strip()]
177
+ root_names.extend(parts)
178
+ # 去重
179
+ try:
180
+ root_names = list(dict.fromkeys(root_names))
181
+ except Exception:
182
+ root_names = sorted(list(set(root_names)))
183
+ if not root_names:
184
+ typer.secho("[c2rust-lib-replace] 错误:必须提供根列表(--root-list-file 或 --root-list-syms)。", fg=typer.colors.RED, err=True)
185
+ raise typer.Exit(code=2)
186
+
187
+ # 解析禁用库列表(可选)
188
+ disabled_list: Optional[List[str]] = None
189
+ if isinstance(disabled_libs, str) and disabled_libs.strip():
190
+ disabled_list = [s.strip() for s in disabled_libs.replace("\n", ",").split(",") if s.strip()]
191
+ if disabled_list:
192
+ typer.secho(f"[c2rust-lib-replace] 禁用库: {', '.join(disabled_list)}", fg=typer.colors.YELLOW)
193
+
194
+ # 必须走 LLM 评估:仅评估提供的根(candidates),不启用强制剪枝模式
195
+ ret = _apply_library_replacement(
196
+ db_path=Path("."),
197
+ library_name=library,
198
+ llm_group=llm_group,
199
+ candidates=root_names, # 仅评估这些根
200
+ out_symbols_path=None,
201
+ out_mapping_path=None,
202
+ max_funcs=None,
203
+ disabled_libraries=disabled_list,
204
+ )
205
+ # 输出简要结果摘要(底层已写出新的符号表与可选转译顺序)
206
+ try:
207
+ order_msg = f"\n[c2rust-lib-replace] 转译顺序: {ret['order']}" if 'order' in ret else ""
208
+ typer.secho(
209
+ f"[c2rust-lib-replace] 替代映射: {ret['mapping']}\n"
210
+ f"[c2rust-lib-replace] 新符号表: {ret['symbols']}"
211
+ + order_msg,
212
+ fg=typer.colors.GREEN,
213
+ )
214
+ except Exception as _e:
215
+ typer.secho(f"[c2rust-lib-replace] 结果输出时发生非致命错误: {_e}", fg=typer.colors.YELLOW, err=True)
216
+ except Exception as e:
217
+ typer.secho(f"[c2rust-lib-replace] 错误: {e}", fg=typer.colors.RED, err=True)
218
+ raise typer.Exit(code=1)
219
+
220
+
221
+
222
+ @app.command("collect")
223
+ def collect(
224
+ files: List[Path] = typer.Argument(..., help="一个或多个 C/C++ 头文件路径(.h/.hh/.hpp/.hxx)"),
225
+ out: Path = typer.Option(..., "-o", "--out", help="输出文件路径(写入唯一函数名,每行一个)"),
226
+ ) -> None:
227
+ """
228
+ 收集指定头文件中的函数名(使用 libclang 解析),并写入指定输出文件(每行一个)。
229
+ 示例:
230
+ jarvis-c2rust collect a.h b.hpp -o funcs.txt
231
+ 说明:
232
+ 非头文件会被跳过(仅支持 .h/.hh/.hpp/.hxx)。
233
+ """
234
+ try:
235
+ from jarvis.jarvis_c2rust.collector import collect_function_names as _collect_fn_names
236
+ _collect_fn_names(files=files, out_path=out)
237
+ typer.secho(f"[c2rust-collect] 函数名已写入: {out}", fg=typer.colors.GREEN)
238
+ except Exception as e:
239
+ typer.secho(f"[c2rust-collect] 错误: {e}", fg=typer.colors.RED, err=True)
240
+ raise typer.Exit(code=1)
241
+
242
+ @app.command("run")
243
+ def run(
244
+ files: Optional[List[Path]] = typer.Option(
245
+ None,
246
+ "--files",
247
+ help="用于 collect 阶段的头文件列表(.h/.hh/.hpp/.hxx);提供则先执行 collect",
248
+ ),
249
+ out: Optional[Path] = typer.Option(
250
+ None,
251
+ "-o",
252
+ "--out",
253
+ help="collect 输出函数名文件;若未提供且指定 --files 则默认为 <root>/.jarvis/c2rust/roots.txt",
254
+ ),
255
+ llm_group: Optional[str] = typer.Option(
256
+ None,
257
+ "-g",
258
+ "--llm-group",
259
+ help="用于 LLM 相关阶段(lib-replace/prepare/transpile)的模型组",
260
+ ),
261
+ root_list_file: Optional[Path] = typer.Option(
262
+ None,
263
+ "--root-list-file",
264
+ help="兼容占位:run 会使用 collect 的 --out 作为 lib-replace 的输入;当提供 --files 时本参数将被忽略;未提供 --files 时,本命令要求使用 --root-list-syms",
265
+ ),
266
+ root_list_syms: Optional[str] = typer.Option(
267
+ None,
268
+ "--root-list-syms",
269
+ help="lib-replace 的根列表内联(逗号分隔)。未提供 --files 时该参数为必填",
270
+ ),
271
+ disabled_libs: Optional[str] = typer.Option(
272
+ None,
273
+ "--disabled-libs",
274
+ help="lib-replace 禁用库列表(逗号分隔)",
275
+ ),
276
+ ) -> None:
277
+ """
278
+ 依次执行流水线:collect -> scan -> lib-replace -> prepare -> transpile
279
+
280
+ 约束:
281
+ - collect 的输出文件就是 lib-replace 的输入文件;
282
+ 当提供 --files 时,lib-replace 将固定读取 --out(或默认值)作为根列表文件,忽略 --root-list-file
283
+ - 未提供 --files 时,必须通过 --root-list-syms 提供根列表
284
+ - scan 始终执行以确保数据完整
285
+ - prepare/transpile 会使用 --llm-group 指定的模型组
286
+ """
287
+ try:
288
+ data_dir = Path(".") / ".jarvis" / "c2rust"
289
+ default_roots = data_dir / "roots.txt"
290
+
291
+ # Step 1: collect(可选)
292
+ roots_path: Optional[Path] = None
293
+ if files:
294
+ try:
295
+ if out is None:
296
+ out = default_roots
297
+ out.parent.mkdir(parents=True, exist_ok=True)
298
+ from jarvis.jarvis_c2rust.collector import (
299
+ collect_function_names as _collect_fn_names,
300
+ )
301
+ _collect_fn_names(files=files, out_path=out)
302
+ typer.secho(f"[c2rust-run] collect: 函数名已写入: {out}", fg=typer.colors.GREEN)
303
+ roots_path = out
304
+ except Exception as _e:
305
+ typer.secho(f"[c2rust-run] collect: 错误: {_e}", fg=typer.colors.RED, err=True)
306
+ raise
307
+
308
+ # Step 2: scan(始终执行)
309
+ typer.secho("[c2rust-run] scan: 开始", fg=typer.colors.BLUE)
310
+ _run_scan(dot=None, only_dot=False, subgraphs_dir=None, only_subgraphs=False, png=False)
311
+ typer.secho("[c2rust-run] scan: 完成", fg=typer.colors.GREEN)
312
+
313
+ # Step 3: lib-replace(强制执行,依据约束获取根列表)
314
+ root_names: List[str] = []
315
+
316
+ if files:
317
+ # 约束:collect 的输出文件作为唯一文件来源
318
+ if not roots_path or not roots_path.exists():
319
+ typer.secho("[c2rust-run] lib-replace: 未找到 collect 输出文件,无法继续", fg=typer.colors.RED, err=True)
320
+ raise typer.Exit(code=2)
321
+ try:
322
+ txt = roots_path.read_text(encoding="utf-8")
323
+ root_names.extend([ln.strip() for ln in txt.splitlines() if ln.strip() and not ln.strip().startswith("#")])
324
+ typer.secho(f"[c2rust-run] lib-replace: 使用根列表文件: {roots_path}", fg=typer.colors.BLUE)
325
+ except Exception as _e:
326
+ typer.secho(f"[c2rust-run] lib-replace: 读取根列表失败: {roots_path}: {_e}", fg=typer.colors.RED, err=True)
327
+ raise
328
+ # 兼容参数提示
329
+ if root_list_file is not None:
330
+ typer.secho("[c2rust-run] 提示: --root-list-file 已被忽略(run 会固定使用 collect 的 --out 作为输入)", fg=typer.colors.YELLOW)
331
+ else:
332
+ # 约束:未传递 files 必须提供 --root-list-syms
333
+ if not (isinstance(root_list_syms, str) and root_list_syms.strip()):
334
+ typer.secho("[c2rust-run] 错误:未提供 --files 时,必须通过 --root-list-syms 指定根列表(逗号分隔)", fg=typer.colors.RED, err=True)
335
+ raise typer.Exit(code=2)
336
+ parts = [s.strip() for s in root_list_syms.replace("\n", ",").split(",") if s.strip()]
337
+ root_names.extend(parts)
338
+
339
+ # 去重并校验非空
340
+ try:
341
+ root_names = list(dict.fromkeys(root_names))
342
+ except Exception:
343
+ root_names = sorted(list(set(root_names)))
344
+ if not root_names:
345
+ typer.secho("[c2rust-run] lib-replace: 根列表为空,无法继续", fg=typer.colors.RED, err=True)
346
+ raise typer.Exit(code=2)
347
+
348
+ # 可选禁用库列表
349
+ disabled_list: Optional[List[str]] = None
350
+ if isinstance(disabled_libs, str) and disabled_libs.strip():
351
+ disabled_list = [s.strip() for s in disabled_libs.replace("\n", ",").split(",") if s.strip()]
352
+ if disabled_list:
353
+ typer.secho(f"[c2rust-run] lib-replace: 禁用库: {', '.join(disabled_list)}", fg=typer.colors.YELLOW)
354
+
355
+ # 执行 lib-replace(默认库 std)
356
+ library = "std"
357
+ typer.secho(f"[c2rust-run] lib-replace: 开始(库: {library},根数: {len(root_names)})", fg=typer.colors.BLUE)
358
+ ret = _apply_library_replacement(
359
+ db_path=Path("."),
360
+ library_name=library,
361
+ llm_group=llm_group,
362
+ candidates=root_names,
363
+ out_symbols_path=None,
364
+ out_mapping_path=None,
365
+ max_funcs=None,
366
+ disabled_libraries=disabled_list,
367
+ )
368
+ try:
369
+ order_msg = f"\n[c2rust-run] lib-replace: 转译顺序: {ret['order']}" if 'order' in ret else ""
370
+ typer.secho(
371
+ f"[c2rust-run] lib-replace: 替代映射: {ret['mapping']}\n"
372
+ f"[c2rust-run] lib-replace: 新符号表: {ret['symbols']}"
373
+ + order_msg,
374
+ fg=typer.colors.GREEN,
375
+ )
376
+ except Exception as _e:
377
+ typer.secho(f"[c2rust-run] lib-replace: 结果输出时发生非致命错误: {_e}", fg=typer.colors.YELLOW, err=True)
378
+
379
+ # Step 4: prepare
380
+ typer.secho("[c2rust-run] prepare: 开始", fg=typer.colors.BLUE)
381
+ _execute_llm_plan(apply=True, llm_group=llm_group)
382
+ typer.secho("[c2rust-run] prepare: 完成", fg=typer.colors.GREEN)
383
+
384
+ # Step 5: transpile
385
+ typer.secho("[c2rust-run] transpile: 开始", fg=typer.colors.BLUE)
386
+ from jarvis.jarvis_c2rust.transpiler import run_transpile as _run_transpile
387
+ _run_transpile(
388
+ project_root=Path("."),
389
+ crate_dir=None,
390
+ llm_group=llm_group,
391
+ only=None,
392
+ )
393
+ typer.secho("[c2rust-run] transpile: 完成", fg=typer.colors.GREEN)
394
+ except Exception as e:
395
+ typer.secho(f"[c2rust-run] 错误: {e}", fg=typer.colors.RED, err=True)
396
+ raise typer.Exit(code=1)
397
+
398
+
399
+ def main() -> None:
400
+ """主入口"""
401
+ app()
402
+
403
+
404
+ if __name__ == "__main__":
405
+ main()
@@ -0,0 +1,209 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ C/C++ 头文件函数名收集器(基于 libclang)。
4
+
5
+ 功能:
6
+ - 接受一个或多个头文件路径(.h/.hh/.hpp/.hxx),使用 libclang 精确解析,收集这些头文件中声明或定义的函数名。
7
+ - 优先输出限定名(qualified_name),回退到普通名称(name)。
8
+ - 自动逐层向上查找 compile_commands.json(直到文件系统根目录);若未找到则跳过并使用基础 -I 参数;失败时回退为空参数。
9
+ - 自动为头文件设置语言选项 (-x c-header 或 -x c++-header),提升解析成功率。
10
+
11
+ 输出:
12
+ - 将唯一的函数名集合写入指定输出文件(每行一个函数名,UTF-8 编码)。
13
+
14
+ 与现有实现的关系:
15
+ - 复用 jarvis.jarvis_c2rust.scanner 中的能力:
16
+ - _try_import_libclang: 兼容多版本 libclang 的加载器
17
+ - find_compile_commands / load_compile_commands: compile_commands.json 解析与逐层向上搜索
18
+ - get_qualified_name / is_function_like: 统一限定名生成和函数节点类型判断
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+ from typing import Dict, List, Optional, Set
25
+
26
+ # 复用 scanner 内的核心能力
27
+ from jarvis.jarvis_c2rust.scanner import ( # type: ignore
28
+ _try_import_libclang,
29
+ find_compile_commands,
30
+ load_compile_commands,
31
+ get_qualified_name,
32
+ is_function_like,
33
+ )
34
+
35
+
36
+ HEADER_EXTS = {".h", ".hh", ".hpp", ".hxx"}
37
+
38
+
39
+ def _resolve_compile_args_map(files: List[Path]) -> Dict[str, List[str]]:
40
+ """
41
+ 逐文件尝试向上查找 compile_commands.json(直到根目录),并合并解析为:
42
+ file_abs_path_str -> compile_args(list[str])
43
+ 若未找到任何 compile_commands.json,则返回空映射。
44
+ """
45
+ mapping: Dict[str, List[str]] = {}
46
+ visited_cc_paths: Set[Path] = set()
47
+
48
+ for f in files:
49
+ try:
50
+ cc_path = find_compile_commands(f.parent)
51
+ except Exception:
52
+ cc_path = None
53
+ if cc_path and cc_path.exists() and cc_path not in visited_cc_paths:
54
+ try:
55
+ m = load_compile_commands(cc_path)
56
+ if isinstance(m, dict):
57
+ mapping.update(m)
58
+ visited_cc_paths.add(cc_path)
59
+ except Exception:
60
+ # ignore parse errors, continue searching next file's tree
61
+ pass
62
+
63
+ return mapping
64
+
65
+
66
+ def _ensure_lang_header_args(file_path: Path, base_args: List[str]) -> List[str]:
67
+ """
68
+ 确保为头文件设置合适的语言选项:
69
+ - C 头文件: -x c-header
70
+ - C++ 头文件: -x c++-header
71
+ 若 base_args 已包含 -x,则尊重现有设置,不再强制覆盖。
72
+ """
73
+ args = list(base_args or [])
74
+ has_x = any(a == "-x" or a.startswith("-x") for a in args)
75
+ if has_x:
76
+ return args
77
+ ext = file_path.suffix.lower()
78
+ if ext in {".hpp", ".hxx", ".hh"}:
79
+ args.extend(["-x", "c++-header"])
80
+ else:
81
+ args.extend(["-x", "c-header"])
82
+ return args
83
+
84
+
85
+ def _scan_header_for_names(cindex, file_path: Path, args: List[str]) -> List[str]:
86
+ """
87
+ 扫描单个头文件,返回该文件中声明或定义的函数的限定名/名称列表。
88
+ - 不要求 is_definition(),以捕获函数原型声明
89
+ - 仅收集位于该文件本身的符号(根据 location.file 判断)
90
+ """
91
+ index = cindex.Index.create()
92
+ tu = index.parse(
93
+ str(file_path),
94
+ args=args,
95
+ options=0,
96
+ )
97
+ names: List[str] = []
98
+
99
+ def visit(node):
100
+ # 只收集属于当前文件的节点
101
+ loc_file = node.location.file
102
+ if loc_file is None or Path(loc_file.name).resolve() != file_path.resolve():
103
+ for ch in node.get_children():
104
+ visit(ch)
105
+ return
106
+
107
+ if is_function_like(node):
108
+ try:
109
+ qn = get_qualified_name(node)
110
+ nm = node.spelling or ""
111
+ label = (qn or nm or "").strip()
112
+ if label:
113
+ names.append(label)
114
+ except Exception:
115
+ nm = (node.spelling or "").strip()
116
+ if nm:
117
+ names.append(nm)
118
+
119
+ for ch in node.get_children():
120
+ visit(ch)
121
+
122
+ visit(tu.cursor)
123
+ return names
124
+
125
+
126
+ def collect_function_names(
127
+ files: List[Path],
128
+ out_path: Path,
129
+ ) -> Path:
130
+ """
131
+ 收集给定头文件中的函数名并写入指定文件。
132
+
133
+ 参数:
134
+ - files: 一个或多个 C/C++ 头文件路径(.h/.hh/.hpp/.hxx)
135
+ - out_path: 输出文件路径(将创建目录)
136
+
137
+ 返回:
138
+ - 写入的输出文件路径
139
+ """
140
+ if not files:
141
+ raise ValueError("必须至少提供一个头文件路径")
142
+
143
+ # 归一化与存在性检查,仅保留头文件
144
+ file_list: List[Path] = []
145
+ for p in files:
146
+ rp = Path(p).resolve()
147
+ if not rp.exists():
148
+ print(f"[c2rust-collector] 警告: 文件不存在,已跳过: {rp}")
149
+ continue
150
+ if not rp.is_file():
151
+ print(f"[c2rust-collector] 警告: 非普通文件,已跳过: {rp}")
152
+ continue
153
+ if rp.suffix.lower() not in HEADER_EXTS:
154
+ print(f"[c2rust-collector] 警告: 非头文件(仅支持 .h/.hh/.hpp/.hxx),已跳过: {rp}")
155
+ continue
156
+ file_list.append(rp)
157
+
158
+ if not file_list:
159
+ raise FileNotFoundError("提供的文件列表均不可用或不包含头文件(支持 .h/.hh/.hpp/.hxx)")
160
+
161
+ # 准备 libclang
162
+ cindex = _try_import_libclang()
163
+ if cindex is None:
164
+ # 与 scanner 的防御式处理保持一致:尽量不让 None 向下游传播
165
+ from clang import cindex as _ci # type: ignore
166
+ cindex = _ci
167
+
168
+ # 预检 Index 创建
169
+ try:
170
+ _ = cindex.Index.create()
171
+ except Exception as e:
172
+ raise RuntimeError(f"libclang 初始化失败: {e}")
173
+
174
+ # 逐层向上自动查找 compile_commands.json,构建参数映射
175
+ cc_args_map = _resolve_compile_args_map(file_list)
176
+
177
+ # 收集唯一函数名(优先限定名)
178
+ names: Set[str] = set()
179
+
180
+ for f in file_list:
181
+ # 优先使用 compile_commands 的精确参数;若无则提供基础 -I 与头文件语言选项
182
+ base_args = cc_args_map.get(str(f), ["-I", str(f.parent)])
183
+ args = _ensure_lang_header_args(f, base_args)
184
+ try:
185
+ fn_names = _scan_header_for_names(cindex, f, args)
186
+ except Exception:
187
+ # 回退到无参数解析(仍添加语言选项)
188
+ try:
189
+ fn_names = _scan_header_for_names(cindex, f, _ensure_lang_header_args(f, []))
190
+ except Exception as e2:
191
+ print(f"[c2rust-collector] 解析失败,已跳过 {f}: {e2}")
192
+ continue
193
+
194
+ for label in fn_names:
195
+ if label:
196
+ names.add(label.strip())
197
+
198
+ # 写出结果
199
+ out_path = Path(out_path)
200
+ out_path.parent.mkdir(parents=True, exist_ok=True)
201
+ try:
202
+ with out_path.open("w", encoding="utf-8") as fo:
203
+ for nm in sorted(names):
204
+ fo.write(nm + "\n")
205
+ except Exception as e:
206
+ raise RuntimeError(f"写入输出文件失败: {out_path}: {e}")
207
+
208
+ print(f"[c2rust-collector] 已收集到 {len(names)} 个函数名(来自头文件) -> {out_path}")
209
+ return out_path