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