如何从 clang 中的 AST 中排除 headers?
How to exclude headers from AST in clang?
我正在使用 clang 生成 AST。我有以下文件 (lambda.cpp) 需要解析:
#include <iostream>
void my_lambda()
{
auto lambda = [](auto x, auto y) {return x + y;};
std::cout << "fabricati diem";
}
我正在使用以下命令对此进行解析:
clang -Xclang -ast-dump -fsyntax-only lambda.cpp
问题是 clang 也解析 headers 内容。结果,我得到了相当大(约 3000 行)的文件,其中包含(对我而言)无用的内容。
如何在生成AST时排除headers?
这是 C++ 的问题,而不是 clang 的问题:C++ 中没有文件,只有编译单元。当你 #include
一个文件时,你将所述文件中的所有定义(递归地)包含到你的编译单元中,并且没有办法区分它们(这是标准期望你的编译器做的)。
想象一个不同的场景:
/////////////////////////////
// headertmp.h
#if defined(A)
struct Foo {
int bar;
};
#elif defined(B)
struct Foo {
short bar;
};
#endif
/////////////////////////////
// foobar.cpp
#ifndef A
# define B
#endif
#include "headertmp.h"
void foobar(Foo foo) {
// do stuff to foo.bar
}
您的 foobar.cpp 声明了一个名为 Foo
的结构和一个名为 foobar
的函数,但是 headertmp.h
本身没有定义任何 Foo
除非 A
或 B
被定义。只有在 foobar 的编译单元中,两者合在一起,你才能理解 headertmp.h
.
如果您对编译单元内的声明子集感兴趣,则必须直接从生成的 AST 中提取必要的信息(类似于链接器在将不同的编译单元链接在一起时必须执行的操作)。当然,您可以在解析器提取的任何元数据上过滤此编译单元的 AST。
clang-check
可能对此事有用,clang-check
有选项 -ast-dump-filter=<string>
记录如下
-ast-dump-filter=<string> - Use with -ast-dump or -ast-print to dump/print only AST declaration nodes having a certain substring in a
qualified name. Use -ast-list to list all filterable declaration node
names.
when clang-check
运行 with -ast-dump-filter=my_lambda
on the sample code (lambda.cpp)
#include <iostream>
void my_lambda()
{
auto lambda = [](auto x, auto y) {return x + y;};
std::cout << "fabricati diem";
}
它只转储匹配的声明节点FunctionDecl my_lambda 'void (void)'
这是命令行参数和输出的几行。
$ clang-check -extra-arg=-std=c++1y -ast-dump -ast-dump-filter=my_lambda lambda.cpp --
FunctionDecl 0x2ddf630 <lambda.cpp:3:1, line:7:1> line:3:6 my_lambda 'void (void)'
`-CompoundStmt 0x2de1558 <line:4:1, line:7:1>
|-DeclStmt 0x2de0960 <line:5:9, col:57>
可以使用 -ast-dump-filter
过滤特定标识符。但是如果你想从一个文件中的所有标识符进行ast怎么办?
我想到了以下解决方案:
在包含后添加一行可识别的行:
#include <iostream>
int XX_MARKER_XX = 123234; // marker line for ast-dump
void my_lambda()
...
然后用
转储ast
clang-check -extra-arg=-std=c++1y -ast-dump lambda.cpp > ast.txt
您可以使用 sed
轻松删除 XX_MARKER_XX
之前的所有内容:
cat ast.txt | sed -n '/XX_MARKER_XX/,$p' | less
仍然很多,但对于更大的文件更有用。
转储的 AST 具有每个节点的源文件的一些指示。所以转储的AST可以根据二级AST节点的loc
数据进行过滤
您需要将 loc
中的 file
和 loc
中的 expansionLoc
中的 file
与顶级文件的名称相匹配。这似乎对我很有效。由于某些原因,一些节点不包含这些元素。具有 isImplicit
的节点应该可以安全地跳过,但我不确定没有文件名信息的其他节点发生了什么。
以下 python 脚本使用这些规则过滤 'astdump.json' 到 'astdump.filtered.json'(以流方式进行转换留作 reader 的练习):
#! /usr/bin/python3
import json
import sys
if len(sys.argv) != 2:
print('Usage: ' + sys.argv[0] + ' filename')
sys.exit(1)
filename = sys.argv[1]
with open('astdump.json', 'rb') as input, open('astdump.filtered.json', 'w') as output:
toplevel = json.load(input)
new_inner = []
for o in toplevel['inner']:
if o.get('isImplicit', False):
continue
file_name = None
loc = o.get('loc', {})
if 'file' in loc:
file_name = loc['file']
if 'expansionLoc' in loc:
if 'file' in loc['expansionLoc']:
file_name = loc['expansionLoc']['file']
if file_name != filename:
continue
new_inner.append(o)
toplevel['inner'] = new_inner
json.dump(toplevel, output, indent=4)
我遇到了同样的问题。我的上下文是我需要以 JSON 格式解析 AST,并且我想删除所有 headers 和不必要的文件。我试图复制@textshell 答案 (https://whosebug.com/a/69150479/3267980) 但我注意到 CLANG 在我的情况下表现不同。我使用的 CLANG 版本是:
$ clang --version
Debian clang version 13.0.1-+rc1-1~exp4
Target: x86_64-pc-linux-gnu
Thread model: posix
为了解释我的情况,让我们考虑以下示例:
my_function
和 main
都是来自同一源文件的函数 (function_definition_invocation.c)。但是,它仅在my_function
的FunctionDecl
节点中指定。我认为这种行为是由于两个函数属于同一个文件,CLANG 仅在属于它的节点中打印文件位置。
一旦找到主文件的第一个匹配项,每个连续的节点都应添加到结果过滤后的 JSON 文件中。我使用的代码是:
def filter_ast_only_source_file(source_file, json_ast):
new_inner = []
first_occurrence_of_main_file = False
for entry in json_ast['inner']:
if not first_occurrence_of_main_file:
if entry.get('isImplicit', False):
continue
file_name = None
loc = entry.get('loc', {})
if 'file' in loc:
file_name = loc['file']
if 'expansionLoc' in loc:
if 'file' in loc['expansionLoc']:
file_name = loc['expansionLoc']['file']
if file_name != source_file:
continue
new_inner.append(entry)
first_occurrence_of_main_file = True
else:
new_inner.append(entry)
json_ast['inner'] = new_inner
我这样称呼它:
generated_ast = subprocess.run(["clang", "-Xclang", "-ast-dump=json", source_file], capture_output=True) # Output is in bytes. In case it's needed, decode it to get string
# Parse the output into a JSON object
json_ast = json.loads(generated_ast.stdout)
filter_ast_only_source_file(source_file, json_ast)
到目前为止它似乎在工作。
我正在使用 clang 生成 AST。我有以下文件 (lambda.cpp) 需要解析:
#include <iostream>
void my_lambda()
{
auto lambda = [](auto x, auto y) {return x + y;};
std::cout << "fabricati diem";
}
我正在使用以下命令对此进行解析:
clang -Xclang -ast-dump -fsyntax-only lambda.cpp
问题是 clang 也解析 headers 内容。结果,我得到了相当大(约 3000 行)的文件,其中包含(对我而言)无用的内容。
如何在生成AST时排除headers?
这是 C++ 的问题,而不是 clang 的问题:C++ 中没有文件,只有编译单元。当你 #include
一个文件时,你将所述文件中的所有定义(递归地)包含到你的编译单元中,并且没有办法区分它们(这是标准期望你的编译器做的)。
想象一个不同的场景:
/////////////////////////////
// headertmp.h
#if defined(A)
struct Foo {
int bar;
};
#elif defined(B)
struct Foo {
short bar;
};
#endif
/////////////////////////////
// foobar.cpp
#ifndef A
# define B
#endif
#include "headertmp.h"
void foobar(Foo foo) {
// do stuff to foo.bar
}
您的 foobar.cpp 声明了一个名为 Foo
的结构和一个名为 foobar
的函数,但是 headertmp.h
本身没有定义任何 Foo
除非 A
或 B
被定义。只有在 foobar 的编译单元中,两者合在一起,你才能理解 headertmp.h
.
如果您对编译单元内的声明子集感兴趣,则必须直接从生成的 AST 中提取必要的信息(类似于链接器在将不同的编译单元链接在一起时必须执行的操作)。当然,您可以在解析器提取的任何元数据上过滤此编译单元的 AST。
clang-check
可能对此事有用,clang-check
有选项 -ast-dump-filter=<string>
记录如下
-ast-dump-filter=<string> - Use with -ast-dump or -ast-print to dump/print only AST declaration nodes having a certain substring in a qualified name. Use -ast-list to list all filterable declaration node names.
when clang-check
运行 with -ast-dump-filter=my_lambda
on the sample code (lambda.cpp)
#include <iostream>
void my_lambda()
{
auto lambda = [](auto x, auto y) {return x + y;};
std::cout << "fabricati diem";
}
它只转储匹配的声明节点FunctionDecl my_lambda 'void (void)'
这是命令行参数和输出的几行。
$ clang-check -extra-arg=-std=c++1y -ast-dump -ast-dump-filter=my_lambda lambda.cpp --
FunctionDecl 0x2ddf630 <lambda.cpp:3:1, line:7:1> line:3:6 my_lambda 'void (void)'
`-CompoundStmt 0x2de1558 <line:4:1, line:7:1>
|-DeclStmt 0x2de0960 <line:5:9, col:57>
可以使用 -ast-dump-filter
过滤特定标识符。但是如果你想从一个文件中的所有标识符进行ast怎么办?
我想到了以下解决方案:
在包含后添加一行可识别的行:
#include <iostream>
int XX_MARKER_XX = 123234; // marker line for ast-dump
void my_lambda()
...
然后用
转储astclang-check -extra-arg=-std=c++1y -ast-dump lambda.cpp > ast.txt
您可以使用 sed
轻松删除 XX_MARKER_XX
之前的所有内容:
cat ast.txt | sed -n '/XX_MARKER_XX/,$p' | less
仍然很多,但对于更大的文件更有用。
转储的 AST 具有每个节点的源文件的一些指示。所以转储的AST可以根据二级AST节点的loc
数据进行过滤
您需要将 loc
中的 file
和 loc
中的 expansionLoc
中的 file
与顶级文件的名称相匹配。这似乎对我很有效。由于某些原因,一些节点不包含这些元素。具有 isImplicit
的节点应该可以安全地跳过,但我不确定没有文件名信息的其他节点发生了什么。
以下 python 脚本使用这些规则过滤 'astdump.json' 到 'astdump.filtered.json'(以流方式进行转换留作 reader 的练习):
#! /usr/bin/python3
import json
import sys
if len(sys.argv) != 2:
print('Usage: ' + sys.argv[0] + ' filename')
sys.exit(1)
filename = sys.argv[1]
with open('astdump.json', 'rb') as input, open('astdump.filtered.json', 'w') as output:
toplevel = json.load(input)
new_inner = []
for o in toplevel['inner']:
if o.get('isImplicit', False):
continue
file_name = None
loc = o.get('loc', {})
if 'file' in loc:
file_name = loc['file']
if 'expansionLoc' in loc:
if 'file' in loc['expansionLoc']:
file_name = loc['expansionLoc']['file']
if file_name != filename:
continue
new_inner.append(o)
toplevel['inner'] = new_inner
json.dump(toplevel, output, indent=4)
我遇到了同样的问题。我的上下文是我需要以 JSON 格式解析 AST,并且我想删除所有 headers 和不必要的文件。我试图复制@textshell 答案 (https://whosebug.com/a/69150479/3267980) 但我注意到 CLANG 在我的情况下表现不同。我使用的 CLANG 版本是:
$ clang --version
Debian clang version 13.0.1-+rc1-1~exp4
Target: x86_64-pc-linux-gnu
Thread model: posix
为了解释我的情况,让我们考虑以下示例:
my_function
和 main
都是来自同一源文件的函数 (function_definition_invocation.c)。但是,它仅在my_function
的FunctionDecl
节点中指定。我认为这种行为是由于两个函数属于同一个文件,CLANG 仅在属于它的节点中打印文件位置。
一旦找到主文件的第一个匹配项,每个连续的节点都应添加到结果过滤后的 JSON 文件中。我使用的代码是:
def filter_ast_only_source_file(source_file, json_ast):
new_inner = []
first_occurrence_of_main_file = False
for entry in json_ast['inner']:
if not first_occurrence_of_main_file:
if entry.get('isImplicit', False):
continue
file_name = None
loc = entry.get('loc', {})
if 'file' in loc:
file_name = loc['file']
if 'expansionLoc' in loc:
if 'file' in loc['expansionLoc']:
file_name = loc['expansionLoc']['file']
if file_name != source_file:
continue
new_inner.append(entry)
first_occurrence_of_main_file = True
else:
new_inner.append(entry)
json_ast['inner'] = new_inner
我这样称呼它:
generated_ast = subprocess.run(["clang", "-Xclang", "-ast-dump=json", source_file], capture_output=True) # Output is in bytes. In case it's needed, decode it to get string
# Parse the output into a JSON object
json_ast = json.loads(generated_ast.stdout)
filter_ast_only_source_file(source_file, json_ast)
到目前为止它似乎在工作。