ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
tool.cpp
Go to the documentation of this file.
1#include "clang/AST/ASTConsumer.h"
2#include "clang/AST/ASTContext.h"
3#include "clang/AST/ASTTypeTraits.h"
4#include "clang/AST/Attr.h"
5#include "clang/AST/ParentMapContext.h"
6#include "clang/AST/RecursiveASTVisitor.h"
7#include "clang/Frontend/CompilerInstance.h"
8#include "clang/Lex/Lexer.h"
9#include "clang/Rewrite/Core/Rewriter.h"
10#include "clang/Tooling/ArgumentsAdjusters.h"
11#include "clang/Tooling/CommonOptionsParser.h"
12#include "clang/Tooling/Tooling.h"
13#include "llvm/ADT/RewriteBuffer.h"
14#include "llvm/ADT/StringRef.h"
15#include "llvm/Support/CommandLine.h"
16#include "llvm/Support/Error.h"
17#include "llvm/Support/FileSystem.h"
18#include "llvm/Support/InitLLVM.h"
19#include "llvm/Support/raw_ostream.h"
20
21#include <algorithm>
22#include <cctype>
23#include <filesystem>
24#include <fstream>
25#include <mutex>
26#include <optional>
27#include <string>
28#include <unordered_set>
29#include <utility>
30#include <vector>
31
32using namespace llvm;
33using namespace clang;
34
35std::mutex &outputRegistryMutex() {
36 static std::mutex mutex;
37 return mutex;
38}
39
40std::unordered_set<std::string> &outputRegistry() {
41 static std::unordered_set<std::string> registry;
42 return registry;
43}
44
45bool registerOutputPath(const std::string &path) {
46 std::lock_guard<std::mutex> guard(outputRegistryMutex());
47 auto &registry = outputRegistry();
48 auto [_, inserted] = registry.insert(path);
49 return inserted;
50}
51
52void unregisterOutputPath(const std::string &path) {
53 std::lock_guard<std::mutex> guard(outputRegistryMutex());
54 outputRegistry().erase(path);
55}
56
57// LLVM command line options MUST be declared at file scope (not in anonymous namespace)
58// to ensure proper static initialization
59namespace fs = std::filesystem;
60
61static cl::OptionCategory ToolCategory("ascii-chat instrumentation options");
62
63static cl::extrahelp CommonHelp(tooling::CommonOptionsParser::HelpMessage);
64static cl::extrahelp MoreHelp("\nInstrumentation tool for ascii-chat debugging\n");
65
66static cl::opt<std::string> OutputDirectoryOption("output-dir",
67 cl::desc("Directory where instrumented sources will be written"),
68 cl::value_desc("path"), cl::Required, cl::cat(ToolCategory));
69
70static cl::opt<std::string>
71 InputRootOption("input-root", cl::desc("Root directory of original sources (used to compute relative paths)"),
72 cl::value_desc("path"), cl::init(""), cl::cat(ToolCategory));
73
74static cl::opt<std::string> BuildPath("p", cl::desc("Build path (directory containing compile_commands.json)"),
75 cl::Optional, cl::cat(ToolCategory));
76static cl::opt<std::string> CompileSourceOption("compile-source",
77 cl::desc("Compilation database source corresponding to the input"),
78 cl::init(""), cl::cat(ToolCategory));
79static cl::opt<std::string> ResourceDirOption("resource-dir", cl::desc("Clang builtin headers directory"), cl::init(""),
80 cl::cat(ToolCategory));
81static cl::opt<bool> LogMacroExpansionsOption("log-macro-expansions",
82 cl::desc("Instrument statements originating from macro expansions"),
83 cl::init(false), cl::cat(ToolCategory));
84
85static cl::opt<bool> LogMacroInvocationsOption(
86 "log-macro-invocations",
87 cl::desc("Emit a synthetic record for the macro invocation site when expansions are instrumented"), cl::init(false),
88 cl::cat(ToolCategory));
89
90static cl::opt<bool> LegacyIncludeMacroExpansionsOption(
91 "include-macro-expansions",
92 cl::desc("Deprecated alias for --log-macro-expansions (kept for backward compatibility)"), cl::init(false),
93 cl::cat(ToolCategory), cl::Hidden);
94
95static cl::list<std::string>
96 FileIncludeFilters("filter-file", cl::desc("Only instrument files whose path contains the given substring"),
97 cl::value_desc("substring"), cl::cat(ToolCategory));
98
99static cl::list<std::string>
100 FunctionIncludeFilters("filter-function",
101 cl::desc("Only instrument functions whose name matches the given substring"),
102 cl::value_desc("substring"), cl::cat(ToolCategory));
103
104static cl::opt<std::string>
105 FileListOption("file-list", cl::desc("Path to file containing newline-delimited translation units to instrument"),
106 cl::value_desc("path"), cl::init(""), cl::cat(ToolCategory));
107
108static cl::list<std::string> SourcePaths(cl::Positional, cl::desc("<source0> [... <sourceN>]"), cl::cat(ToolCategory));
109
110static cl::opt<std::string> SignalHandlerAnnotation(
111 "signal-handler-annotation", cl::desc("Annotation string used to mark functions that should be skipped"),
112 cl::value_desc("annotation"), cl::init("ASCII_INSTR_SOURCE_PRINT_SIGNAL_HANDLER"), cl::cat(ToolCategory));
113
114constexpr unsigned kMacroFlagNone = 0U;
115constexpr unsigned kMacroFlagExpansion = 1U;
116constexpr unsigned kMacroFlagInvocation = 2U;
117
118namespace {
119
120// The compiled file can be a generated source; parse the original using that
121// target's exact compile command, while retaining the original file's context.
122class MappedCompilationDatabase final : public tooling::CompilationDatabase {
123public:
124 MappedCompilationDatabase(const tooling::CompilationDatabase &base, std::string compiled)
125 : base_(base), compiled_(std::move(compiled)) {}
126
127 std::vector<tooling::CompileCommand> getCompileCommands(StringRef input) const override {
128 auto commands = base_.getCompileCommands(compiled_);
129 for (auto &command : commands) {
130 for (auto &arg : command.CommandLine) {
131 if ((fs::path(command.Directory) / fs::path(arg)).lexically_normal() == fs::path(compiled_).lexically_normal())
132 arg = input.str();
133 }
134 command.Filename = input.str();
135 }
136 return commands;
137 }
138
139private:
140 const tooling::CompilationDatabase &base_;
141 std::string compiled_;
142};
143
144class InstrumentationVisitor : public clang::RecursiveASTVisitor<InstrumentationVisitor> {
145public:
146 InstrumentationVisitor(clang::ASTContext &context, clang::Rewriter &rewriter, const fs::path &outputDir,
147 const fs::path &inputRoot, bool enableFileFilters, bool enableFunctionFilters,
148 bool logMacroInvocations, bool logMacroExpansions)
149 : context_(context), rewriter_(rewriter), outputDir_(outputDir), inputRoot_(inputRoot),
150 enableFileFilters_(enableFileFilters), enableFunctionFilters_(enableFunctionFilters),
151 logMacroInvocations_(logMacroInvocations), logMacroExpansions_(logMacroExpansions) {}
152
153 bool TraverseFunctionDecl(clang::FunctionDecl *funcDecl) {
154 const bool previousSkipState = skipCurrentFunction_;
155 currentFunction_ = funcDecl;
156 skipCurrentFunction_ = shouldSkipFunction(funcDecl);
157
158 const bool result = clang::RecursiveASTVisitor<InstrumentationVisitor>::TraverseFunctionDecl(funcDecl);
159
160 skipCurrentFunction_ = previousSkipState;
161 currentFunction_ = nullptr;
162 return result;
163 }
164
165 bool VisitStmt(clang::Stmt *statement) {
166 if (!statement || !currentFunction_) {
167 return true;
168 }
169
170 if (!isDirectChildOfCompound(*statement)) {
171 return true;
172 }
173
174 if (llvm::isa<clang::CompoundStmt>(statement) || llvm::isa<clang::NullStmt>(statement)) {
175 return true;
176 }
177
178 if (skipCurrentFunction_) {
179 return true;
180 }
181
182 if (!shouldInstrumentStatement(*statement)) {
183 return true;
184 }
185
186 clang::SourceManager &sourceManager = context_.getSourceManager();
187 const clang::LangOptions &langOptions = context_.getLangOpts();
188
189 clang::SourceLocation beginLocation = statement->getBeginLoc();
190 if (beginLocation.isInvalid()) {
191 return true;
192 }
193
194 clang::SourceLocation expansionLocation = sourceManager.getExpansionLoc(beginLocation);
195 if (!expansionLocation.isValid()) {
196 return true;
197 }
198
199 if (!sourceManager.isWrittenInMainFile(expansionLocation)) {
200 return true;
201 }
202
203 const clang::FileID fileId = sourceManager.getFileID(expansionLocation);
204 const clang::FileEntry *fileEntry = sourceManager.getFileEntryForID(fileId);
205 if (fileEntry == nullptr) {
206 return true;
207 }
208
209 const llvm::StringRef absoluteFilePath = fileEntry->tryGetRealPathName();
210 if (absoluteFilePath.empty()) {
211 return true;
212 }
213
214 const fs::path filePath = fs::path(absoluteFilePath.str());
215 if (enableFileFilters_ && !matchesFileFilters(filePath)) {
216 return true;
217 }
218
219 const std::string relativePath = makeRelativePath(filePath);
220 const std::string uniqueKey = buildUniqueKey(filePath, expansionLocation, sourceManager);
221 if (!instrumentedLocations_.insert(uniqueKey).second) {
222 return true;
223 }
224
225 const bool isMacroExpansion =
226 sourceManager.isMacroBodyExpansion(beginLocation) || sourceManager.isMacroArgExpansion(beginLocation);
227
228 std::string instrumentationBlock;
229
230 if (isMacroExpansion) {
231 if (logMacroInvocations_) {
232 const std::optional<MacroInvocationMetadata> invocationMetadata =
233 buildMacroInvocationMetadata(*statement, sourceManager, langOptions);
234 if (invocationMetadata.has_value()) {
235 if (macroInvocationLocations_.insert(invocationMetadata->uniqueKey).second) {
236 instrumentationBlock.append(buildInstrumentationLine(invocationMetadata->relativePath,
237 invocationMetadata->lineNumber,
238 invocationMetadata->snippet, kMacroFlagInvocation));
239 }
240 }
241 }
242
243 if (logMacroExpansions_) {
244 const unsigned lineNumber = sourceManager.getSpellingLineNumber(expansionLocation);
245 const std::optional<std::string> snippetOpt = extractSnippet(*statement, sourceManager, langOptions);
246 const llvm::StringRef snippetRef = snippetOpt ? llvm::StringRef(*snippetOpt) : llvm::StringRef("<unavailable>");
247 instrumentationBlock.append(
248 buildInstrumentationLine(relativePath, lineNumber, snippetRef, kMacroFlagExpansion));
249 }
250
251 if (instrumentationBlock.empty()) {
252 return true;
253 }
254 } else {
255 const unsigned lineNumber = sourceManager.getSpellingLineNumber(expansionLocation);
256 const std::optional<std::string> snippetOpt = extractSnippet(*statement, sourceManager, langOptions);
257 const llvm::StringRef snippetRef = snippetOpt ? llvm::StringRef(*snippetOpt) : llvm::StringRef("<unavailable>");
258 instrumentationBlock.append(buildInstrumentationLine(relativePath, lineNumber, snippetRef, kMacroFlagNone));
259 }
260
261 if (instrumentationBlock.empty()) {
262 return true;
263 }
264
265 rewriter_.InsertText(expansionLocation, instrumentationBlock, true, true);
266 includeNeeded_ = true;
267 return true;
268 }
269
270 bool includeNeeded() const {
271 return includeNeeded_;
272 }
273
274 std::string buildUniqueKey(const fs::path &filePath, clang::SourceLocation location,
275 const clang::SourceManager &sourceManager) const {
276 const unsigned offset = sourceManager.getFileOffset(location);
277 return (filePath.string() + ":" + std::to_string(offset));
278 }
279
280 std::string buildInstrumentationLine(const std::string &relativePath, unsigned lineNumber, llvm::StringRef snippet,
281 unsigned macroFlag) const {
282 std::string escapedSnippet = escapeSnippet(snippet);
283 std::string instrumentationLine;
284 instrumentationLine.reserve(escapedSnippet.size() + 256);
285 instrumentationLine.append("asciichat_instr_log_line(\"");
286 instrumentationLine.append(relativePath);
287 instrumentationLine.append("\", ");
288 instrumentationLine.append(std::to_string(lineNumber));
289 instrumentationLine.append(", __func__, \"");
290 instrumentationLine.append(escapedSnippet);
291 instrumentationLine.append("\", ");
292 instrumentationLine.append(std::to_string(macroFlag));
293 instrumentationLine.append(");\n");
294 return instrumentationLine;
295 }
296
297 struct MacroInvocationMetadata {
298 std::string relativePath;
299 unsigned lineNumber = 0;
300 std::string snippet;
301 std::string uniqueKey;
302 };
303
304 std::optional<MacroInvocationMetadata> buildMacroInvocationMetadata(const clang::Stmt &statement,
305 clang::SourceManager &sourceManager,
306 const clang::LangOptions &langOptions) const {
307 clang::SourceLocation beginLocation = statement.getBeginLoc();
308 if (!(sourceManager.isMacroBodyExpansion(beginLocation) || sourceManager.isMacroArgExpansion(beginLocation))) {
309 return std::nullopt;
310 }
311
312 clang::SourceLocation callerLocation = sourceManager.getImmediateMacroCallerLoc(beginLocation);
313 if (!callerLocation.isValid()) {
314 return std::nullopt;
315 }
316
317 callerLocation = sourceManager.getExpansionLoc(callerLocation);
318 if (!callerLocation.isValid() || !sourceManager.isWrittenInMainFile(callerLocation)) {
319 return std::nullopt;
320 }
321
322 const clang::FileID callerFileId = sourceManager.getFileID(callerLocation);
323 const clang::FileEntry *callerFileEntry = sourceManager.getFileEntryForID(callerFileId);
324 if (callerFileEntry == nullptr) {
325 return std::nullopt;
326 }
327
328 const llvm::StringRef callerPathRef = callerFileEntry->tryGetRealPathName();
329 if (callerPathRef.empty()) {
330 return std::nullopt;
331 }
332
333 MacroInvocationMetadata metadata;
334 const fs::path callerPath = fs::path(callerPathRef.str());
335 metadata.relativePath = makeRelativePath(callerPath);
336 metadata.lineNumber = sourceManager.getSpellingLineNumber(callerLocation);
337 metadata.uniqueKey = buildUniqueKey(callerPath, callerLocation, sourceManager);
338
339 clang::CharSourceRange expansionRange = sourceManager.getImmediateExpansionRange(beginLocation);
340 if (expansionRange.isValid()) {
341 bool invalid = false;
342 llvm::StringRef invocationSource =
343 clang::Lexer::getSourceText(expansionRange, sourceManager, langOptions, &invalid);
344 if (!invalid && !invocationSource.empty()) {
345 metadata.snippet = invocationSource.str();
346 }
347 }
348
349 if (metadata.snippet.empty()) {
350 bool invalid = false;
351 clang::CharSourceRange tokenRange = clang::CharSourceRange::getTokenRange(callerLocation, callerLocation);
352 llvm::StringRef fallback = clang::Lexer::getSourceText(tokenRange, sourceManager, langOptions, &invalid);
353 if (!invalid && !fallback.empty()) {
354 metadata.snippet = fallback.str();
355 } else {
356 metadata.snippet = "<macro invocation>";
357 }
358 }
359
360 return metadata;
361 }
362
363 bool matchesFileFilters(const fs::path &filePath) const {
364 if (FileIncludeFilters.empty()) {
365 return true;
366 }
367 const std::string filePathString = filePath.generic_string();
368 for (const std::string &token : FileIncludeFilters) {
369 if (filePathString.find(token) != std::string::npos) {
370 return true;
371 }
372 }
373 return false;
374 }
375
376 bool matchesFunctionFilters(const clang::FunctionDecl *functionDecl) const {
377 if (FunctionIncludeFilters.empty()) {
378 return true;
379 }
380 if (functionDecl == nullptr) {
381 return false;
382 }
383 const std::string functionName = functionDecl->getNameInfo().getAsString();
384 for (const std::string &token : FunctionIncludeFilters) {
385 if (functionName.find(token) != std::string::npos) {
386 return true;
387 }
388 }
389 return false;
390 }
391
392 bool shouldInstrumentStatement(const clang::Stmt &statement) const {
393 if (llvm::isa<clang::NullStmt>(&statement)) {
394 return false;
395 }
396 if (llvm::isa<clang::ImplicitCastExpr>(&statement)) {
397 return false;
398 }
399 if (llvm::isa<clang::ParenExpr>(&statement)) {
400 return false;
401 }
402
403 if (enableFunctionFilters_ && !matchesFunctionFilters(currentFunction_)) {
404 return false;
405 }
406
407 return true;
408 }
409
410 bool shouldSkipFunction(const clang::FunctionDecl *functionDecl) const {
411 if (functionDecl == nullptr) {
412 return true;
413 }
414
415 if (functionDecl->isImplicit()) {
416 return true;
417 }
418
419 clang::SourceManager &sourceManager = context_.getSourceManager();
420 clang::SourceLocation location = functionDecl->getLocation();
421 location = sourceManager.getExpansionLoc(location);
422 if (!location.isValid() || !sourceManager.isWrittenInMainFile(location)) {
423 return true;
424 }
425
426 for (const clang::AnnotateAttr *annotation : functionDecl->specific_attrs<clang::AnnotateAttr>()) {
427 if (!annotation) {
428 continue;
429 }
430 if (annotation->getAnnotation() == SignalHandlerAnnotation.getValue()) {
431 return true;
432 }
433 }
434
435 return false;
436 }
437
438 std::string makeRelativePath(const fs::path &absolutePath) const {
439 if (inputRoot_.empty()) {
440 return absolutePath.generic_string();
441 }
442
443 std::error_code errorCode;
444 fs::path relative = fs::relative(absolutePath, inputRoot_, errorCode);
445 if (errorCode) {
446 return absolutePath.generic_string();
447 }
448 return relative.generic_string();
449 }
450
451 static std::string escapeSnippet(llvm::StringRef snippet) {
452 std::string result;
453 result.reserve(snippet.size());
454 constexpr std::size_t kMaxSnippetLength = 1024;
455 std::size_t processed = 0;
456 for (char ch : snippet) {
457 if (processed >= kMaxSnippetLength) {
458 result.append("<truncated>");
459 break;
460 }
461 processed++;
462 const unsigned char uch = static_cast<unsigned char>(ch);
463 switch (ch) {
464 case '\\':
465 result.append("\\\\");
466 break;
467 case '"':
468 result.append("\\\"");
469 break;
470 case '\n':
471 result.append("\\n");
472 break;
473 case '\r':
474 result.append("\\r");
475 break;
476 case '\t':
477 result.append("\\t");
478 break;
479 default:
480 if (std::isprint(uch) != 0 || uch >= 0x80) {
481 result.push_back(static_cast<char>(uch));
482 } else {
483 result.append("\\\\x");
484 constexpr char hexDigits[] = "0123456789ABCDEF";
485 result.push_back(hexDigits[(uch >> 4) & 0xF]);
486 result.push_back(hexDigits[uch & 0xF]);
487 }
488 break;
489 }
490 }
491 return result;
492 }
493
494 static std::optional<std::string> extractSnippet(const clang::Stmt &statement,
495 const clang::SourceManager &sourceManager,
496 const clang::LangOptions &langOptions) {
497 clang::SourceLocation begin = statement.getBeginLoc();
498 clang::SourceLocation end = statement.getEndLoc();
499
500 if (begin.isInvalid() || end.isInvalid()) {
501 return std::nullopt;
502 }
503
504 clang::SourceLocation expansionBegin = sourceManager.getExpansionLoc(begin);
505 clang::SourceLocation expansionEnd = sourceManager.getExpansionLoc(end);
506 if (!expansionBegin.isValid() || !expansionEnd.isValid()) {
507 return std::nullopt;
508 }
509
510 clang::CharSourceRange range = clang::CharSourceRange::getTokenRange(expansionBegin, expansionEnd);
511 bool invalid = false;
512 llvm::StringRef text = clang::Lexer::getSourceText(range, sourceManager, langOptions, &invalid);
513 if (invalid || text.empty()) {
514 return std::nullopt;
515 }
516 return text.str();
517 }
518
519private:
520 clang::ASTContext &context_;
521 clang::Rewriter &rewriter_;
522 fs::path outputDir_;
523 fs::path inputRoot_;
524 bool enableFileFilters_;
525 bool enableFunctionFilters_;
526 bool logMacroInvocations_;
527 bool logMacroExpansions_;
528 const clang::FunctionDecl *currentFunction_ = nullptr;
529 bool skipCurrentFunction_ = false;
530 bool includeNeeded_ = false;
531 std::unordered_set<std::string> instrumentedLocations_;
532 std::unordered_set<std::string> macroInvocationLocations_;
533
534 bool isDirectChildOfCompound(const clang::Stmt &statement) const {
535 clang::DynTypedNodeList parents = context_.getParents(statement);
536 for (const auto &parent : parents) {
537 if (parent.get<clang::CompoundStmt>()) {
538 return true;
539 }
540 }
541 return false;
542 }
543};
544
545class InstrumentationASTConsumer : public clang::ASTConsumer {
546public:
547 explicit InstrumentationASTConsumer(InstrumentationVisitor &visitor) : visitor_(visitor) {}
548
549 void HandleTranslationUnit(clang::ASTContext &context) override {
550 visitor_.TraverseDecl(context.getTranslationUnitDecl());
551 }
552
553private:
554 InstrumentationVisitor &visitor_;
555};
556
557class InstrumentationFrontendAction : public clang::ASTFrontendAction {
558public:
559 explicit InstrumentationFrontendAction(const fs::path &outputDir, const fs::path &inputRoot, bool enableFileFilters,
560 bool enableFunctionFilters, bool logMacroInvocations, bool logMacroExpansions)
561 : outputDir_(outputDir), inputRoot_(inputRoot), enableFileFilters_(enableFileFilters),
562 enableFunctionFilters_(enableFunctionFilters), logMacroInvocations_(logMacroInvocations),
563 logMacroExpansions_(logMacroExpansions) {}
564
565 void EndSourceFileAction() override {
566 clang::SourceManager &sourceManager = rewriter_.getSourceMgr();
567 const clang::FileEntry *fileEntry = sourceManager.getFileEntryForID(sourceManager.getMainFileID());
568 if (!fileEntry) {
569 return;
570 }
571
572 if (!visitor_) {
573 llvm::errs() << "Instrumentation visitor not initialized; skipping file output" << "\n";
574 hadWriteError_ = true;
575 return;
576 }
577
578 const llvm::StringRef filePathRef = fileEntry->tryGetRealPathName();
579 if (filePathRef.empty()) {
580 llvm::errs() << "Unable to resolve file path for instrumented output\n";
581 return;
582 }
583
584 const fs::path originalPath = fs::path(filePathRef.str());
585 const std::string relativePath = visitor_->makeRelativePath(originalPath);
586 const fs::path destinationPath = outputDir_ / relativePath;
587 const std::string destinationString = destinationPath.string();
588
589 if (!registerOutputPath(destinationString)) {
590 return;
591 }
592
593 std::error_code sameFileError;
594 const bool sameFile = fs::equivalent(originalPath, destinationPath, sameFileError);
595 if (fs::exists(destinationPath) && (CompileSourceOption.empty() || (!sameFileError && sameFile))) {
596 llvm::errs() << "Refusing to overwrite existing file: " << destinationPath.c_str() << "\n";
597 unregisterOutputPath(destinationString);
598 hadWriteError_ = true;
599 return;
600 }
601
602 const fs::path parent = destinationPath.parent_path();
603 std::error_code directoryError;
604 fs::create_directories(parent, directoryError);
605 if (directoryError) {
606 llvm::errs() << "Failed to create output directory: " << parent.c_str() << " - " << directoryError.message()
607 << "\n";
608 unregisterOutputPath(destinationString);
609 hadWriteError_ = true;
610 return;
611 }
612
613 ensureIncludeInserted(originalPath);
614
615 std::string rewrittenContents;
616 if (const llvm::RewriteBuffer *buffer = rewriter_.getRewriteBufferFor(sourceManager.getMainFileID())) {
617 rewrittenContents.assign(buffer->begin(), buffer->end());
618 } else {
619 rewrittenContents = sourceManager.getBufferData(sourceManager.getMainFileID()).str();
620 }
621
622 std::error_code fileError;
623 llvm::raw_fd_ostream outputStream(destinationPath.string(), fileError, llvm::sys::fs::OF_Text);
624 if (fileError) {
625 llvm::errs() << "Failed to open output file: " << destinationPath.c_str() << " - " << fileError.message() << "\n";
626 unregisterOutputPath(destinationString);
627 hadWriteError_ = true;
628 return;
629 }
630 outputStream << rewrittenContents;
631 outputStream.close();
632 if (outputStream.has_error()) {
633 llvm::errs() << "Error while writing instrumented file: " << destinationPath.c_str() << "\n";
634 unregisterOutputPath(destinationString);
635 hadWriteError_ = true;
636 }
637 }
638
639 bool hadWriteError() const {
640 return hadWriteError_;
641 }
642
643protected:
644 std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(clang::CompilerInstance &compiler, llvm::StringRef) override {
645 rewriter_.setSourceMgr(compiler.getSourceManager(), compiler.getLangOpts());
646 visitor_ = std::make_unique<InstrumentationVisitor>(compiler.getASTContext(), rewriter_, outputDir_, inputRoot_,
647 enableFileFilters_, enableFunctionFilters_,
648 logMacroInvocations_, logMacroExpansions_);
649 return std::make_unique<InstrumentationASTConsumer>(*visitor_);
650 }
651
652private:
653 void ensureIncludeInserted(const fs::path &originalPath) {
654 (void)originalPath; // Unused parameter - kept for future use
655 if (!visitor_ || !visitor_->includeNeeded()) {
656 return;
657 }
658
659 clang::SourceManager &sourceManager = rewriter_.getSourceMgr();
660 const clang::FileID fileId = sourceManager.getMainFileID();
661 const llvm::StringRef bufferData = sourceManager.getBufferData(fileId);
662 if (bufferData.contains("#include <ascii-chat/tooling/panic/instrument_log.h>")) {
663 return;
664 }
665
666 clang::SourceLocation insertionLocation = sourceManager.getLocForStartOfFile(fileId);
667 rewriter_.InsertText(insertionLocation, "#include <ascii-chat/tooling/panic/instrument_log.h>\n", false, true);
668 }
669
670 clang::Rewriter rewriter_;
671 fs::path outputDir_;
672 fs::path inputRoot_;
673 bool enableFileFilters_;
674 bool enableFunctionFilters_;
675 bool logMacroInvocations_;
676 bool logMacroExpansions_;
677 std::unique_ptr<InstrumentationVisitor> visitor_;
678 bool hadWriteError_ = false;
679};
680
681class InstrumentationActionFactory : public clang::tooling::FrontendActionFactory {
682public:
683 InstrumentationActionFactory(const fs::path &outputDir, const fs::path &inputRoot, bool enableFileFilters,
684 bool enableFunctionFilters, bool logMacroInvocations, bool logMacroExpansions)
685 : outputDir_(outputDir), inputRoot_(inputRoot), enableFileFilters_(enableFileFilters),
686 enableFunctionFilters_(enableFunctionFilters), logMacroInvocations_(logMacroInvocations),
687 logMacroExpansions_(logMacroExpansions) {}
688
689 std::unique_ptr<clang::FrontendAction> create() {
690 return std::make_unique<InstrumentationFrontendAction>(
691 outputDir_, inputRoot_, enableFileFilters_, enableFunctionFilters_, logMacroInvocations_, logMacroExpansions_);
692 }
693
694private:
695 fs::path outputDir_;
696 fs::path inputRoot_;
697 bool enableFileFilters_;
698 bool enableFunctionFilters_;
699 bool logMacroInvocations_;
700 bool logMacroExpansions_;
701};
702
703} // namespace
704
705int main(int argc, const char **argv) {
706 // Initialize LLVM infrastructure (this triggers command-line option registration!)
707 llvm::InitLLVM InitLLVM(argc, argv);
708
709 // Parse command-line options
710 cl::ParseCommandLineOptions(argc, argv, "ascii-chat instrumentation tool\n");
711
712 const fs::path outputDir = fs::path(OutputDirectoryOption.getValue());
713 fs::path inputRoot;
714 if (!InputRootOption.getValue().empty()) {
715 inputRoot = fs::path(InputRootOption.getValue());
716 } else {
717 inputRoot = fs::current_path();
718 }
719
720 std::vector<std::string> rawSourceArgs;
721 rawSourceArgs.reserve(SourcePaths.size());
722 for (const auto &path : SourcePaths) {
723 rawSourceArgs.push_back(path);
724 }
725 std::vector<std::string> sourcePaths;
726 std::vector<std::string> extraCompilerArgs;
727 for (std::size_t i = 0; i < rawSourceArgs.size(); ++i) {
728 const std::string &entry = rawSourceArgs[i];
729 if (entry.empty()) {
730 continue;
731 }
732 if (entry == "--") {
733 continue;
734 }
735 if (entry[0] == '-') {
736 extraCompilerArgs.push_back(entry);
737 const bool consumesNext = (entry == "-I" || entry == "-isystem" || entry == "-include" ||
738 entry == "-include-pch" || entry == "-imacros" || entry == "-idirafter" ||
739 entry == "-iprefix" || entry == "-iwithprefix" || entry == "-iwithprefixbefore" ||
740 entry == "-resource-dir" || entry == "-Xclang" || entry == "-Xpreprocessor");
741 if (consumesNext && (i + 1) < rawSourceArgs.size()) {
742 extraCompilerArgs.push_back(rawSourceArgs[i + 1]);
743 ++i;
744 }
745 continue;
746 }
747 sourcePaths.push_back(entry);
748 }
749 if (!FileListOption.getValue().empty()) {
750 std::ifstream listStream(FileListOption.getValue());
751 if (!listStream.is_open()) {
752 llvm::errs() << "Failed to open file list: " << FileListOption.getValue() << "\n";
753 return 1;
754 }
755
756 std::string line;
757 while (std::getline(listStream, line)) {
758 llvm::StringRef trimmed(line);
759 trimmed = trimmed.trim();
760 if (trimmed.empty()) {
761 continue;
762 }
763 sourcePaths.emplace_back(trimmed.str());
764 }
765 }
766
767 if (sourcePaths.empty()) {
768 llvm::errs()
769 << "No translation units specified for instrumentation. Provide positional source paths or --file-list."
770 << "\n";
771 return 1;
772 }
773
774 if (fs::exists(outputDir)) {
775 if (!fs::is_directory(outputDir)) {
776 llvm::errs() << "Output path exists and is not a directory: " << outputDir.c_str() << "\n";
777 return 1;
778 }
779 } else {
780 std::error_code errorCode;
781 fs::create_directories(outputDir, errorCode);
782 if (errorCode) {
783 llvm::errs() << "Failed to create output directory: " << outputDir.c_str() << " - " << errorCode.message()
784 << "\n";
785 return 1;
786 }
787 }
788
789 const bool logMacroExpansions = LogMacroExpansionsOption.getValue() || LegacyIncludeMacroExpansionsOption.getValue();
790 const bool logMacroInvocations = LogMacroInvocationsOption.getValue();
791
792 if (!LogMacroExpansionsOption.getValue() && LegacyIncludeMacroExpansionsOption.getValue()) {
793 llvm::errs() << "warning: --include-macro-expansions is deprecated; use --log-macro-expansions instead\n";
794 }
795
796 // Load compilation database
797 std::string buildPath = BuildPath.getValue();
798 if (buildPath.empty()) {
799 buildPath = ".";
800 }
801 std::string errorMessage;
802 std::unique_ptr<tooling::CompilationDatabase> compilations =
803 tooling::CompilationDatabase::loadFromDirectory(buildPath, errorMessage);
804 if (!compilations) {
805 llvm::errs() << "Error loading compilation database from '" << buildPath << "': " << errorMessage << "\n";
806 return 1;
807 }
808
809 std::unique_ptr<MappedCompilationDatabase> mapped;
810 if (!CompileSourceOption.empty()) {
811 if (sourcePaths.size() != 1) {
812 llvm::errs() << "--compile-source requires exactly one input\n";
813 return 1;
814 }
815 mapped = std::make_unique<MappedCompilationDatabase>(*compilations, CompileSourceOption.getValue());
816 }
817 clang::tooling::ClangTool tool(mapped ? static_cast<const tooling::CompilationDatabase &>(*mapped) : *compilations,
818 sourcePaths);
819
820 if (!ResourceDirOption.empty()) {
821 const std::string resourceDir = ResourceDirOption.getValue();
822 tool.appendArgumentsAdjuster([resourceDir](const tooling::CommandLineArguments &args, StringRef) {
823 auto adjusted = args;
824 adjusted.push_back("-resource-dir");
825 adjusted.push_back(resourceDir);
826 return adjusted;
827 });
828 }
829 if (mapped) {
830 tool.appendArgumentsAdjuster([](const tooling::CommandLineArguments &args, StringRef input) {
831 auto adjusted = args;
832 adjusted.push_back("-DDEFER_PARSING=1");
833 adjusted.push_back("-iquote");
834 adjusted.push_back(fs::path(input.str()).parent_path().string());
835 return adjusted;
836 });
837 }
838
839 if (!extraCompilerArgs.empty()) {
840 const std::vector<std::string> extraArgsCopy(extraCompilerArgs.begin(), extraCompilerArgs.end());
841 tool.appendArgumentsAdjuster([extraArgsCopy](const clang::tooling::CommandLineArguments &args, llvm::StringRef) {
842 clang::tooling::CommandLineArguments adjusted = args;
843 adjusted.insert(adjusted.end(), extraArgsCopy.begin(), extraArgsCopy.end());
844 return adjusted;
845 });
846 }
847
848 auto stripPchAdjuster = [](const clang::tooling::CommandLineArguments &args, llvm::StringRef) {
849 clang::tooling::CommandLineArguments result;
850 const auto containsCMakePch = [](llvm::StringRef value) { return value.contains("cmake_pch"); };
851
852 for (std::size_t i = 0; i < args.size(); ++i) {
853 const std::string &arg = args[i];
854 llvm::StringRef argRef(arg);
855
856 if ((argRef == "-include" || argRef == "--include" || argRef == "-include-pch" || argRef == "--include-pch") &&
857 (i + 1) < args.size()) {
858 if (containsCMakePch(args[i + 1])) {
859 ++i;
860 continue;
861 }
862 }
863
864 if ((argRef.starts_with("-include=") || argRef.starts_with("--include=") || argRef.starts_with("-include-pch=") ||
865 argRef.starts_with("--include-pch=")) &&
866 containsCMakePch(argRef)) {
867 continue;
868 }
869
870 result.push_back(arg);
871 }
872
873 return result;
874 };
875 tool.appendArgumentsAdjuster(stripPchAdjuster);
876
877 auto ensureSourceIncludeAdjuster = [&inputRoot](const clang::tooling::CommandLineArguments &args, llvm::StringRef) {
878 clang::tooling::CommandLineArguments result = args;
879
880 const auto hasInclude = [&result](const std::string &dir) {
881 const std::string combined = "-I" + dir;
882 if (std::find(result.begin(), result.end(), combined) != result.end()) {
883 return true;
884 }
885 for (std::size_t i = 0; i + 1 < result.size(); ++i) {
886 if (result[i] == "-I" && result[i + 1] == dir) {
887 return true;
888 }
889 }
890 return false;
891 };
892
893 const auto appendInclude = [&result](const std::string &dir) {
894 result.push_back("-I");
895 result.push_back(dir);
896 };
897
898 const std::string libDir = (inputRoot / "lib").generic_string();
899 const std::string srcDir = (inputRoot / "src").generic_string();
900
901 if (!hasInclude(libDir)) {
902 appendInclude(libDir);
903 }
904 if (!hasInclude(srcDir)) {
905 appendInclude(srcDir);
906 }
907
908 return result;
909 };
910 tool.appendArgumentsAdjuster(ensureSourceIncludeAdjuster);
911
912 // Instrumented headers are now hard-linked into the output tree, so we can
913 // rely on the original include paths without further adjustment.
914
915 // NOTE: -skip-function-bodies optimization removed - it was causing errors with LibTooling
916 // LibTooling handles arguments differently than the clang driver, so -Xclang flags don't work
917 // The optimization wasn't providing significant benefit anyway
918
919 // Optimization: strip unnecessary compilation flags
920 auto stripUnnecessaryFlags = [](const clang::tooling::CommandLineArguments &args, llvm::StringRef) {
921 clang::tooling::CommandLineArguments result;
922
923 for (size_t i = 0; i < args.size(); ++i) {
924 const std::string &arg = args[i];
925
926 // Skip sanitizer flags (not needed for instrumentation, slow down parsing)
927 if (arg.find("-fsanitize") != std::string::npos)
928 continue;
929 if (arg.find("-fno-sanitize") != std::string::npos)
930 continue;
931 if (arg.find("sanitize") != std::string::npos)
932 continue;
933
934 // Skip debug info generation flags (not needed, slow down codegen)
935 if (arg == "-g" || arg == "-g2" || arg == "-g3")
936 continue;
937 if (arg == "-gcolumn-info")
938 continue;
939 if (arg == "-fstandalone-debug")
940 continue;
941 if (arg.find("-gcodeview") != std::string::npos)
942 continue;
943 if (arg.find("-gdwarf") != std::string::npos)
944 continue;
945
946 // Skip stack protector (not needed for instrumentation)
947 if (arg.find("-fstack-protector") != std::string::npos)
948 continue;
949
950 // Skip frame pointer flags
951 if (arg.find("-fno-omit-frame-pointer") != std::string::npos)
952 continue;
953 if (arg.find("-fomit-frame-pointer") != std::string::npos)
954 continue;
955
956 // Skip optimization-related debug flags
957 if (arg == "-fno-inline")
958 continue;
959 if (arg == "-fno-eliminate-unused-debug-types")
960 continue;
961
962 result.push_back(arg);
963 }
964
965 // NOTE: -w flag removed - it was causing "no such file or directory" errors with LibTooling
966 // Warnings are already suppressed by the stripped flags above
967
968 return result;
969 };
970 tool.appendArgumentsAdjuster(stripUnnecessaryFlags);
971
972 InstrumentationActionFactory actionFactory(outputDir, inputRoot, !FileIncludeFilters.empty(),
973 !FunctionIncludeFilters.empty(), logMacroInvocations, logMacroExpansions);
974 const int executionResult = tool.run(&actionFactory);
975 if (executionResult != 0) {
976 llvm::errs() << "Instrumenter failed with code " << executionResult << "\n";
977 }
978 return executionResult;
979}
action_args_t args
Definition actions.c:78
constexpr unsigned kMacroFlagExpansion
Definition tool.cpp:115
void unregisterOutputPath(const std::string &path)
Definition tool.cpp:52
std::mutex & outputRegistryMutex()
Definition tool.cpp:35
constexpr unsigned kMacroFlagInvocation
Definition tool.cpp:116
int main(int argc, const char **argv)
Definition tool.cpp:705
std::unordered_set< std::string > & outputRegistry()
Definition tool.cpp:40
constexpr unsigned kMacroFlagNone
Definition tool.cpp:114
bool registerOutputPath(const std::string &path)
Definition tool.cpp:45