如何从 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 除非 AB 被定义。只有在 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 中的 fileloc 中的 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_functionmain 都是来自同一源文件的函数 (function_definition_invocation.c)。但是,它仅在my_functionFunctionDecl 节点中指定。我认为这种行为是由于两个函数属于同一个文件,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)

到目前为止它似乎在工作。