用于模糊解析C++的叮当声

Clang for fuzzy parsing C++

本文关键字:叮当声 C++ 模糊 用于      更新时间:2023-10-16

是否可以使用其现有的libclang API使用clang解析不完整声明的C++? 即解析.cpp文件而不包含所有标头,即时推断声明。 因此,例如以下文本:

A B::Foo(){return stuff();}

将检测未知符号 A,调用我扣除 A 的回调是使用我的魔法启发式的类,然后用 B 和 Foo 之类的方式以相同的方式调用此回调。最后,我希望能够推断出我看到 B 类的成员 Foo 返回 A,而 stuff 是一个函数。或者类似的东西。上下文:我想看看我是否可以在不快速解析所有标头的情况下进行合理的语法突出显示和动态代码分析。

[编辑] 澄清一下,我正在寻找非常严格限制的解析C++,可能有一些启发式来解除一些限制。

C++语法充满了上下文依赖关系。Foo() 是函数调用还是 Foo 类的临时构造?是福<吧>的东西;一个模板 Foo变量内容的实例化和声明,还是对重载运算符<和运算符>的 2 次调用看起来很奇怪?只能在上下文中判断,上下文通常来自解析标头。

我正在寻找一种插入自定义约定规则的方法。 例如,我知道我不重载 Win32 符号,所以我可以放心地假设 CreateFile 始终是一个函数,我什至知道它的签名。我也知道我所有的类都以大写字母开头,是名词,函数通常是动词,所以我可以合理地猜测 Foo 和 Bar 是类名。在更复杂的场景中,我知道我不会写像 c 这样的无副作用表达式;所以我可以假设 a 始终是一个模板实例化。等等。

所以,问题是是否可以使用 Clang API 在每次遇到未知符号时回调,并使用我自己的非C++启发式方法给出答案。如果我的启发式失败,那么解析显然会失败。我不是在谈论解析 Boost 库:)我说的是非常简单的C++,可能没有模板,仅限于 clang 在这种情况下可以处理的最低限度。

我知道这个问题已经很老了,但请看这里:

LibFuzzy 是一个基于 Clang 的启发式解析C++库 雷克瑟。模糊解析器是容错的,无需了解即可工作 构建系统和不完整的源文件。作为解析器 必然会做出猜测,得到的语法树可能部分 错。

它是来自clang-highlight的子项目,一个(实验性?)工具,似乎不再开发。

我只对模糊解析部分感兴趣,并在我的 github 页面上分叉了它,在那里我修复了几个小问题并使该工具自治(它可以在 clang 的源代码树之外编译)。不要尝试用 C++14(G++ 6 的默认模式)编译它,因为会与 make_unique 发生冲突。

根据这个页面,clang格式有自己的模糊解析器(并且正在积极开发),但解析器与工具耦合得更紧密。

除非你严格限制允许人们编写的代码,否则基本上不可能在不解析所有标头的情况下很好地解析C++(以及语法突出显示关键字/正则表达式之外)。预处理器特别擅长为您搞砸事情。

这里有一些关于模糊解析(在可视化工作室的上下文中)的困难的想法,可能会感兴趣:http://blogs.msdn.com/b/vcblog/archive/2011/03/03/10136696.aspx

我认为比

模糊解析更适合OP的另一种解决方案。

解析时,clang 通过分析器的 Sema 部分维护语义信息。 当遇到未知符号时,Sema将回退到ExternalSemaSource以获取有关此符号的一些信息。通过这个,你可以实现你想要的。

下面是如何设置它的快速示例。它不是完全起作用的(我没有在 LookupUnqualified 方法中做任何事情),您可能需要进行进一步的调查,我认为这是一个好的开始。

// Declares clang::SyntaxOnlyAction.
#include <clang/Frontend/FrontendActions.h>
#include <clang/Tooling/CommonOptionsParser.h>
#include <clang/Tooling/Tooling.h>
#include <llvm/Support/CommandLine.h>
#include <clang/AST/AST.h>
#include <clang/AST/ASTConsumer.h>
#include <clang/AST/RecursiveASTVisitor.h>
#include <clang/Frontend/ASTConsumers.h>
#include <clang/Frontend/FrontendActions.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Tooling/CommonOptionsParser.h>
#include <clang/Tooling/Tooling.h>
#include <clang/Rewrite/Core/Rewriter.h>
#include <llvm/Support/raw_ostream.h>
#include <clang/Sema/ExternalSemaSource.h>
#include <clang/Sema/Sema.h>
#include "clang/Basic/DiagnosticOptions.h"
#include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/TargetOptions.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Parse/Parser.h"
#include "clang/Parse/ParseAST.h"
#include <clang/Sema/Lookup.h>
#include <iostream>
using namespace clang;
using namespace clang::tooling;
using namespace llvm;
class ExampleVisitor : public RecursiveASTVisitor<ExampleVisitor> {
private:
  ASTContext *astContext;
public:
  explicit ExampleVisitor(CompilerInstance *CI, StringRef file)
      : astContext(&(CI->getASTContext())) {}
  virtual bool VisitVarDecl(VarDecl *d) {
    std::cout << d->getNameAsString() << "@n";
    return true;
  }
};
class ExampleASTConsumer : public ASTConsumer {
private:
  ExampleVisitor visitor;
public:
  explicit ExampleASTConsumer(CompilerInstance *CI, StringRef file)
      : visitor(CI, file) {}
  virtual void HandleTranslationUnit(ASTContext &Context) {
    // de cette façon, on applique le visiteur sur l'ensemble de la translation
    // unit
    visitor.TraverseDecl(Context.getTranslationUnitDecl());
  }
};
class DynamicIDHandler : public clang::ExternalSemaSource {
public:
  DynamicIDHandler(clang::Sema *Sema)
      : m_Sema(Sema), m_Context(Sema->getASTContext()) {}
  ~DynamicIDHandler() = default;
  /// brief Provides last resort lookup for failed unqualified lookups
  ///
  /// If there is failed lookup, tell sema to create an artificial declaration
  /// which is of dependent type. So the lookup result is marked as dependent
  /// and the diagnostics are suppressed. After that is's an interpreter's
  /// responsibility to fix all these fake declarations and lookups.
  /// It is done by the DynamicExprTransformer.
  ///
  /// @param[out] R The recovered symbol.
  /// @param[in] S The scope in which the lookup failed.
  virtual bool LookupUnqualified(clang::LookupResult &R, clang::Scope *S) {
     DeclarationName Name = R.getLookupName();
     std::cout << Name.getAsString() << "n";
    // IdentifierInfo *II = Name.getAsIdentifierInfo();
    // SourceLocation Loc = R.getNameLoc();
    // VarDecl *Result =
    //     // VarDecl::Create(m_Context, R.getSema().getFunctionLevelDeclContext(),
    //     //                 Loc, Loc, II, m_Context.DependentTy,
    //     //                 /*TypeSourceInfo*/ 0, SC_None, SC_None);
    // if (Result) {
    //   R.addDecl(Result);
    //   // Say that we can handle the situation. Clang should try to recover
    //   return true;
    // } else{
    //   return false;
    // }
    return false;
  }
private:
  clang::Sema *m_Sema;
  clang::ASTContext &m_Context;
};
// *****************************************************************************/
LangOptions getFormattingLangOpts(bool Cpp03 = false) {
  LangOptions LangOpts;
  LangOpts.CPlusPlus = 1;
  LangOpts.CPlusPlus11 = Cpp03 ? 0 : 1;
  LangOpts.CPlusPlus14 = Cpp03 ? 0 : 1;
  LangOpts.LineComment = 1;
  LangOpts.Bool = 1;
  LangOpts.ObjC1 = 1;
  LangOpts.ObjC2 = 1;
  return LangOpts;
}
int main() {
  using clang::CompilerInstance;
  using clang::TargetOptions;
  using clang::TargetInfo;
  using clang::FileEntry;
  using clang::Token;
  using clang::ASTContext;
  using clang::ASTConsumer;
  using clang::Parser;
  using clang::DiagnosticOptions;
  using clang::TextDiagnosticPrinter;
  CompilerInstance ci;
  ci.getLangOpts() = getFormattingLangOpts(false);
  DiagnosticOptions diagnosticOptions;
  ci.createDiagnostics();
  std::shared_ptr<clang::TargetOptions> pto = std::make_shared<clang::TargetOptions>();
  pto->Triple = llvm::sys::getDefaultTargetTriple();
  TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), pto);
  ci.setTarget(pti);
  ci.createFileManager();
  ci.createSourceManager(ci.getFileManager());
  ci.createPreprocessor(clang::TU_Complete);
  ci.getPreprocessorOpts().UsePredefines = false;
  ci.createASTContext();
  ci.setASTConsumer(
      llvm::make_unique<ExampleASTConsumer>(&ci, "../src/test.cpp"));
  ci.createSema(TU_Complete, nullptr);
  auto &sema = ci.getSema();
  sema.Initialize();
  DynamicIDHandler handler(&sema);
  sema.addExternalSource(&handler);
  const FileEntry *pFile = ci.getFileManager().getFile("../src/test.cpp");
  ci.getSourceManager().setMainFileID(ci.getSourceManager().createFileID(
      pFile, clang::SourceLocation(), clang::SrcMgr::C_User));
  ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(),
                                           &ci.getPreprocessor());
  clang::ParseAST(sema,true,false);
  ci.getDiagnosticClient().EndSourceFile();
  return 0;
}

这个想法和 DynamicIDHandler 类来自 cling 项目,其中未知符号是可变的(因此注释和代码)。

OP

不想要"模糊解析"。 他想要的是完全独立于上下文的C++源代码解析,不需要任何名称和类型解析。他计划根据解析结果对类型进行有根据的猜测。

Clang 适当的纠结解析和名称/类型解析,这意味着它在解析时必须具有所有可用的背景类型信息。 其他答案建议使用产生不正确解析树的 LibFuzzy,以及一些我一无所知的 clang 格式模糊解析器。 如果坚持生成经典的 AST,那么面对模棱两可的解析,这些解决方案都不会产生"正确"的树。

我们的DMS软件再造工具包及其C++前端可以在没有类型信息的情况下解析C++源代码,并生成准确的"AST";这些实际上是抽象的语法dag,其中树中的分支根据语言精确的语法(歧义(子)解析)表示对源代码的不同可能解释。

Clang 试图做的是在解析时使用类型信息来避免产生这些多个子解析。 DMS所做的是产生模糊的解析,并在(可选)解析后(属性语法评估)传递中,收集符号表信息并消除与类型不一致的子解析;对于格式良好的程序,这将生成一个没有歧义的普通 AST。

如果 OP 想要对类型信息进行启发式猜测,他需要知道这些可能的解释。 如果提前淘汰,他无法直接猜测可能需要什么类型。 一个有趣的可能性是修改属性语法(作为 DMS C++前端的一部分以源代码形式提供)的想法,该语法已经知道C++所有类型规则,以使用部分信息执行此操作。 这将是从头开始构建启发式分析器的巨大开端,因为它必须从标准中了解大约 600 页晦涩的名称和类型解析规则。

您可以看到DMS解析器生成的(dag)示例。