如何在clang中从AST中排除头文件?

24

我正在使用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还会解析头文件内容。因此,我得到了一个相当大的(约3000行)文件,其中包含我不需要的内容。

如何在生成AST时排除头文件?


当clang需要从头文件中获取名称/定义等以生成源文件的AST时,您希望它执行什么操作? - Mark B
1
@MarkB,我表达得可能不太清楚。我希望clang在解析时使用头文件,但仅显示我的文件的AST - 而不是来自头文件的AST。 - Kao
Header_guards不是可以完成这个工作吗? - dhein
你真正想做什么?我有一点使用clang python后端的经验,并且通过查看ast我知道你正在经历的痛苦。有几个选项可以将你想要的东西拼接在一起。如果我没记错的话,你可以从clang获取行号信息并忽略之前的所有内容。 - Alexander Oh
5个回答

19
clang-check可能会对此有帮助,clang-check有一个选项-ast-dump-filter=<string>,文档如下所示。当在示例代码(lambda.cpp)上运行clang-check并使用-ast-dump-filter=my_lambda时,只会输出AST声明节点中包含特定子字符串("my_lambda")的部分。
#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>

1
clang-check 看起来很不错! - BeyelerStudios
@alper,你知道是否有一种方法可以获取所有节点而不必导入要获取AST的类的所有依赖项吗?我想能够获取类的语法节点,而不必解析依赖项(传递框架路径),以便我可以以特定方式重写相同的类,我是否在寻找实现此目标的正确方法?任何评论都将不胜感激! - Andrespch
但是clang-check无法输出为json格式... - fatfatson

4

如果您想要从一个文件中获取所有标识符的ast,那么仅使用-ast-dump-filter筛选特定标识符是不够的。

我想到了以下解决方案:

在包含文件后添加一行可识别的代码:

#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

仍然有很多,但在处理更大的文件时更加有用。


根据我的需求,我不需要初始化XX_MARKER_XX并将命令行缩短为clang++ -Xclang -ast-dump minimal.cpp | sed -n '/XX_MARKER_XX/,$p' - Nestor Demeure
是的,但如果你试图在别人的资源上这样做就不行了 :q - SasQ

3
我遇到了同样的问题。我的情境是需要以JSON格式解析AST,我想摆脱所有的头文件和不必要的文件。我尝试复制@textshell的答案(https://dev59.com/rF4c5IYBdhLWcg3wVo9I#69150479),但我注意到CLANG在我的情况下表现不同。我正在使用的CLANG版本是:
$ clang --version                                             
Debian clang version 13.0.1-+rc1-1~exp4
Target: x86_64-pc-linux-gnu
Thread model: posix

为了解释我的情况,让我们考虑以下例子:

enter image description here

“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)

到目前为止,它似乎正在起作用。

1
这是一个关于C++的问题,而不是clang的问题:在C++中没有文件,只有编译单元。当你#include一个文件时,你会将该文件中的所有定义(递归地)包含到你的编译单元中,没有办法区分它们(这是标准希望你的编译器做的)。请注意保留html标签。

想象一个不同的场景:

/////////////////////////////
// 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,除非定义了 AB。只有在 foobar 的编译单元中,这两者才能一起理解 headertmp.h

如果你对编译单元中某些声明的子集感兴趣,你将不得不直接从生成的 AST 中提取必要的信息(类似于链接器在链接不同的编译单元时所做的)。当然,你可以根据解析器提取的任何元数据过滤此编译单元的 AST。


1
虽然在翻译单元的完整编译过程中,头文件中的代码和非头文件中的代码应该没有任何区别,但这个问题是关于限制AST提取的,因此没有理由为什么这不可能实现(正如其他答案所示)。 - Kyle Strand
@KyleStrand 请阅读我最后一段,它恰好表达了您评论的观点:您可以将提取/转储限制为编译单元的一部分,而不是排除特定的头文件(因为在AST中没有这样的概念)。 - BeyelerStudios
你的答案不仅仅是说头文件不能被跳过,而且说其中的定义与文件中直接的定义无法区分。这对于编译是正确的,但不一定对于原始AST提取是正确的。 - Kyle Strand
@KyleStrand 我不同意或者说我不理解你的观点:你能否在我的例子中展示一下,如何从仅有的头文件中提取AST呢?也就是说,在什么情况下需要进行预处理呢? - BeyelerStudios
1
你的意思是仅从非头文件中提取吗?简单来说,预处理器需要记录哪些行来自哪些文件(这是现代预处理器通常执行的操作),而AST转储工具只会从非头文件中转储定义。 - Kyle Strand
@KyleStrand 我现在明白了。感谢你为我解惑。 - BeyelerStudios

0
转换成中文:

转储的 AST 对每个节点都有源文件的一些指示。因此,可以基于第二级 AST 节点的 loc 数据过滤已转储的 AST。

您需要将locexpansionLoc中的file与顶层文件名进行匹配。这对我来说似乎工作得相当好。出于某种原因,一些节点不包含这些元素。带有isImplicit的节点应该是安全跳过的,但我不确定没有文件名信息的其他节点正在发生什么。

以下 Python 脚本使用这些规则(以流式方式进行转换留给读者作为练习),将'astdump.json'过滤到'astdump.filtered.json':

#! /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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接