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