parent
c9c558103c
commit
7c5c716b36
8 changed files with 1430 additions and 38 deletions
@ -0,0 +1,495 @@ |
||||
#!/usr/bin/env python3 |
||||
""" |
||||
使用 libclang 解析 C++ 代码并添加头文件路径 |
||||
""" |
||||
|
||||
import clang.cindex |
||||
from clang.cindex import Index, CursorKind, TypeKind, StorageClass |
||||
import json |
||||
import sys |
||||
import os |
||||
from pathlib import Path |
||||
import subprocess |
||||
import argparse |
||||
|
||||
|
||||
class PHPConfigHelper: |
||||
"""PHP 配置辅助类""" |
||||
|
||||
def __init__(self, php_config_path='php-config'): |
||||
self.php_config = php_config_path |
||||
self._check_availability() |
||||
|
||||
def _check_availability(self): |
||||
"""检查 php-config 是否可用""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--version'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
print(f"✓ 找到 PHP {result.stdout.strip()}") |
||||
except (FileNotFoundError, subprocess.CalledProcessError) as e: |
||||
print(f"警告: php-config 不可用: {e}") |
||||
# 尝试使用常见路径 |
||||
common_paths = [ |
||||
'/usr/bin/php-config', |
||||
'/usr/local/bin/php-config', |
||||
'/opt/php/bin/php-config' |
||||
] |
||||
for path in common_paths: |
||||
if os.path.exists(path): |
||||
self.php_config = path |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--version'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
print(f"✓ 找到 PHP {result.stdout.strip()} 在 {path}") |
||||
return |
||||
except (FileNotFoundError, subprocess.CalledProcessError): |
||||
continue |
||||
print("警告: php-config 在任何常见路径都不可用,使用默认路径") |
||||
|
||||
def get_includes(self): |
||||
""" |
||||
获取 include 路径列表 |
||||
|
||||
Returns: |
||||
list: 头文件路径列表(不带 -I 前缀) |
||||
""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--includes'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
|
||||
# 解析输出: "-I/path1 -I/path2" -> ['/path1', '/path2'] |
||||
includes = [] |
||||
for flag in result.stdout.strip().split(): |
||||
if flag.startswith('-I'): |
||||
includes.append(flag[2:]) |
||||
|
||||
return includes |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
print("警告: 无法获取 PHP includes,使用默认路径") |
||||
return ['/usr/include/php', '/usr/include/php/20210902'] # 默认路径 |
||||
|
||||
def get_include_dir(self): |
||||
"""获取主 include 目录""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--include-dir'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
return result.stdout.strip() |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
return '/usr/include/php' |
||||
|
||||
def get_extension_dir(self): |
||||
"""获取扩展目录""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--extension-dir'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
return result.stdout.strip() |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
return '/usr/lib/php' |
||||
|
||||
def get_version(self): |
||||
"""获取 PHP 版本""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--version'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
return result.stdout.strip() |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
return 'unknown' |
||||
|
||||
def get_php_binary(self): |
||||
"""获取 PHP 二进制路径""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--php-binary'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
return result.stdout.strip() |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
return 'php' |
||||
|
||||
def get_configure_options(self): |
||||
"""获取配置选项""" |
||||
try: |
||||
result = subprocess.run( |
||||
[self.php_config, '--configure-options'], |
||||
capture_output=True, |
||||
text=True, |
||||
check=True |
||||
) |
||||
return result.stdout.strip() |
||||
except (subprocess.CalledProcessError, FileNotFoundError): |
||||
return '' |
||||
|
||||
def get_all_info(self): |
||||
"""获取所有配置信息""" |
||||
return { |
||||
'version': self.get_version(), |
||||
'includes': self.get_includes(), |
||||
'include_dir': self.get_include_dir(), |
||||
'extension_dir': self.get_extension_dir(), |
||||
'php_binary': self.get_php_binary(), |
||||
'configure_options': self.get_configure_options(), |
||||
} |
||||
|
||||
|
||||
class ClangParser: |
||||
def __init__(self, libclang_path=None): |
||||
""" |
||||
初始化 Clang 解析器 |
||||
|
||||
Args: |
||||
libclang_path: libclang 库的路径(可选) |
||||
""" |
||||
if libclang_path: |
||||
try: |
||||
clang.cindex.Config.set_library_file(libclang_path) |
||||
except Exception as e: |
||||
print(f"警告: 无法设置 libclang 路径 {libclang_path}: {e}") |
||||
print("尝试使用默认路径...") |
||||
|
||||
try: |
||||
self.index = Index.create() |
||||
except Exception as e: |
||||
print(f"错误: 无法创建 Clang 索引: {e}") |
||||
print("请确保已安装 python3-clang 和 clang 库") |
||||
raise |
||||
|
||||
def parse_file(self, filename, include_paths=None, defines=None, |
||||
compiler_args=None, language='c++'): |
||||
""" |
||||
解析 C++ 文件 |
||||
|
||||
Args: |
||||
filename: 要解析的文件路径 |
||||
include_paths: 头文件搜索路径列表 |
||||
defines: 宏定义列表 ['MACRO=value', 'DEBUG'] |
||||
compiler_args: 额外的编译器参数 |
||||
language: 语言类型 ('c', 'c++', 'objective-c') |
||||
|
||||
Returns: |
||||
TranslationUnit 对象 |
||||
""" |
||||
if not os.path.exists(filename): |
||||
raise FileNotFoundError(f"文件不存在: {filename}") |
||||
|
||||
args = [] |
||||
|
||||
# 1. 设置语言标准 |
||||
if language == 'c++': |
||||
args.extend([ |
||||
'-x', 'c++', |
||||
'-std=c++14', # 更标准的 C++ 版本 |
||||
]) |
||||
elif language == 'c': |
||||
args.extend(['-x', 'c', '-std=c11']) |
||||
|
||||
# 2. 添加头文件搜索路径 |
||||
if include_paths: |
||||
for path in include_paths: |
||||
if os.path.exists(path): # 检查路径是否存在 |
||||
args.append(f'-I{path}') |
||||
else: |
||||
print(f"警告: 包含路径不存在: {path}") |
||||
|
||||
# 3. 添加宏定义 |
||||
if defines: |
||||
for define in defines: |
||||
args.append(f'-D{define}') |
||||
|
||||
# 4. 添加额外的编译器参数 |
||||
if compiler_args: |
||||
args.extend(compiler_args) |
||||
|
||||
# 5. 常用的编译选项 |
||||
args.extend([ |
||||
'-Wno-pragma-once-outside-header', # 忽略警告 |
||||
'-ferror-limit=0', # 不限制错误数量 |
||||
'-fno-delayed-template-parsing', # 避免某些 C++ 模板解析问题 |
||||
'-w', # 禁用所有警告以减少输出 |
||||
]) |
||||
|
||||
print(f"编译参数: {' '.join(args)}") |
||||
|
||||
# 解析文件 |
||||
try: |
||||
tu = self.index.parse( |
||||
filename, |
||||
args=args, |
||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
||||
) |
||||
except Exception as e: |
||||
print(f"解析文件时出错: {e}") |
||||
print("尝试使用最小参数集...") |
||||
# 尝试使用最小参数集 |
||||
minimal_args = ['-x', 'c++', '-std=c++14', '-w'] |
||||
if include_paths: |
||||
for path in include_paths: |
||||
if os.path.exists(path): |
||||
minimal_args.append(f'-I{path}') |
||||
try: |
||||
tu = self.index.parse( |
||||
filename, |
||||
args=minimal_args, |
||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
||||
) |
||||
print("使用最小参数集成功解析") |
||||
except Exception as e2: |
||||
print(f"使用最小参数集也失败: {e2}") |
||||
print("尝试解析不包含头文件的简化版本...") |
||||
# 创建一个临时文件,移除头文件包含行 |
||||
temp_filename = filename + ".tmp" |
||||
with open(filename, 'r') as original: |
||||
lines = original.readlines() |
||||
|
||||
# 移除 #include 行 |
||||
filtered_lines = [line for line in lines if not line.strip().startswith('#include')] |
||||
|
||||
with open(temp_filename, 'w') as temp: |
||||
temp.writelines(filtered_lines) |
||||
|
||||
try: |
||||
tu = self.index.parse( |
||||
temp_filename, |
||||
args=minimal_args, |
||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
||||
) |
||||
print("解析简化版本成功") |
||||
# 清理临时文件 |
||||
os.remove(temp_filename) |
||||
except Exception as e3: |
||||
print(f"简化版本也失败: {e3}") |
||||
# 清理临时文件 |
||||
if os.path.exists(temp_filename): |
||||
os.remove(temp_filename) |
||||
raise |
||||
|
||||
# 检查诊断信息 |
||||
if tu.diagnostics: |
||||
print(f"\n诊断信息 ({len(tu.diagnostics)} 个):") |
||||
error_count = 0 |
||||
warning_count = 0 |
||||
|
||||
for diag in tu.diagnostics: |
||||
if diag.severity >= 3: # 错误级别 |
||||
error_count += 1 |
||||
else: # 警告级别 |
||||
warning_count += 1 |
||||
|
||||
print(f"错误: {error_count}, 警告: {warning_count}") |
||||
|
||||
# 只显示前几个诊断信息,避免输出过多 |
||||
for i, diag in enumerate(tu.diagnostics): |
||||
if i >= 5: # 只显示前5个 |
||||
print("... 还有更多诊断信息") |
||||
break |
||||
print(f" [{diag.severity}] {diag.spelling}") |
||||
if diag.location.file: |
||||
print(f" at {diag.location.file.name}:{diag.location.line}") |
||||
|
||||
return tu |
||||
|
||||
def extract_functions(self, tu, name_prefixes=None): |
||||
""" |
||||
提取函数定义 |
||||
|
||||
Args: |
||||
tu: TranslationUnit 对象 |
||||
name_prefixes: 函数名前缀过滤列表 |
||||
|
||||
Returns: |
||||
函数信息列表 |
||||
""" |
||||
functions = [] |
||||
|
||||
def visit_node(node, depth=0): |
||||
# 只处理函数声明/定义 |
||||
if node.kind == CursorKind.FUNCTION_DECL: |
||||
try: |
||||
func_info = self.parse_function(node) |
||||
|
||||
# 过滤函数名 |
||||
if name_prefixes: |
||||
if any(func_info['name'].startswith(prefix) |
||||
for prefix in name_prefixes): |
||||
functions.append(func_info) |
||||
else: |
||||
functions.append(func_info) |
||||
except Exception as e: |
||||
print(f"解析函数时出错: {e}") |
||||
|
||||
# 递归访问子节点 |
||||
for child in node.get_children(): |
||||
visit_node(child, depth + 1) |
||||
|
||||
visit_node(tu.cursor) |
||||
return functions |
||||
|
||||
def parse_function(self, cursor): |
||||
""" |
||||
解析函数详细信息 |
||||
""" |
||||
# 检查方法是否存在 |
||||
def safe_call(method, default_value=None): |
||||
try: |
||||
return method() |
||||
except AttributeError: |
||||
return default_value |
||||
|
||||
# 基本信息 |
||||
func_info = { |
||||
'name': cursor.spelling, |
||||
'displayName': cursor.displayname, |
||||
'mangledName': cursor.mangled_name, |
||||
'returnType': cursor.result_type.spelling, |
||||
'isStatic': cursor.storage_class == StorageClass.STATIC, |
||||
'isInline': safe_call(lambda: cursor.is_inline_function(), False), |
||||
'isVirtual': safe_call(lambda: cursor.is_virtual_method(), False), |
||||
'isConst': safe_call(lambda: cursor.is_const_method(), False), |
||||
'location': { |
||||
'file': str(cursor.location.file) if cursor.location.file else None, |
||||
'line': cursor.location.line, |
||||
'column': cursor.location.column, |
||||
}, |
||||
'parameters': [], |
||||
'namespaces': self.get_namespaces(cursor), |
||||
} |
||||
|
||||
# 解析参数 |
||||
for arg in cursor.get_arguments(): |
||||
param_info = { |
||||
'name': arg.spelling or f'arg{len(func_info["parameters"])}', |
||||
'type': arg.type.spelling, |
||||
'canonicalType': arg.type.get_canonical().spelling, |
||||
} |
||||
|
||||
# 检查是否有默认值 |
||||
try: |
||||
for token in arg.get_tokens(): |
||||
if token.spelling == '=': |
||||
# 有默认值 |
||||
param_info['hasDefault'] = True |
||||
break |
||||
except: |
||||
# 如果无法获取 tokens,跳过默认值检查 |
||||
pass |
||||
|
||||
func_info['parameters'].append(param_info) |
||||
|
||||
return func_info |
||||
|
||||
def get_namespaces(self, cursor): |
||||
""" |
||||
获取函数所在的命名空间 |
||||
""" |
||||
namespaces = [] |
||||
parent = cursor.semantic_parent |
||||
|
||||
while parent and parent.kind != CursorKind.TRANSLATION_UNIT: |
||||
if parent.kind == CursorKind.NAMESPACE: |
||||
namespaces.insert(0, parent.spelling) |
||||
parent = parent.semantic_parent |
||||
|
||||
return namespaces |
||||
|
||||
|
||||
def main(): |
||||
parser = argparse.ArgumentParser(description='使用 libclang 解析 C++ 代码并提取函数信息') |
||||
parser.add_argument('filename', help='要解析的 C++ 文件路径') |
||||
parser.add_argument('--libclang-path', help='libclang 库路径') |
||||
parser.add_argument('--include-paths', nargs='*', help='额外的包含路径') |
||||
parser.add_argument('--function-prefixes', nargs='*', help='函数名前缀过滤器') |
||||
|
||||
args = parser.parse_args() |
||||
|
||||
if not os.path.exists(args.filename): |
||||
print(f"错误: 文件不存在: {args.filename}") |
||||
sys.exit(1) |
||||
|
||||
try: |
||||
# 创建解析器 |
||||
parser_obj = ClangParser(libclang_path=args.libclang_path) |
||||
|
||||
# 配置头文件路径 |
||||
include_paths = args.include_paths or [ |
||||
"/usr/include/linux", |
||||
"/home/swoole/workspace/projects/phpx/include" |
||||
] |
||||
|
||||
# 尝试获取 PHP 配置的头文件路径 |
||||
try: |
||||
php_config = PHPConfigHelper() |
||||
php_includes = php_config.get_includes() |
||||
include_paths.extend(php_includes) |
||||
except Exception as e: |
||||
print(f"警告: 无法获取 PHP 配置: {e}") |
||||
print("继续使用默认路径...") |
||||
|
||||
# 配置宏定义 |
||||
defines = [ |
||||
'HAVE_CONFIG_H', |
||||
'ZEND_ENABLE_STATIC_TSRMLS_CACHE=1', |
||||
] |
||||
|
||||
# 额外的编译器参数 |
||||
compiler_args = [ |
||||
'-fparse-all-comments', # 解析所有注释 |
||||
'-Wno-unknown-pragmas', |
||||
] |
||||
|
||||
# 解析文件 |
||||
tu = parser_obj.parse_file( |
||||
args.filename, |
||||
include_paths=include_paths, |
||||
defines=defines, |
||||
compiler_args=compiler_args, |
||||
language='c++' |
||||
) |
||||
|
||||
# 提取函数 |
||||
name_prefixes = args.function_prefixes or None |
||||
functions = parser_obj.extract_functions(tu, name_prefixes=name_prefixes) |
||||
|
||||
# 输出结果 |
||||
output = { |
||||
'file': args.filename, |
||||
'functions': functions, |
||||
'total': len(functions), |
||||
} |
||||
|
||||
print(json.dumps(output, indent=2, ensure_ascii=False)) |
||||
|
||||
except clang.cindex.TranslationUnitLoadError as e: |
||||
print(f"翻译单元加载错误: {e}") |
||||
print("这通常意味着 C++ 代码包含语法错误或缺少必要的头文件") |
||||
sys.exit(1) |
||||
except Exception as e: |
||||
print(f"错误: {e}") |
||||
sys.exit(1) |
||||
|
||||
if __name__ == '__main__': |
||||
main() |
||||
@ -0,0 +1,157 @@ |
||||
<?php |
||||
require __DIR__ . '/bootstrap.php'; |
||||
// ============================================================================ |
||||
// 命令行接口 |
||||
// ============================================================================ |
||||
|
||||
function showUsage(): void |
||||
{ |
||||
echo <<<USAGE |
||||
用法: php extract_functions.php <file.cpp> [prefix] [output.json] |
||||
|
||||
参数: |
||||
file.cpp 要分析的 C/C++ 文件(必需) |
||||
prefix 函数名前缀,多个前缀用逗号分隔(默认: php_) |
||||
output.json 输出文件路径(默认: stdout) |
||||
|
||||
选项: |
||||
--help, -h 显示此帮助信息 |
||||
--pretty 美化 JSON 输出 |
||||
--batch 批量处理模式(从 stdin 读取文件列表) |
||||
|
||||
示例: |
||||
# 基本用法 |
||||
php extract_functions.php myfile.cpp |
||||
|
||||
# 指定前缀 |
||||
php extract_functions.php myfile.cpp php_ |
||||
|
||||
# 多个前缀 |
||||
php extract_functions.php myfile.cpp php_,swoole_,zend_ |
||||
|
||||
# 保存到文件 |
||||
php extract_functions.php myfile.cpp php_ output.json |
||||
|
||||
# 美化输出 |
||||
php extract_functions.php myfile.cpp php_ output.json --pretty |
||||
|
||||
# 批量处理 |
||||
find . -name "*.cpp" | php extract_functions.php --batch php_ |
||||
|
||||
特点: |
||||
✓ 无需头文件 |
||||
✓ 速度快 |
||||
✓ 支持 C/C++ |
||||
✓ 输出 JSON 格式 |
||||
✓ 支持复杂的参数类型 |
||||
|
||||
USAGE; |
||||
} |
||||
|
||||
function main(array $argv): void |
||||
{ |
||||
// 解析命令行参数 |
||||
$options = [ |
||||
'help' => false, |
||||
'pretty' => false, |
||||
'batch' => false, |
||||
]; |
||||
|
||||
$args = []; |
||||
|
||||
for ($i = 1; $i < count($argv); $i++) { |
||||
$arg = $argv[$i]; |
||||
|
||||
if ($arg === '--help' || $arg === '-h') { |
||||
$options['help'] = true; |
||||
} elseif ($arg === '--pretty') { |
||||
$options['pretty'] = true; |
||||
} elseif ($arg === '--batch') { |
||||
$options['batch'] = true; |
||||
} else { |
||||
$args[] = $arg; |
||||
} |
||||
} |
||||
|
||||
// 显示帮助 |
||||
if ($options['help'] || (empty($args) && !$options['batch'])) { |
||||
showUsage(); |
||||
exit(0); |
||||
} |
||||
|
||||
// 创建提取器 |
||||
$extractor = new PhpAot\Php\Extractor(); |
||||
|
||||
// 批量模式 |
||||
if ($options['batch']) { |
||||
$prefixes = !empty($args[0]) ? explode(',', $args[0]) : ['php_']; |
||||
$files = []; |
||||
|
||||
while ($line = fgets(STDIN)) { |
||||
$file = trim($line); |
||||
if (!empty($file) && file_exists($file)) { |
||||
$files[] = $file; |
||||
} |
||||
} |
||||
|
||||
if (empty($files)) { |
||||
fprintf(STDERR, "错误: 未找到有效的文件\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
$functions = $extractor->extractFromFiles($files, $prefixes); |
||||
|
||||
// 输出结果 |
||||
$jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; |
||||
if ($options['pretty']) { |
||||
$jsonFlags |= JSON_PRETTY_PRINT; |
||||
} |
||||
|
||||
echo json_encode($functions, $jsonFlags) . "\n"; |
||||
exit(0); |
||||
} |
||||
|
||||
// 单文件模式 |
||||
$filename = $args[0] ?? null; |
||||
$prefixesStr = $args[1] ?? 'php_'; |
||||
$output = $args[2] ?? null; |
||||
|
||||
if (!$filename) { |
||||
fprintf(STDERR, "错误: 未指定文件\n"); |
||||
showUsage(); |
||||
exit(1); |
||||
} |
||||
|
||||
// 解析前缀 |
||||
$prefixes = array_map('trim', explode(',', $prefixesStr)); |
||||
|
||||
try { |
||||
// 提取函数 |
||||
$functions = $extractor->extractFunctions($filename, $prefixes); |
||||
|
||||
// 准备 JSON 输出 |
||||
$jsonFlags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; |
||||
if ($options['pretty']) { |
||||
$jsonFlags |= JSON_PRETTY_PRINT; |
||||
} |
||||
|
||||
$json = json_encode($functions, $jsonFlags); |
||||
|
||||
// 输出 |
||||
if ($output) { |
||||
file_put_contents($output, $json . "\n"); |
||||
fprintf(STDERR, "\033[0;32m已保存到: %s\033[0m\n", $output); |
||||
} else { |
||||
echo $json . "\n"; |
||||
} |
||||
|
||||
} catch (Exception $e) { |
||||
fprintf(STDERR, "\033[0;31m错误: %s\033[0m\n", $e->getMessage()); |
||||
exit(1); |
||||
} |
||||
} |
||||
|
||||
// 运行主函数 |
||||
if (php_sapi_name() === 'cli') { |
||||
main($argv); |
||||
} |
||||
@ -0,0 +1,614 @@ |
||||
<?php |
||||
|
||||
namespace PhpAot\Php; |
||||
|
||||
class Extractor |
||||
{ |
||||
private string $ctagsPath = 'ctags'; |
||||
private bool $isUniversalCtags = false; |
||||
|
||||
public function __construct() |
||||
{ |
||||
$this->checkCtags(); |
||||
} |
||||
|
||||
/** |
||||
* 检查 ctags 是否可用 |
||||
*/ |
||||
private function checkCtags(): void |
||||
{ |
||||
$output = shell_exec("{$this->ctagsPath} --version 2>&1"); |
||||
|
||||
if ($output === null) { |
||||
$this->error("未找到 ctags 命令\n安装: sudo apt install universal-ctags"); |
||||
} |
||||
|
||||
$this->isUniversalCtags = stripos($output, 'Universal Ctags') !== false; |
||||
|
||||
if (!$this->isUniversalCtags) { |
||||
$this->warn("建议使用 Universal Ctags 以获得更好的支持"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 提取函数定义 |
||||
* |
||||
* @param string $filename 文件路径 |
||||
* @param array $prefixes 函数名前缀列表 |
||||
* @return array 函数列表 |
||||
*/ |
||||
public function extractFunctions(string $filename, array $prefixes = ['php_']): array |
||||
{ |
||||
if (!file_exists($filename)) { |
||||
throw new RuntimeException("文件不存在: {$filename}"); |
||||
} |
||||
|
||||
$this->info("分析文件: {$filename}"); |
||||
$this->info("函数前缀: " . implode(', ', $prefixes)); |
||||
|
||||
// 运行 ctags |
||||
$tags = $this->runCtags($filename); |
||||
|
||||
// 过滤和解析函数 |
||||
$functions = []; |
||||
foreach ($tags as $tag) { |
||||
if ($tag['kind'] !== 'function') { |
||||
continue; |
||||
} |
||||
|
||||
$funcName = $tag['name'] ?? ''; |
||||
|
||||
// 检查前缀 |
||||
$matched = false; |
||||
foreach ($prefixes as $prefix) { |
||||
if (str_starts_with($funcName, $prefix)) { |
||||
$matched = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (!$matched) { |
||||
continue; |
||||
} |
||||
|
||||
// 解析函数详细信息 |
||||
$funcInfo = $this->parseFunction($filename, $tag); |
||||
if ($funcInfo) { |
||||
$functions[] = $funcInfo; |
||||
} |
||||
} |
||||
|
||||
$this->info("找到 " . count($functions) . " 个函数"); |
||||
|
||||
return $functions; |
||||
} |
||||
|
||||
/** |
||||
* 运行 ctags 命令 |
||||
*/ |
||||
private function runCtags(string $filename): array |
||||
{ |
||||
$cmd = sprintf( |
||||
'%s --output-format=json --fields=+nKSzZt --kinds-c++=f --extras=+q -f - %s 2>&1', |
||||
escapeshellcmd($this->ctagsPath), |
||||
escapeshellarg($filename) |
||||
); |
||||
|
||||
$output = shell_exec($cmd); |
||||
|
||||
if ($output === null) { |
||||
throw new RuntimeException("ctags 执行失败"); |
||||
} |
||||
|
||||
// 解析 JSON 输出 |
||||
$tags = []; |
||||
$lines = explode("\n", trim($output)); |
||||
|
||||
foreach ($lines as $line) { |
||||
if (empty($line)) { |
||||
continue; |
||||
} |
||||
|
||||
$tag = json_decode($line, true); |
||||
if ($tag === null) { |
||||
continue; |
||||
} |
||||
|
||||
$tags[] = $tag; |
||||
} |
||||
|
||||
return $tags; |
||||
} |
||||
|
||||
/** |
||||
* 解析单个函数的详细信息 |
||||
*/ |
||||
private function parseFunction(string $filename, array $tag): ?array |
||||
{ |
||||
$funcName = $tag['name'] ?? ''; |
||||
$lineNum = $tag['line'] ?? 0; |
||||
|
||||
if (empty($funcName) || $lineNum < 1) { |
||||
return null; |
||||
} |
||||
|
||||
// 提取完整的函数签名 |
||||
$signature = $this->extractSignature($filename, $lineNum, $funcName); |
||||
|
||||
if (empty($signature)) { |
||||
return null; |
||||
} |
||||
|
||||
// 解析返回类型 |
||||
$returnType = $this->parseReturnType($signature, $funcName); |
||||
|
||||
// 解析参数 |
||||
$parameters = $this->parseParameters($signature, $funcName); |
||||
|
||||
return [ |
||||
'name' => $funcName, |
||||
'returnType' => $returnType, |
||||
'signature' => $signature, |
||||
'parameters' => $parameters, |
||||
'location' => [ |
||||
'file' => $filename, |
||||
'line' => $lineNum |
||||
], |
||||
'scope' => $tag['scope'] ?? null, |
||||
'scopeKind' => $tag['scopeKind'] ?? null |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* 从源文件中提取完整的函数签名 |
||||
*/ |
||||
private function extractSignature(string $filename, int $lineNum, string $funcName): string |
||||
{ |
||||
$lines = file($filename, FILE_IGNORE_NEW_LINES); |
||||
|
||||
if ($lines === false || $lineNum > count($lines)) { |
||||
return ''; |
||||
} |
||||
|
||||
// 从函数声明行开始收集,直到遇到 { 或 ; |
||||
$signatureLines = []; |
||||
$maxLines = min($lineNum + 20, count($lines)); |
||||
|
||||
for ($i = $lineNum - 1; $i < $maxLines; $i++) { |
||||
$line = $lines[$i]; |
||||
$signatureLines[] = $line; |
||||
|
||||
// 检查是否到达函数体或声明结束 |
||||
if (strpos($line, '{') !== false || strpos($line, ';') !== false) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// 合并并清理 |
||||
$signature = implode(' ', $signatureLines); |
||||
|
||||
// 移除 { 或 ; 之后的内容 |
||||
$signature = preg_replace('/[{;].*$/', '', $signature); |
||||
|
||||
// 合并多个空白字符 |
||||
$signature = preg_replace('/\s+/', ' ', $signature); |
||||
|
||||
// 清理首尾空白 |
||||
$signature = trim($signature); |
||||
|
||||
return $signature; |
||||
} |
||||
|
||||
/** |
||||
* 解析返回类型 |
||||
*/ |
||||
private function parseReturnType(string $signature, string $funcName): string |
||||
{ |
||||
// 匹配: <返回类型> <函数名>( |
||||
$pattern = '/^(.+?)\s+' . preg_quote($funcName, '/') . '\s*\(/'; |
||||
|
||||
if (preg_match($pattern, $signature, $matches)) { |
||||
$returnType = trim($matches[1]); |
||||
|
||||
// 移除可能的修饰符 |
||||
$returnType = preg_replace('/\b(static|inline|extern|virtual|explicit)\b/', '', $returnType); |
||||
$returnType = preg_replace('/\s+/', ' ', $returnType); |
||||
$returnType = trim($returnType); |
||||
|
||||
return $returnType ?: 'void'; |
||||
} |
||||
|
||||
return 'unknown'; |
||||
} |
||||
|
||||
/** |
||||
* 解析参数列表 |
||||
*/ |
||||
private function parseParameters(string $signature, string $funcName): array |
||||
{ |
||||
// 提取括号内的参数 |
||||
$pattern = '/' . preg_quote($funcName, '/') . '\s*\((.*?)\)/s'; |
||||
|
||||
if (!preg_match($pattern, $signature, $matches)) { |
||||
return []; |
||||
} |
||||
|
||||
$paramsStr = trim($matches[1]); |
||||
|
||||
// 空参数或 void |
||||
if (empty($paramsStr) || $paramsStr === 'void') { |
||||
return []; |
||||
} |
||||
|
||||
// 分割参数(处理嵌套的模板和括号) |
||||
$params = $this->splitParameters($paramsStr); |
||||
|
||||
$parameters = []; |
||||
foreach ($params as $param) { |
||||
$param = trim($param); |
||||
|
||||
if (empty($param)) { |
||||
continue; |
||||
} |
||||
|
||||
$paramInfo = $this->parseParameter($param); |
||||
if ($paramInfo) { |
||||
$parameters[] = $paramInfo; |
||||
} |
||||
} |
||||
|
||||
return $parameters; |
||||
} |
||||
|
||||
/** |
||||
* 智能分割参数(处理嵌套的模板和括号) |
||||
*/ |
||||
private function splitParameters(string $paramsStr): array |
||||
{ |
||||
$params = []; |
||||
$current = ''; |
||||
$depth = 0; |
||||
$length = strlen($paramsStr); |
||||
|
||||
for ($i = 0; $i < $length; $i++) { |
||||
$char = $paramsStr[$i]; |
||||
|
||||
if ($char === '<' || $char === '(' || $char === '[') { |
||||
$depth++; |
||||
$current .= $char; |
||||
} elseif ($char === '>' || $char === ')' || $char === ']') { |
||||
$depth--; |
||||
$current .= $char; |
||||
} elseif ($char === ',' && $depth === 0) { |
||||
$params[] = $current; |
||||
$current = ''; |
||||
} else { |
||||
$current .= $char; |
||||
} |
||||
} |
||||
|
||||
if (!empty($current)) { |
||||
$params[] = $current; |
||||
} |
||||
|
||||
return $params; |
||||
} |
||||
|
||||
/** |
||||
* 解析单个参数 |
||||
*/ |
||||
private function parseParameter(string $param): ?array |
||||
{ |
||||
$param = trim($param); |
||||
|
||||
// 移除默认值 |
||||
$param = preg_replace('/\s*=\s*.*$/', '', $param); |
||||
|
||||
// 尝试匹配: <类型> <名称> |
||||
// 支持复杂类型如: const char*, std::string&, int**, etc. |
||||
if (preg_match('/^(.+?)\s+(\w+)\s*$/', $param, $matches)) { |
||||
return [ |
||||
'type' => trim($matches[1]), |
||||
'name' => trim($matches[2]) |
||||
]; |
||||
} |
||||
|
||||
// 只有类型,没有名称 |
||||
return [ |
||||
'type' => $param, |
||||
'name' => '' |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* 批量提取多个文件 |
||||
*/ |
||||
public function extractFromFiles(array $files, array $prefixes = ['php_']): array |
||||
{ |
||||
$allFunctions = []; |
||||
|
||||
foreach ($files as $file) { |
||||
try { |
||||
$functions = $this->extractFunctions($file, $prefixes); |
||||
$allFunctions = array_merge($allFunctions, $functions); |
||||
} catch (Exception $e) { |
||||
$this->error("处理文件 {$file} 失败: " . $e->getMessage()); |
||||
} |
||||
} |
||||
|
||||
return $allFunctions; |
||||
} |
||||
|
||||
/** |
||||
* 输出信息 |
||||
*/ |
||||
private function info(string $message): void |
||||
{ |
||||
fprintf(STDERR, "\033[0;32m%s\033[0m\n", $message); |
||||
} |
||||
|
||||
/** |
||||
* 输出警告 |
||||
*/ |
||||
private function warn(string $message): void |
||||
{ |
||||
fprintf(STDERR, "\033[1;33m警告: %s\033[0m\n", $message); |
||||
} |
||||
|
||||
/** |
||||
* 输出错误并退出 |
||||
*/ |
||||
private function error(string $message): void |
||||
{ |
||||
fprintf(STDERR, "\033[0;31m错误: %s\033[0m\n", $message); |
||||
exit(1); |
||||
} |
||||
|
||||
/** |
||||
* 提取函数并添加额外信息 |
||||
*/ |
||||
public function extractWithMetadata(string $filename, array $prefixes = ['php_']): array |
||||
{ |
||||
$functions = $this->extractFunctions($filename, $prefixes); |
||||
|
||||
// 添加额外的元数据 |
||||
foreach ($functions as &$func) { |
||||
$func['metadata'] = $this->extractMetadata($filename, $func); |
||||
} |
||||
|
||||
return $functions; |
||||
} |
||||
|
||||
/** |
||||
* 提取函数的元数据(注释、属性等) |
||||
*/ |
||||
private function extractMetadata(string $filename, array $func): array |
||||
{ |
||||
$lineNum = $func['location']['line']; |
||||
$lines = file($filename, FILE_IGNORE_NEW_LINES); |
||||
|
||||
$metadata = [ |
||||
'comments' => [], |
||||
'attributes' => [], |
||||
'isPHPFunction' => false, |
||||
'isStatic' => false, |
||||
'isInline' => false, |
||||
]; |
||||
|
||||
// 提取函数前的注释 |
||||
$comments = $this->extractComments($lines, $lineNum); |
||||
$metadata['comments'] = $comments; |
||||
|
||||
// 检测 PHP 函数宏 |
||||
$metadata['isPHPFunction'] = $this->isPHPFunction($func['signature']); |
||||
|
||||
// 检测修饰符 |
||||
$signature = $func['signature']; |
||||
$metadata['isStatic'] = strpos($signature, 'static') !== false; |
||||
$metadata['isInline'] = strpos($signature, 'inline') !== false; |
||||
|
||||
// 提取文档注释中的标签 |
||||
$metadata['docTags'] = $this->parseDocTags($comments); |
||||
|
||||
return $metadata; |
||||
} |
||||
|
||||
/** |
||||
* 提取函数前的注释 |
||||
*/ |
||||
private function extractComments(array $lines, int $lineNum): array |
||||
{ |
||||
$comments = []; |
||||
$i = $lineNum - 2; // 从函数声明的前一行开始 |
||||
|
||||
// 向上查找注释 |
||||
while ($i >= 0) { |
||||
$line = trim($lines[$i]); |
||||
|
||||
// 空行 |
||||
if (empty($line)) { |
||||
$i--; |
||||
continue; |
||||
} |
||||
|
||||
// C++ 风格注释 |
||||
if (str_starts_with($line, '//')) { |
||||
array_unshift($comments, substr($line, 2)); |
||||
$i--; |
||||
continue; |
||||
} |
||||
|
||||
// C 风格注释结束 |
||||
if (str_ends_with($line, '*/')) { |
||||
$commentLines = [$line]; |
||||
$i--; |
||||
|
||||
// 继续向上查找注释开始 |
||||
while ($i >= 0) { |
||||
$commentLine = trim($lines[$i]); |
||||
array_unshift($commentLines, $commentLine); |
||||
|
||||
if (str_starts_with($commentLine, '/*')) { |
||||
break; |
||||
} |
||||
$i--; |
||||
} |
||||
|
||||
// 解析多行注释 |
||||
$comment = implode("\n", $commentLines); |
||||
$comment = preg_replace('#^/\*+\s*#', '', $comment); |
||||
$comment = preg_replace('#\s*\*+/$#', '', $comment); |
||||
$comment = preg_replace('#^\s*\*\s?#m', '', $comment); |
||||
|
||||
array_unshift($comments, trim($comment)); |
||||
$i--; |
||||
continue; |
||||
} |
||||
|
||||
// 遇到非注释行,停止 |
||||
break; |
||||
} |
||||
|
||||
return $comments; |
||||
} |
||||
|
||||
/** |
||||
* 检测是否是 PHP 函数宏定义 |
||||
*/ |
||||
private function isPHPFunction(string $signature): bool |
||||
{ |
||||
$phpMacros = [ |
||||
'PHP_FUNCTION', |
||||
'PHP_METHOD', |
||||
'ZEND_FUNCTION', |
||||
'ZEND_METHOD', |
||||
]; |
||||
|
||||
foreach ($phpMacros as $macro) { |
||||
if (strpos($signature, $macro) !== false) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* 解析文档注释标签 |
||||
*/ |
||||
private function parseDocTags(array $comments): array |
||||
{ |
||||
$tags = []; |
||||
|
||||
foreach ($comments as $comment) { |
||||
// 匹配 @tag 格式 |
||||
if (preg_match_all('/@(\w+)\s+(.*)$/m', $comment, $matches, PREG_SET_ORDER)) { |
||||
foreach ($matches as $match) { |
||||
$tagName = $match[1]; |
||||
$tagValue = trim($match[2]); |
||||
|
||||
if (!isset($tags[$tagName])) { |
||||
$tags[$tagName] = []; |
||||
} |
||||
|
||||
$tags[$tagName][] = $tagValue; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return $tags; |
||||
} |
||||
|
||||
/** |
||||
* 生成函数统计信息 |
||||
*/ |
||||
public function generateStatistics(array $functions): array |
||||
{ |
||||
$stats = [ |
||||
'total' => count($functions), |
||||
'byReturnType' => [], |
||||
'byParameterCount' => [], |
||||
'byPrefix' => [], |
||||
'withComments' => 0, |
||||
'isPHPFunction' => 0, |
||||
]; |
||||
|
||||
foreach ($functions as $func) { |
||||
// 按返回类型统计 |
||||
$returnType = $func['returnType']; |
||||
$stats['byReturnType'][$returnType] = |
||||
($stats['byReturnType'][$returnType] ?? 0) + 1; |
||||
|
||||
// 按参数数量统计 |
||||
$paramCount = count($func['parameters']); |
||||
$stats['byParameterCount'][$paramCount] = |
||||
($stats['byParameterCount'][$paramCount] ?? 0) + 1; |
||||
|
||||
// 按前缀统计 |
||||
$name = $func['name']; |
||||
$prefix = preg_match('/^([a-z_]+_)/i', $name, $m) ? $m[1] : 'other'; |
||||
$stats['byPrefix'][$prefix] = |
||||
($stats['byPrefix'][$prefix] ?? 0) + 1; |
||||
|
||||
// 有注释的函数 |
||||
if (!empty($func['metadata']['comments'])) { |
||||
$stats['withComments']++; |
||||
} |
||||
|
||||
// PHP 函数宏 |
||||
if ($func['metadata']['isPHPFunction'] ?? false) { |
||||
$stats['isPHPFunction']++; |
||||
} |
||||
} |
||||
|
||||
return $stats; |
||||
} |
||||
|
||||
/** |
||||
* 导出为 Markdown 文档 |
||||
*/ |
||||
public function exportToMarkdown(array $functions, string $title = 'API 文档'): string |
||||
{ |
||||
$md = "# {$title}\n\n"; |
||||
$md .= "生成时间: " . date('Y-m-d H:i:s') . "\n\n"; |
||||
$md .= "总计: " . count($functions) . " 个函数\n\n"; |
||||
$md .= "---\n\n"; |
||||
|
||||
foreach ($functions as $func) { |
||||
$md .= "## {$func['name']}\n\n"; |
||||
|
||||
// 签名 |
||||
$md .= "```c\n{$func['signature']}\n```\n\n"; |
||||
|
||||
// 返回类型 |
||||
$md .= "**返回类型**: `{$func['returnType']}`\n\n"; |
||||
|
||||
// 参数 |
||||
if (!empty($func['parameters'])) { |
||||
$md .= "**参数**:\n\n"; |
||||
foreach ($func['parameters'] as $param) { |
||||
$name = $param['name'] ?: '(unnamed)'; |
||||
$md .= "- `{$param['type']}` **{$name}**\n"; |
||||
} |
||||
$md .= "\n"; |
||||
} else { |
||||
$md .= "**参数**: 无\n\n"; |
||||
} |
||||
|
||||
// 注释 |
||||
if (!empty($func['metadata']['comments'])) { |
||||
$md .= "**说明**:\n\n"; |
||||
foreach ($func['metadata']['comments'] as $comment) { |
||||
$md .= $comment . "\n\n"; |
||||
} |
||||
} |
||||
|
||||
// 位置 |
||||
$md .= "**位置**: {$func['location']['file']}:{$func['location']['line']}\n\n"; |
||||
|
||||
$md .= "---\n\n"; |
||||
} |
||||
|
||||
return $md; |
||||
} |
||||
} |
||||
@ -0,0 +1,63 @@ |
||||
<?php |
||||
|
||||
namespace PhpAot\Php; |
||||
|
||||
use FilesystemIterator; |
||||
|
||||
class FileScanner |
||||
{ |
||||
private string $directory; |
||||
private array $extensions; |
||||
private array $excludePatterns; |
||||
|
||||
public function __construct(string $directory, array $extensions = ['.php', '.cc', '.cpp', '.cxx', '.c', '.h', '.hpp']) |
||||
{ |
||||
if (!is_dir($directory)) { |
||||
throw new \InvalidArgumentException("Directory does not exist: $directory"); |
||||
} |
||||
|
||||
$this->directory = rtrim($directory, DIRECTORY_SEPARATOR); |
||||
$this->extensions = $extensions; |
||||
$this->excludePatterns = []; |
||||
} |
||||
|
||||
public function addExcludePattern(string $pattern): self |
||||
{ |
||||
$this->excludePatterns[] = $pattern; |
||||
return $this; |
||||
} |
||||
|
||||
public function scan(): array |
||||
{ |
||||
$files = []; |
||||
$iterator = new \RecursiveIteratorIterator( |
||||
new \RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS) |
||||
); |
||||
|
||||
foreach ($iterator as $file) { |
||||
if ($file->isFile()) { |
||||
$extension = '.' . $file->getExtension(); |
||||
if (in_array($extension, $this->extensions)) { |
||||
$filePath = $file->getPathname(); |
||||
$excluded = false; |
||||
foreach ($this->excludePatterns as $pattern) { |
||||
if ($this->matchPattern($pattern, $filePath)) { |
||||
$excluded = true; |
||||
break; |
||||
} |
||||
} |
||||
if (!$excluded) { |
||||
$files[] = $filePath; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
sort($files); |
||||
return $files; |
||||
} |
||||
|
||||
private function matchPattern(string $pattern, string $path): bool |
||||
{ |
||||
return fnmatch($pattern, $path, FNM_PATHNAME); |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
<?php |
||||
|
||||
namespace PhpAot\Php; |
||||
|
||||
class Unsupported extends \RuntimeException |
||||
{ |
||||
|
||||
} |
||||
Loading…
Reference in new issue