Clang用于模糊解析C++。

24

使用clang及其现有的libclang API,是否有可能解析具有不完整声明的C++代码?也就是说,可以在不包含所有头文件的情况下解析.cpp文件,并动态推断声明。例如,以下文本:

A B::Foo(){return stuff();}
将检测未知符号A,调用我的回调函数,使用我的魔法启发式推断出A是一个类,然后以与B、Foo和stuff相同的方式调用此回调函数。最终,我想能够推断出我看到了类B的成员Foo返回A,而stuff是一个函数。或者类似的东西。 上下文:我想看看是否可以在不解析所有头文件的情况下进行合理的语法高亮和即时代码分析。

[编辑]为了澄清,我正在寻找非常受限制的C++解析方法,可能带有一些启发式方法来解除一些限制。

C++语法充满上下文依赖性。Foo()是函数调用还是类Foo的临时构造?Foo<Bar>stuff;是模板Foo<Bar>实例化和变量stuff的声明,还是奇怪的两个重载运算符<和>的调用?只有在上下文中才能确定,而上下文通常来自解析头文件。

我正在寻找一种插入自定义约定规则的方法。例如,我知道我没有重载Win32符号,因此可以安全地假定CreateFile始终是一个函数,并且我甚至知道它的签名。我还知道我的所有类都以大写字母开头并且是名词,函数通常是动词,因此我可以合理地猜测Foo和Bar是类名。在更复杂的场景中,我知道我不编写没有副作用的表达式,例如a < b > c;,所以我可以假定a始终是模板实例化。等等。

因此,问题是是否可能使用Clang API每次遇到未知符号时都回调它,并使用自己的非C++启发式方法给出答案。如果我的启发式方法失败,则解析失败,显然。我不是在谈论解析Boost库:)我正在谈论非常简单的C ++,可能没有模板,限制为clang在这种情况下可以处理的最小值。


你总是可以直接修改 CLang。但是,由于查找可能在许多情况下合法地导致未找到任何内容(例如依赖上下文、ADL),我不确定这样做有多容易。 - Richard Corden
你是否一定需要使用clang?如果不是,也许尝试其他解决方案会更有意义?它们可能会表现得更好。 - Kirill Kobelev
是的,我看了一下antlr,虽然可行,但我怀疑它会更难以实现且性能较差。事实上,我正在使用antlr解析一些有限的c++代码,所以它对我来说很熟悉。还有其他真正的替代方案吗? - Sergiy Migdalskiy
是的,修改clang始终是一个选择,尽管可能非常困难,除非有人之前已经做过。 - Sergiy Migdalskiy
所以我现在开始在与Antlr相同的代码上使用Clang,对于那些可以使用启发式方法跳过大量代码而不进行任何分析的情况,Antlr可能更快。但是,当我可以使用PCH并需要分析所有内容时,Clang绝对更快。 - Sergiy Migdalskiy
4个回答

6

我知道这个问题很旧了,但是看看这里

LibFuzzy是一个基于Clang Lexer的启发式解析C++的库。模糊解析器是容错的,可以在没有构建系统以及不完整的源文件上工作。由于解析器必须做出猜测,因此生成的语法树可能部分错误。

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

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

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


5
除非你严格限制人们编写的代码,否则解析C++(因此除了关键字/正则表达式之外的语法突出显示)并不容易。 预处理器特别擅长为您搞砸事情。
在这里,有一些关于模糊解析的困难思考(在Visual Studio的上下文中),可能会引起兴趣:http://blogs.msdn.com/b/vcblog/archive/2011/03/03/10136696.aspx

1
是的,我正在寻找非常严格限制的C++解析,可能带有一些启发式方法来解除某些限制。C++语法充满了上下文依赖性。我想知道是否有可能实现。 - Sergiy Migdalskiy
我在我的问题中添加了一个澄清。 感谢您的回答! - Sergiy Migdalskiy

4

另一个我认为比模糊解析更适合 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 项目,其中未知符号为变量(因此有注释和代码)。


1
OP不想要“模糊解析”。他希望对C++源代码进行完全的上下文无关解析,而无需进行名称和类型解析。
他计划根据解析结果对类型进行有根据的猜测。
Clang proper混淆了解析和名称/类型解析,这意味着它在解析时必须拥有所有背景类型信息。其他答案建议使用LibFuzzy生成不正确的解析树,以及一些我不知道的clang-format模糊解析器。如果坚持生成经典AST,则这些解决方案都无法在面对模糊解析时产生“正确”的树。
我们的DMS软件重构工具包及其C++前端可以解析C++源代码{{link1:不需要类型信息}},并生成准确的“ASTs”;实际上这些是抽象语法dag,其中树中的分叉表示根据语言精确语法的不同可能解释源代码的情况(模糊(子)解析)。
Clang试图通过在解析时使用类型信息来避免产生这些多个子解析。DMS会产生模棱两可的解析,并在(可选的)后解析(属性语法评估)过程中收集符号表信息并消除与类型不一致的子解析;对于格式正确的程序,这将产生一个没有歧义的平面AST。
如果OP想要对类型信息进行启发式猜测,他需要知道这些可能的解释。如果它们被提前消除了,他就不能直接猜测可能需要的类型。一个有趣的可能性是修改属性语法(作为DMS的C++前端的一部分以源代码形式提供),该语法已经知道所有C++类型规则,以便使用部分信息进行操作。相比从头开始构建启发式分析器,这将是一个巨大的优势,因为它必须了解标准中约600页晦涩的名称和类型解析规则。
你可以看到DMS解析器生成的(dag)的例子。

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