diff --git a/clang/include/clang/Tooling/Syntax/Tokens.h b/clang/include/clang/Tooling/Syntax/Tokens.h new file mode 100644 --- /dev/null +++ b/clang/include/clang/Tooling/Syntax/Tokens.h @@ -0,0 +1,257 @@ +//===- Tokens.h - collect tokens from preprocessing ---------------*- -*-=====// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// Record tokens that a preprocessor emits and define operations to map between +// the tokens written in a file and tokens produced by the preprocessor. +// +// When running the compiler, there are two token streams we are interested in: +// - "raw" tokens directly correspond to a substring written in some source +// file. +// - "expanded" tokens represent the result of preprocessing, parses consumes +// this token stream to produce the AST. +// +// Expanded tokens correspond directly to locations found in the AST, allowing +// to find subranges of the token stream covered by various AST nodes. Raw +// tokens correspond directly to the source code written by the user. +// +// To allow composing these two use-cases, we also define operations that map +// between expanded and raw tokens that produced them (macro calls, directives, +// etc). +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLING_SYNTAX_TOKENS_H +#define LLVM_CLANG_TOOLING_SYNTAX_TOKENS_H + +#include "clang/Basic/FileManager.h" +#include "clang/Basic/LangOptions.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/TokenKinds.h" +#include "clang/Lex/Token.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Compiler.h" +#include "llvm/Support/raw_ostream.h" +#include +#include + +namespace clang { +class Preprocessor; + +namespace syntax { +class TokenBuffer; + +/// A token coming directly from a file or from a macro invocation. Has just +/// enough information to locate the token in the source code. +/// Used to represent both expanded and raw tokens. +class Token { +public: + Token(SourceLocation Location, unsigned Length, tok::TokenKind Kind) + : Location(Location), Length(Length), Kind(Kind) {} + /// EXPECTS: clang::Token is not an annotation token. + explicit Token(const clang::Token &T); + + tok::TokenKind kind() const { return Kind; } + SourceLocation location() const { return Location; } + SourceLocation endLocation() const { + return Location.getLocWithOffset(Length); + } + unsigned length() const { return Length; } + + /// Get the substring covered by the token. Note that will include all + /// digraphs, newline continuations, etc. E.g. tokens for 'int' and + /// in\ + /// t + /// both have the same kind tok::kw_int, but results of text() are different. + llvm::StringRef text(const SourceManager &SM) const; + + /// For debugging purposes. More verbose than the other overload, but requries + /// a source manager. + std::string str(const SourceManager &SM) const; + /// For debugging purposes. + std::string str() const; + +private: + SourceLocation Location; + unsigned Length; + tok::TokenKind Kind; +}; +/// For debugging purposes. Equivalent to a call to Token::str(). +llvm::raw_ostream& operator<<(llvm::raw_ostream &OS, const Token &T); + +/// A half-open range inside a particular file, the start offset is included and +/// the end offset is excluded from the range. +struct FileRange { + FileID File; + /// Start offset (inclusive) in a corresponding file. + unsigned Begin = 0; + /// End offset (exclusive) in a corresponding file. + unsigned End = 0; +}; +inline bool operator==(const FileRange &L, const FileRange &R) { + return std::tie(L.File, L.Begin, L.End) == std::tie(R.File, R.Begin, R.End); +} +inline bool operator!=(const FileRange &L, const FileRange &R) { + return !(L == R); +} +/// For debugging purposes. +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const FileRange &R); + +/// A list of tokens obtained by preprocessing a text buffer and operations to +/// map between the expanded and raw tokens, i.e. TokenBuffer has information +/// about two token streams: +/// 1. Expanded tokens: tokens produced by the preprocessor after all macro +/// replacements, +/// 2. Raw tokens: corresponding directly to the source code of a file before +/// any macro replacements occurred. +/// Here's an example to illustrate a difference between those two: +/// #define FOO 10 +/// int a = FOO; +/// +/// Raw tokens are {'#', 'define', 'FOO', '10', 'int', 'a', '=', 'FOO', ';'}. +/// Expanded tokens are {'int', 'a', '=', '10', ';'}. +/// +/// The full list expanded tokens can be obtained with expandedTokens(). Raw +/// tokens for each of the files can be obtained via rawTokens(FileID). +/// +/// To map between the expanded and raw token streams, see findRawByExpanded(). +/// +/// To build a token buffer use the TokenCollector class. You can also compute +/// the raw tokens of a file using the tokenize() helper. +class TokenBuffer { +public: + /// All tokens produced by the preprocessor after all macro replacements, + /// directives, etc. Source locations found in the clang AST will always + /// point to one of these tokens. + /// FIXME: the notable exception is '>>' being split into two '>'. figure out + /// how to deal with it. + llvm::ArrayRef expandedTokens() const { + return ExpandedTokens; + } + + /// Attempt to find the subrange of raw tokens that produced the corresponding + /// \p Expanded tokens. Will fail if the raw tokens cannot be determined + /// unambiguously. E.g. for the following example: + /// + /// #define FIRST f1 f2 f3 + /// #define SECOND s1 s2 s3 + /// + /// a FIRST b SECOND c // expanded tokens are: a f1 f2 f3 b s1 s2 s3 c + /// + /// the results would be: + /// expanded => raw + /// ------------------------ + /// a => a + /// s1 s2 s3 => SECOND + /// a f1 f2 f3 => a FIRST + /// a f1 => can't map + /// s1 s2 => can't map + /// + /// If \p Expanded is empty, the returned value is llvm::None. + /// Complexity is logarithmic. + llvm::Optional> + findRawByExpanded(llvm::ArrayRef Expanded, + const SourceManager &SM) const; + + /// Obtain the text offsets corresponding to the tokens returned by + /// findRawByExpanded. + llvm::Optional + findOffsetsByExpanded(llvm::ArrayRef Expanded, + const SourceManager &SM) const; + + /// Lexed tokens of a file before preprocessing. E.g. for the following input + /// #define DECL(name) int name = 10 + /// DECL(a); + /// rawTokens() returns {"#", "define", "DECL", "(", "name", ")"}. + /// FIXME: we do not yet store tokens of directives, like #include, #define, + /// #pragma, etc. + llvm::ArrayRef rawTokens(FileID FID) const; + + /// For debugging purposes. + void dump(llvm::raw_ostream &OS, const SourceManager &SM) const; + +private: + /// Describes a mapping between a continuous subrange of raw tokens and the + /// expanded tokens. Represents macro expansions, preprocessor directives, + /// conditionally disabled pp regions, etc. + /// #define FOO 1+2 + /// #define BAR(a) a + 1 + /// FOO // invocation #1, tokens = {'1','+','2'}, macroTokens = {'FOO'}. + /// BAR(1) // invocation #2, tokens = {'a', '+', '1'}, + /// macroTokens = {'BAR', '(', '1', ')'}. + struct Mapping { + // Positions in the corresponding raw token stream. The corresponding range + // is never empty. + unsigned BeginRawToken = 0; + unsigned EndRawToken = 0; + // Positions in the expanded token stream. The corresponding range can be + // empty. + unsigned BeginExpandedToken = 0; + unsigned EndExpandedToken = 0; + + /// For debugging purposes. + std::string str() const; + }; + /// Raw tokens of the file with information about the subranges. + struct MarkedFile { + /// Lexed, but not preprocessed, tokens of the file. These map directly to + /// text in the corresponding files and include tokens of all preprocessor + /// directives. + /// FIXME: raw tokens don't change across FileID that map to the same + /// FileEntry. We could consider deduplicating them to save memory. + std::vector RawTokens; + /// A sorted list to convert between the raw and expanded token streams. + std::vector Mappings; + }; + + friend class TokenCollector; + // Testing code has access to internal mapping. + friend class TokensTest; + + /// Token stream produced after preprocessing, conceputally this captures the + /// same stream as 'clang -E' (excluding the preprocessor directives like + /// #file, etc.). + std::vector ExpandedTokens; + llvm::DenseMap Files; +}; + +/// Lex the text buffer, corresponding to \p FID, in raw mode and record the +/// resulting tokens. Does minimal post-processing on raw identifiers, setting +/// the appropriate token kind (instead of the raw_identifier reported by lexer +/// in raw mode). This is a very low-level function, most users should prefer to +/// use TokenCollector. Lexing in raw mode produces wildly different results +/// from what one might expect when running a C++ frontend, e.g. preprocessor +/// does not run at all. +std::vector tokenize(FileID FID, const SourceManager &SM, + const LangOptions &LO); + +/// Collects tokens for the main file while running the frontend action. An +/// instance of this object should be created on +/// FrontendAction::BeginSourceFile() and the results should be consumed after +/// FrontendAction::Execute() finishes. +class TokenCollector { +public: + /// Adds the hooks to collect the tokens. Should be called before the + /// preprocessing starts, i.e. as a part of BeginSourceFile() or + /// CreateASTConsumer(). + TokenCollector(Preprocessor &P); + + /// Finalizes token collection. Should be called after preprocessing is + /// finished, i.e. after running Execute(). + LLVM_NODISCARD TokenBuffer consume() &&; + +private: + class Callbacks; + TokenBuffer Tokens; +}; + +} // namespace syntax +} // namespace clang + +#endif diff --git a/clang/lib/Tooling/CMakeLists.txt b/clang/lib/Tooling/CMakeLists.txt --- a/clang/lib/Tooling/CMakeLists.txt +++ b/clang/lib/Tooling/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(Inclusions) add_subdirectory(Refactoring) add_subdirectory(ASTDiff) +add_subdirectory(Syntax) add_clang_library(clangTooling AllTUsExecution.cpp diff --git a/clang/lib/Tooling/Syntax/CMakeLists.txt b/clang/lib/Tooling/Syntax/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/clang/lib/Tooling/Syntax/CMakeLists.txt @@ -0,0 +1,10 @@ +set(LLVM_LINK_COMPONENTS Support) + +add_clang_library(clangToolingSyntax + Tokens.cpp + + LINK_LIBS + clangBasic + clangFrontend + clangLex + ) diff --git a/clang/lib/Tooling/Syntax/Tokens.cpp b/clang/lib/Tooling/Syntax/Tokens.cpp new file mode 100644 --- /dev/null +++ b/clang/lib/Tooling/Syntax/Tokens.cpp @@ -0,0 +1,377 @@ +//===- Tokens.cpp - collect tokens from preprocessing -------------*- -*-=====// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "clang/Tooling/Syntax/Tokens.h" + +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/IdentifierTable.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/TokenKinds.def" +#include "clang/Basic/TokenKinds.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Lex/Token.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include + +using namespace clang; +using namespace clang::syntax; + +syntax::Token::Token(const clang::Token &T) + : Token(T.getLocation(), T.getLength(), T.getKind()) { + assert(!T.isAnnotation()); +} + +llvm::StringRef syntax::Token::text(const SourceManager &SM) const { + bool Invalid = false; + const char *Start = SM.getCharacterData(location(), &Invalid); + assert(!Invalid); + return llvm::StringRef(Start, length()); +} + +std::string syntax::Token::str() const { + return llvm::formatv("Token({0}, length = {1})", tok::getTokenName(kind()), + length()); +} + +std::string syntax::Token::str(const SourceManager &SM) const { + return llvm::formatv("Token({0}, length = {1}, location = {2}, text = {3})", + tok::getTokenName(kind()), length(), + location().printToString(SM), text(SM)); +} + +llvm::raw_ostream &syntax::operator<<(llvm::raw_ostream &OS, const Token &T) { + return OS << T.str(); +} + +llvm::raw_ostream &syntax::operator<<(llvm::raw_ostream &OS, + const FileRange &R) { + return OS << llvm::formatv("FileRange(file = {0}, offsets = {1}-{2})", + R.File.getHashValue(), R.Begin, R.End); +} + +void TokenBuffer::dump(llvm::raw_ostream &OS, const SourceManager &SM) const { + OS << "expanded tokens:\n"; + for (unsigned I = 0; I < ExpandedTokens.size(); ++I) + OS << llvm::formatv(" {0}: {1}\n", I, ExpandedTokens[I].str(SM)); + + std::vector Keys; + for (auto F : Files) + Keys.push_back(F.first); + llvm::sort(Keys); + + for (FileID ID : Keys) { + const MarkedFile &File = Files.find(ID)->second; + + auto *Entry = SM.getFileEntryForID(ID); + OS << " file " << (Entry ? Entry->getName() : "<>") << "\n"; + OS << " raw tokens:\n"; + for (unsigned I = 0; I < File.RawTokens.size(); ++I) + OS << llvm::formatv(" {0}: {1}\n", I, File.RawTokens[I].str(SM)); + OS << " mappings:\n"; + for (auto &M : File.Mappings) + OS << " " << M.str() << "\n"; + } +} + +llvm::ArrayRef TokenBuffer::rawTokens(FileID FID) const { + auto It = Files.find(FID); + assert(It != Files.end()); + return It->second.RawTokens; +} + +std::string TokenBuffer::Mapping::str() const { + return llvm::formatv("raw tokens: [{0},{1}), expanded " + "tokens: [{2},{3})", + BeginRawToken, EndRawToken, BeginExpandedToken, + EndExpandedToken); +} + +llvm::Optional +TokenBuffer::findOffsetsByExpanded(llvm::ArrayRef Expanded, + const SourceManager &SM) const { + auto Tokens = findRawByExpanded(Expanded, SM); + if (!Tokens) + return llvm::None; + assert(!Tokens->empty()); + + FileRange R; + std::tie(R.File, R.Begin) = SM.getDecomposedLoc(Tokens->front().location()); + R.End = SM.getFileOffset(Tokens->back().endLocation()); + return R; +} + +llvm::Optional> +TokenBuffer::findRawByExpanded(llvm::ArrayRef Expanded, + const SourceManager &SM) const { + // Mapping an empty range is not well-defined, bail out in that case. + if (Expanded.empty()) + return llvm::None; + + auto FileIt = + Files.find(SM.getFileID(SM.getExpansionLoc(Expanded.front().location()))); + assert(FileIt != Files.end() && "no file for an expanded token"); + // Crossing the file boundaries is not supported at the moment. + if (1 < Expanded.size() && + FileIt != Files.find(SM.getFileID( + SM.getExpansionLoc(Expanded.back().location())))) + return llvm::None; + const MarkedFile &File = FileIt->second; + + unsigned BeginIndex = Expanded.begin() - ExpandedTokens.data(); + unsigned EndIndex = Expanded.end() - ExpandedTokens.data(); + + // Find the first mapping that intersects with our range. + auto FirstCall = llvm::upper_bound(File.Mappings, BeginIndex, + [](unsigned BeginIndex, const Mapping &R) { + return BeginIndex < R.BeginExpandedToken; + }); + if (FirstCall != File.Mappings.begin()) { + --FirstCall; + if (FirstCall->EndExpandedToken <= BeginIndex) + FirstCall = File.Mappings.end(); + } else { + FirstCall = File.Mappings.end(); + } + // Find the last mapping that intersects with our range. + auto LastCall = llvm::lower_bound(File.Mappings, EndIndex, + [](const Mapping &L, unsigned EndIndex) { + return L.EndExpandedToken < EndIndex; + }); + if (LastCall != File.Mappings.end() && + EndIndex <= LastCall->BeginExpandedToken) + LastCall = File.Mappings.end(); + // A sanity-check. + assert(FirstCall == File.Mappings.end() || LastCall == File.Mappings.end() || + FirstCall <= LastCall); + + // Only allow changes that cover the mapping completely. + // FIXME: also allow changes uniquely mapping to macro arguments. + + // Check the first macro call is fully-covered. + if (FirstCall != File.Mappings.end() && + (FirstCall->BeginExpandedToken < BeginIndex || + EndIndex < FirstCall->EndExpandedToken)) { + return llvm::None; + } + // Check the last macro call is fully-covered. + if (LastCall != File.Mappings.end() && + (LastCall->BeginExpandedToken < BeginIndex || + EndIndex < LastCall->EndExpandedToken)) { + return llvm::None; + } + + const Token *BeginRaw; + if (FirstCall != File.Mappings.end()) { + BeginRaw = File.RawTokens.data() + FirstCall->BeginRawToken; + } else { + BeginRaw = &*llvm::lower_bound( + File.RawTokens, SM.getFileOffset(Expanded.front().location()), + [&](const syntax::Token &L, unsigned Offset) { + return SM.getFileOffset(L.location()) < Offset; + }); + } + + const Token *EndRaw; + if (LastCall != File.Mappings.end()) { + EndRaw = File.RawTokens.data() + LastCall->EndRawToken; + } else { + EndRaw = &*llvm::upper_bound( + File.RawTokens, SM.getFileOffset(Expanded.back().location()), + [&](unsigned Offset, const syntax::Token &R) { + return Offset < SM.getFileOffset(R.location()); + }); + } + return llvm::makeArrayRef(BeginRaw, EndRaw); +} + +std::vector syntax::tokenize(FileID FID, const SourceManager &SM, + const LangOptions &LO) { + std::vector Tokens; + IdentifierTable Identifiers(LO); + auto AddToken = [&](clang::Token T) { + // Fill the proper token kind for keywords, etc. + if (T.getKind() == tok::raw_identifier && !T.needsCleaning() && + !T.hasUCN()) { // FIXME: support needsCleaning and hasUCN cases. + clang::IdentifierInfo &II = Identifiers.get(T.getRawIdentifier()); + T.setIdentifierInfo(&II); + T.setKind(II.getTokenID()); + } + Tokens.push_back(syntax::Token(T)); + }; + + Lexer L(FID, SM.getBuffer(FID), SM, LO); + + clang::Token T; + while (!L.LexFromRawLexer(T)) + AddToken(T); + AddToken(T); + + return Tokens; +} + +/// Fills in the TokenBuffer by tracing the run of a preprocessor. The +/// implementation tracks the tokens, macro expansions and directives coming +/// from the preprocessor and: +/// - for each token, figures out if it is a part of an expanded token stream, +/// raw token stream or both. Stores the tokens appropriately. +/// - records mappings from the raw to expanded token ranges, e.g. for macro +/// expansions. +class TokenCollector::Callbacks : public PPCallbacks { +public: + Callbacks(const SourceManager &SM, TokenBuffer &Result) + : Result(Result), SM(SM) {} + + void FileChanged(SourceLocation Loc, FileChangeReason Reason, + SrcMgr::CharacteristicKind FileType, + FileID PrevFID) override { + assert(Loc.isFileID()); + File = &Result.Files.try_emplace(SM.getFileID(Loc)).first->second; + flushMacroExpansion(); + } + + void tokenLexed(const clang::Token &T, TokenSource S) { + if (S == TokenSource::Precached) + return; // the cached tokens are reported multiple times, we have already + // recorded these. + + auto L = T.getLocation(); + flushCurrentExpansion(L); + + if (ExpansionStart.isValid() && SM.getExpansionLoc(L) != ExpansionStart) { + // The token comes from intermediate replacements while processing macro + // arguments. These are not part of the expanded token and we only record + // the top-level macro expansions, so skip this token. + return; + } + + // 'eod' is a control token that we don't capture. + if (T.getKind() == tok::eod) + return; + + DEBUG_WITH_TYPE("collect-tokens", { + llvm::dbgs() << "$[token] " << syntax::Token(T).str(SM) << "\n"; + }); + + // Depending on where the token comes from, put it into an expanded token + // stream, a raw token stream, or both. + switch (S) { + case TokenSource::File: + assert(T.getLocation().isFileID()); + Result.ExpandedTokens.push_back(syntax::Token(T)); + File->RawTokens.push_back(syntax::Token(T)); + break; + case clang::TokenSource::MacroExpansion: + assert(T.getLocation().isMacroID()); + Result.ExpandedTokens.push_back(syntax::Token(T)); + break; + case clang::TokenSource::MacroNameOrArg: + case TokenSource::MacroDirective: + case TokenSource::SkippedPPBranch: + assert(T.getLocation().isFileID()); + File->RawTokens.push_back(syntax::Token(T)); + break; + case TokenSource::Precached: + llvm_unreachable("cached tokens should be handled before"); + case TokenSource::AfterModuleImport: + llvm_unreachable("not implemented yet"); + } + } + + void MacroExpands(const clang::Token &MacroNameTok, const MacroDefinition &MD, + SourceRange Range, const MacroArgs *Args) override { + auto MacroNameLoc = MacroNameTok.getLocation(); + flushCurrentExpansion(MacroNameLoc); + + // We do not record recursive invocations. + if (isMacroExpanding()) + return; + + // Find the first raw token of the macro invocation, i.e. the name of the + // macro. + auto InvocationStart = llvm::find_if( + llvm::reverse(File->RawTokens), + [&](const syntax::Token &T) { return T.location() == MacroNameLoc; }); + assert(InvocationStart != File->RawTokens.rend() && + "macro name must be recorded."); + + // Add a raw-to-expanded mapping for this macro invocation. + TokenBuffer::Mapping M; + M.BeginRawToken = + std::prev(InvocationStart.base()) - File->RawTokens.begin(); + M.EndRawToken = File->RawTokens.size(); + + M.BeginExpandedToken = Result.ExpandedTokens.size(); + // MI.EndExpandedToken is filled by flushCurrentExpansion() when macro + // expansion finishes. + + File->Mappings.push_back(M); + + // We have to record where invocation ends in order to track it properly. + std::tie(MacroInvocationFile, ExpansionEndOffset) = + SM.getDecomposedLoc(Range.getEnd()); + this->ExpansionStart = Range.getBegin(); + } + +private: + bool isMacroExpanding() const { return MacroInvocationFile.isValid(); } + + void flushMacroExpansion() { + if (!MacroInvocationFile.isValid()) + return; + assert(!File->Mappings.empty()); + assert(File->Mappings.back().EndExpandedToken == 0); + File->Mappings.back().EndExpandedToken = Result.ExpandedTokens.size(); + + MacroInvocationFile = FileID(); + ExpansionStart = SourceLocation(); + ExpansionEndOffset = 0; + } + + void flushCurrentExpansion(SourceLocation L) { + assert(L.isValid()); + if (!MacroInvocationFile.isValid()) + return; + FileID File; + unsigned Offset; + std::tie(File, Offset) = SM.getDecomposedLoc(L); + // Note that we always get a token inside the same file after macro + // expansion finishes (eof would be the last token) + if (File != MacroInvocationFile || Offset <= ExpansionEndOffset) + return; + // Check we are not inside the current macro arguments. + flushMacroExpansion(); + } + + TokenBuffer::MarkedFile *File = nullptr; + /// When valid, the file of the last active top-level macro invocation. + FileID MacroInvocationFile; + SourceLocation ExpansionStart; + unsigned ExpansionEndOffset = 0; + TokenBuffer &Result; + const SourceManager &SM; +}; + +TokenCollector::TokenCollector(Preprocessor &PP) { + auto CBOwner = llvm::make_unique(PP.getSourceManager(), Tokens); + auto *CB = CBOwner.get(); + + PP.addPPCallbacks(std::move(CBOwner)); + PP.setTokenWatcher( + [CB](const clang::Token &T, TokenSource S) { CB->tokenLexed(T, S); }); +} + +TokenBuffer TokenCollector::consume() && { return std::move(Tokens); } diff --git a/clang/unittests/Tooling/CMakeLists.txt b/clang/unittests/Tooling/CMakeLists.txt --- a/clang/unittests/Tooling/CMakeLists.txt +++ b/clang/unittests/Tooling/CMakeLists.txt @@ -69,3 +69,6 @@ clangToolingInclusions clangToolingRefactor ) + + +add_subdirectory(Syntax) diff --git a/clang/unittests/Tooling/Syntax/CMakeLists.txt b/clang/unittests/Tooling/Syntax/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/clang/unittests/Tooling/Syntax/CMakeLists.txt @@ -0,0 +1,20 @@ +set(LLVM_LINK_COMPONENTS + ${LLVM_TARGETS_TO_BUILD} + Support + ) + +add_clang_unittest(TokensTest + TokensTest.cpp +) + +target_link_libraries(TokensTest + PRIVATE + clangAST + clangBasic + clangFrontend + clangLex + clangSerialization + clangTooling + clangToolingSyntax + LLVMTestingSupport + ) diff --git a/clang/unittests/Tooling/Syntax/TokensTest.cpp b/clang/unittests/Tooling/Syntax/TokensTest.cpp new file mode 100644 --- /dev/null +++ b/clang/unittests/Tooling/Syntax/TokensTest.cpp @@ -0,0 +1,631 @@ +//===- TokensTest.cpp -----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Tooling/Syntax/Tokens.h" +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/Expr.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/DiagnosticIDs.h" +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/FileSystemOptions.h" +#include "clang/Basic/LLVM.h" +#include "clang/Basic/LangOptions.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/TokenKinds.def" +#include "clang/Basic/TokenKinds.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/FrontendAction.h" +#include "clang/Frontend/Utils.h" +#include "clang/Lex/Lexer.h" +#include "clang/Lex/PreprocessorOptions.h" +#include "clang/Lex/Token.h" +#include "clang/Tooling/Tooling.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/VirtualFileSystem.h" +#include "llvm/Support/raw_os_ostream.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Testing/Support/Annotations.h" +#include "gmock/gmock-more-matchers.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace clang; +using namespace clang::syntax; + +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::Matcher; +using ::testing::Pointwise; + +namespace { +// Matchers for syntax::Token. +MATCHER_P(Kind, K, "") { return arg.kind() == K; } +MATCHER_P2(HasText, Text, SourceMgr, "") { + return arg.text(*SourceMgr) == Text; +} +MATCHER_P2(IsIdent, Text, SourceMgr, "") { + return arg.kind() == tok::identifier && arg.text(*SourceMgr) == Text; +} +/// Checks the start and end location of a token are equal to SourceRng. +MATCHER_P(RangeIs, SourceRng, "") { + return arg.location() == SourceRng.first && + arg.endLocation() == SourceRng.second; +} +/// Checks the passed tuple has two similar tokens, i.e. both are of the same +/// kind and have the same text if they are identifiers. +/// Ignores differences in kind between the raw and non-raw mode. +MATCHER_P(IsSameToken, SourceMgr, "") { + auto ToEquivalenceClass = [](tok::TokenKind Kind) { + if (Kind == tok::identifier || Kind == tok::raw_identifier || + tok::getKeywordSpelling(Kind) != nullptr) + return tok::identifier; + if (Kind == tok::string_literal || Kind == tok::header_name) + return tok::string_literal; + return Kind; + }; + + auto &L = std::get<0>(arg); + auto &R = std::get<1>(arg); + if (ToEquivalenceClass(L.kind()) != ToEquivalenceClass(R.kind())) + return false; + return L.text(*SourceMgr) == L.text(*SourceMgr); +} +} // namespace + +// Actual test fixture lives in the syntax namespace as it's a friend of +// TokenBuffer. +class syntax::TokensTest : public ::testing::Test { +public: + /// Run the clang frontend, collect the preprocessed tokens from the frontend + /// invocation and store them in this->Buffer. + /// This also clears SourceManager before running the compiler. + void recordTokens(llvm::StringRef Code) { + class RecordTokens : public ASTFrontendAction { + public: + explicit RecordTokens(TokenBuffer &Result) : Result(Result) {} + + bool BeginSourceFileAction(CompilerInstance &CI) override { + assert(!Collector && "expected only a single call to BeginSourceFile"); + Collector.emplace(CI.getPreprocessor()); + return true; + } + void EndSourceFileAction() override { + assert(Collector && "BeginSourceFileAction was never called"); + Result = std::move(*Collector).consume(); + } + + std::unique_ptr + CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override { + return llvm::make_unique(); + } + + private: + TokenBuffer &Result; + llvm::Optional Collector; + }; + + constexpr const char *FileName = "./input.cpp"; + FS->addFile(FileName, time_t(), llvm::MemoryBuffer::getMemBufferCopy("")); + // Prepare to run a compiler. + std::vector Args = {"tok-test", "-std=c++03", "-fsyntax-only", + FileName}; + auto CI = createInvocationFromCommandLine(Args, Diags, FS); + assert(CI); + CI->getFrontendOpts().DisableFree = false; + CI->getPreprocessorOpts().addRemappedFile( + FileName, llvm::MemoryBuffer::getMemBufferCopy(Code).release()); + LangOpts = *CI->getLangOpts(); + CompilerInstance Compiler; + Compiler.setInvocation(std::move(CI)); + if (!Diags->getClient()) + Diags->setClient(new IgnoringDiagConsumer); + Compiler.setDiagnostics(Diags.get()); + Compiler.setFileManager(FileMgr.get()); + Compiler.setSourceManager(SourceMgr.get()); + + this->Buffer = TokenBuffer(); + RecordTokens Recorder(this->Buffer); + ASSERT_TRUE(Compiler.ExecuteAction(Recorder)) + << "failed to run the frontend"; + + DEBUG_WITH_TYPE("syntax-tokens-test", { + llvm::dbgs() << "=== Recorded token stream:\n"; + this->Buffer.dump(llvm::dbgs(), *SourceMgr); + }); + } + + /// Run syntax::tokenize() and return the results. + std::vector tokenize(llvm::StringRef Text) { + // Null-terminate so that we always see 'tok::eof' at the end. + std::string NullTerminated = Text.str(); + auto FID = SourceMgr->createFileID(llvm::MemoryBuffer::getMemBufferCopy( + StringRef(NullTerminated.data(), NullTerminated.size() + 1))); + return syntax::tokenize(FID, *SourceMgr, LangOpts); + } + + /// Checks that lexing \p ExpectedText in raw mode would produce the same + /// token stream as the one stored in this->Buffer.expandedTokens(). + void expectTokens(llvm::StringRef ExpectedText) { + std::vector ExpectedTokens = tokenize(ExpectedText); + EXPECT_THAT(std::vector(Buffer.expandedTokens()), + Pointwise(IsSameToken(), ExpectedTokens)) + << "\texpected tokens: " << ExpectedText; + } + + void expectSameTokens(llvm::ArrayRef Actual, + llvm::ArrayRef Expected) { + EXPECT_THAT(std::vector(Actual), + Pointwise(IsSameToken(), std::vector(Expected))); + } + + struct ExpectedInvocation { + ExpectedInvocation( + std::string From, std::string To, + llvm::Optional Range = llvm::None) + : From(std::move(From)), To(std::move(To)), Range(Range) {} + /// A textual representation of the macro tokens. + std::string From; + /// A textual representation of the tokens after macro replacement. + std::string To; + /// A text range the macro invocation in the source code. + llvm::Optional Range; + }; + + // FIXME: use a vocabulary range type instead. + std::pair + mappingTextRange(const TokenBuffer::Mapping &M, + const TokenBuffer::MarkedFile &F) { + assert(M.BeginRawToken < M.EndRawToken && "Invalid mapping"); + return { + SourceMgr->getFileOffset(F.RawTokens.at(M.BeginRawToken).location()), + SourceMgr->getFileOffset( + F.RawTokens.at(M.EndRawToken - 1).endLocation())}; + } + + FileID findFile(llvm::StringRef Name) const { + const FileEntry *Entry = FileMgr->getFile(Name); + FileID Found = SourceMgr->translateFile(Entry); + if (!Found.isValid()) { + ADD_FAILURE() << "SourceManager does not track " << Name; + std::abort(); + } + return Found; + } + /// Checks the this->Buffer.macroInvocations() for the main file match the \p + /// Expected ones. + void expectMacroInvocations(llvm::ArrayRef Expected, + FileID FID = FileID()) { + if (!FID.isValid()) + FID = SourceMgr->getMainFileID(); + EXPECT_TRUE(Buffer.Files.count(FID)) << "tokens for file were not recorded"; + TokenBuffer::MarkedFile &File = Buffer.Files[FID]; + + llvm::ArrayRef Actual = File.Mappings; + ASSERT_EQ(Actual.size(), Expected.size()); + + for (unsigned I = 0; I < Actual.size(); ++I) { + const auto &A = Actual[I]; + const auto &E = Expected[I]; + + if (E.Range) + ASSERT_EQ(mappingTextRange(A, File), + (std::pair(E.Range->Begin, E.Range->End))) + << "\trange does not match"; + + auto DropEOF = [](std::vector Tokens) { + if (Tokens.empty() || Tokens.back().kind() != tok::eof) { + ADD_FAILURE() << "expected 'eof' at the end of the tokens"; + return Tokens; + } + Tokens.pop_back(); + return Tokens; + }; + + std::vector ActualRaw( + File.RawTokens.begin() + A.BeginRawToken, + File.RawTokens.begin() + A.EndRawToken); + ASSERT_THAT(ActualRaw, + Pointwise(IsSameToken(), DropEOF(tokenize(E.From)))) + << "\tmacro tokens do not match, expected " << E.From; + + std::vector ActualExpanded( + Buffer.ExpandedTokens.begin() + A.BeginExpandedToken, + Buffer.ExpandedTokens.begin() + A.EndExpandedToken); + ASSERT_THAT(ActualExpanded, + Pointwise(IsSameToken(), DropEOF(tokenize(E.To)))) + << "\ttokens after macro replacements do not match, expected " + << E.To; + } + } + + // Specialized versions of matchers that rely on SourceManager. + Matcher IsIdent(std::string Text) const { + return ::IsIdent(Text, SourceMgr.get()); + } + Matcher HasText(std::string Text) const { + return ::HasText(Text, SourceMgr.get()); + } + Matcher RangeIs(llvm::Annotations::Range R) const { + std::pair Ls; + Ls.first = SourceMgr->getLocForStartOfFile(SourceMgr->getMainFileID()) + .getLocWithOffset(R.Begin); + Ls.second = SourceMgr->getLocForStartOfFile(SourceMgr->getMainFileID()) + .getLocWithOffset(R.End); + return ::RangeIs(Ls); + } + + Matcher> + IsSameToken() const { + return ::IsSameToken(SourceMgr.get()); + } + + void addFile(llvm::StringRef Path, llvm::StringRef Contents) { + if (!FS->addFile(Path, time_t(), + llvm::MemoryBuffer::getMemBufferCopy(Contents))) { + ADD_FAILURE() << "could not add a file to VFS: " << Path; + } + } + + // Data fields. + llvm::IntrusiveRefCntPtr Diags = + new DiagnosticsEngine(new DiagnosticIDs, new DiagnosticOptions); + IntrusiveRefCntPtr FS = + new llvm::vfs::InMemoryFileSystem; + llvm::IntrusiveRefCntPtr FileMgr = + new FileManager(FileSystemOptions(), FS); + llvm::IntrusiveRefCntPtr SourceMgr = + new SourceManager(*Diags, *FileMgr); + /// Contains last result of calling recordTokens(). + TokenBuffer Buffer; + /// Contains options from last run of recordTokens(). + LangOptions LangOpts; +}; + +namespace { +TEST_F(TokensTest, RawMode) { + EXPECT_THAT(tokenize("int main() {}"), + ElementsAre(Kind(tok::kw_int), IsIdent("main"), + Kind(tok::l_paren), Kind(tok::r_paren), + Kind(tok::l_brace), Kind(tok::r_brace), + Kind(tok::eof))); + // Comments are ignored for now. + EXPECT_THAT(tokenize("/* foo */int a; // more comments"), + ElementsAre(Kind(tok::kw_int), IsIdent("a"), Kind(tok::semi), + Kind(tok::eof))); +} + +TEST_F(TokensTest, Basic) { + recordTokens("int main() {}"); + EXPECT_THAT(Buffer.expandedTokens(), + ElementsAre(Kind(tok::kw_int), IsIdent("main"), + Kind(tok::l_paren), Kind(tok::r_paren), + Kind(tok::l_brace), Kind(tok::r_brace), + Kind(tok::eof))); + // All kinds of whitespace are ignored. + recordTokens("\t\n int\t\n main\t\n (\t\n )\t\n{\t\n }\t\n"); + EXPECT_THAT(Buffer.expandedTokens(), + ElementsAre(Kind(tok::kw_int), IsIdent("main"), + Kind(tok::l_paren), Kind(tok::r_paren), + Kind(tok::l_brace), Kind(tok::r_brace), + Kind(tok::eof))); + + llvm::Annotations Code(R"cpp( + $r1[[int]] $r2[[a]] $r3[[=]] $r4[["foo bar baz"]] $r5[[;]] + )cpp"); + recordTokens(Code.code()); + EXPECT_THAT( + Buffer.expandedTokens(), + ElementsAre(AllOf(Kind(tok::kw_int), RangeIs(Code.range("r1"))), + AllOf(Kind(tok::identifier), RangeIs(Code.range("r2"))), + AllOf(Kind(tok::equal), RangeIs(Code.range("r3"))), + AllOf(Kind(tok::string_literal), RangeIs(Code.range("r4"))), + AllOf(Kind(tok::semi), RangeIs(Code.range("r5"))), + Kind(tok::eof))); +} + +TEST_F(TokensTest, MacroDirectives) { + // Macro directives are not stored anywhere at the moment. + llvm::StringLiteral Code = R"cpp( + #define FOO a + #include "unresolved_file.h" + #undef FOO + #ifdef X + #else + #endif + #ifndef Y + #endif + #if 1 + #elif 2 + #else + #endif + #pragma once + #pragma something lalala + + int a; + )cpp"; + recordTokens(Code); + + expectTokens("int a;"); + expectMacroInvocations({}); + + expectSameTokens(Buffer.rawTokens(SourceMgr->getMainFileID()), + tokenize(Code)); +} + +TEST_F(TokensTest, MacroReplacements) { + // A simple object-like macro. + llvm::Annotations Code(R"cpp( + #define INT int const + [[INT]] a; + )cpp"); + recordTokens(Code.code()); + + expectTokens("int const a;"); + expectMacroInvocations({{"INT", "int const", Code.range()}}); + + // A simple function-like macro. + Code = llvm::Annotations(R"cpp( + #define INT(a) const int + [[INT(10+10)]] a; + )cpp"); + recordTokens(Code.code()); + + expectTokens("const int a;"); + expectMacroInvocations({{"INT(10+10)", "const int", Code.range()}}); + + // Recursive macro replacements. + Code = llvm::Annotations(R"cpp( + #define ID(X) X + #define INT int const + [[ID(ID(INT))]] a; + )cpp"); + recordTokens(Code.code()); + + expectTokens("int const a;"); + expectMacroInvocations({{"ID(ID(INT))", "int const", Code.range()}}); + + // A little more complicated recursive macro replacements. + Code = llvm::Annotations(R"cpp( + #define ADD(X, Y) X+Y + #define MULT(X, Y) X*Y + + int a = [[ADD(MULT(1,2), MULT(3,ADD(4,5)))]]; + )cpp"); + recordTokens(Code.code()); + + expectTokens("int a = 1*2+3*4+5;"); + expectMacroInvocations( + {{"ADD(MULT(1,2), MULT(3,ADD(4,5)))", "1*2+3*4+5", Code.range()}}); + + // Empty macro replacement. + Code = llvm::Annotations(R"cpp( + #define EMPTY + #define EMPTY_FUNC(X) + $m[[EMPTY]] + $f[[EMPTY_FUNC(1+2+3)]] + )cpp"); + recordTokens(Code.code()); + + expectTokens(""); + expectMacroInvocations({{"EMPTY", "", Code.range("m")}, + {"EMPTY_FUNC(1+2+3)", "", Code.range("f")}}); + + // File ends with a macro replacement. + Code = llvm::Annotations(R"cpp( + #define FOO 10+10; + int a = [[FOO]])cpp"); + recordTokens(Code.code()); + + expectTokens("int a = 10+10;"); + expectMacroInvocations({{"FOO", "10+10;", Code.range()}}); +} + +TEST_F(TokensTest, SpecialTokens) { + // Tokens coming from concatenations. + recordTokens(R"cpp( + #define CONCAT(a, b) a ## b + int a = CONCAT(1, 2); + )cpp"); + expectTokens("int a = 12;"); + // Multi-line tokens with slashes at the end. + recordTokens("i\\\nn\\\nt"); + EXPECT_THAT(Buffer.expandedTokens(), + ElementsAre(AllOf(Kind(tok::kw_int), HasText("i\\\nn\\\nt")), + Kind(tok::eof))); + // FIXME: test tokens with digraphs and UCN identifiers. +} + +TEST_F(TokensTest, LateBoundTokens) { + // The parser eventually breaks the first '>>' into two tokens ('>' and '>'), + // but we choose to record them as a single token (for now). + llvm::Annotations Code(R"cpp( + template + struct foo { int a; }; + int bar = foo>]]().a; + int baz = 10 $op[[>>]] 2; + )cpp"); + recordTokens(Code.code()); + EXPECT_THAT(std::vector(Buffer.expandedTokens()), + AllOf(Contains(AllOf(Kind(tok::greatergreater), + RangeIs(Code.range("br")))), + Contains(AllOf(Kind(tok::greatergreater), + RangeIs(Code.range("op")))))); +} + +TEST_F(TokensTest, DelayedParsing) { + llvm::StringLiteral Code = R"cpp( + struct Foo { + int method() { + // Parser will visit method bodies and initializers multiple times, but + // TokenBuffer should only record the first walk over the tokens; + return 100; + } + int a = 10; + int b = 20; + + struct Subclass { + void foo() { + Foo().method(); + } + }; + }; + )cpp"; + recordTokens(Code); + // Checks that lexing in raw mode produces the same results, hence we're not + // recording any tokens twice and the order is the same. + expectTokens(Code); +} + +TEST_F(TokensTest, Offsets) { + llvm::Annotations Code(""); + /// Finds a token with the specified text. + auto Find = [this](llvm::StringRef Text) { + llvm::ArrayRef Tokens = Buffer.expandedTokens(); + auto TokenMatches = [=](const syntax::Token &T) { + return T.text(*SourceMgr) == Text; + }; + auto It = llvm::find_if(Tokens, TokenMatches); + if (It == Tokens.end()) { + ADD_FAILURE() << "could not find the token for " << Text; + std::abort(); + } + if (std::find_if(std::next(It), Tokens.end(), TokenMatches) != + Tokens.end()) { + ADD_FAILURE() << "token is not unique: " << Text; + std::abort(); + }; + return It; + }; + auto Range = [&](llvm::StringRef Name) { + auto R = Code.range(Name); + syntax::FileRange FR; + FR.File = SourceMgr->getMainFileID(); + FR.Begin = R.Begin; + FR.End = R.End; + return FR; + }; + + Code = llvm::Annotations(R"cpp( + $all[[$first[[a1 a2 a3]] FIRST $second[[b1 b2]] LAST]] + )cpp"); + + recordTokens(Code.code()); + EXPECT_EQ( + Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), std::next(Find("LAST"))), *SourceMgr), + Range("all")); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("FIRST")), *SourceMgr), + Range("first")); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("b1"), Find("LAST")), *SourceMgr), + Range("second")); + + Code = llvm::Annotations(R"cpp( + #define A a1 a2 a3 + #define B b1 b2 + + $all[[$first[[A]] FIRST $second[[B]] LAST]] + )cpp"); + recordTokens(Code.code()); + + EXPECT_EQ( + Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), std::next(Find("LAST"))), *SourceMgr), + Range("all")); + EXPECT_EQ(*Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("FIRST")), *SourceMgr), + Range("first")); + EXPECT_EQ(*Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("b1"), Find("LAST")), *SourceMgr), + Range("second")); + // Ranges not fully covering macro invocations should fail. + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("a3")), *SourceMgr), + llvm::None); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("b2"), Find("LAST")), *SourceMgr), + llvm::None); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a2"), Find("b2")), *SourceMgr), + llvm::None); + + Code = llvm::Annotations(R"cpp( + #define ID(x) x + #define B b1 b2 + + $both[[$first[[ID(ID(ID(a1) a2 a3))]] FIRST $second[[ID(B)]]]] LAST + )cpp"); + recordTokens(Code.code()); + + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("FIRST")), *SourceMgr), + Range("first")); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("b1"), Find("LAST")), *SourceMgr), + Range("second")); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("LAST")), *SourceMgr), + Range("both")); + + // Ranges crossing macro call boundaries. + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("b2")), *SourceMgr), + llvm::None); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a2"), Find("b2")), *SourceMgr), + llvm::None); + // FIXME: next two examples should map to macro arguments, but currently they + // fail. + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a2"), Find("a3")), *SourceMgr), + llvm::None); + EXPECT_EQ(Buffer.findOffsetsByExpanded( + llvm::makeArrayRef(Find("a1"), Find("a3")), *SourceMgr), + llvm::None); +} + +TEST_F(TokensTest, MultiFile) { + addFile("./foo.h", R"cpp( + #define ADD(X, Y) X+Y + int a = 100; + #include "bar.h" + )cpp"); + addFile("./bar.h", R"cpp( + int b = ADD(1, 2); + #define MULT(X, Y) X*Y + )cpp"); + recordTokens(R"cpp( + #include "foo.h" + int c = ADD(1, MULT(2,3)); + )cpp"); + + expectTokens(R"cpp( + int a = 100; + int b = 1+2; + int c = 1+2*3; + )cpp"); + expectMacroInvocations({{"ADD(1,MULT(2,3))", "1+2*3"}}); + expectMacroInvocations({{}}, findFile("./foo.h")); + expectMacroInvocations({{"ADD(1,2)", "1+2"}}, findFile("./bar.h")); +} +} // namespace