File indexing completed on 2024-04-28 16:57:50

0001 /*
0002   This file is part of the clazy static checker.
0003 
0004     Copyright (C) 2019 Christian Gagneraud <chgans@gmail.com>
0005 
0006     This library is free software; you can redistribute it and/or
0007     modify it under the terms of the GNU Library General Public
0008     License as published by the Free Software Foundation; either
0009     version 2 of the License, or (at your option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful,
0012     but WITHOUT ANY WARRANTY; without even the implied warranty of
0013     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014     Library General Public License for more details.
0015 
0016     You should have received a copy of the GNU Library General Public License
0017     along with this library; see the file COPYING.LIB.  If not, write to
0018     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0019     Boston, MA 02110-1301, USA.
0020 */
0021 
0022 #include "FixItExporter.h"
0023 #include "SourceCompatibilityHelpers.h"
0024 
0025 #include <clang/Frontend/FrontendDiagnostic.h>
0026 #include <clang/Tooling/DiagnosticsYaml.h>
0027 #include <clang/Basic/SourceManager.h>
0028 #include <clang/Rewrite/Frontend/FixItRewriter.h>
0029 
0030 // #define DEBUG_FIX_IT_EXPORTER
0031 
0032 using namespace clang;
0033 
0034 static clang::tooling::TranslationUnitDiagnostics& getTuDiag()
0035 {
0036     static clang::tooling::TranslationUnitDiagnostics s_tudiag;
0037     return s_tudiag;
0038 }
0039 
0040 FixItExporter::FixItExporter(DiagnosticsEngine &DiagEngine, SourceManager &SourceMgr,
0041                              const LangOptions &LangOpts, const std::string &exportFixes,
0042                              bool isClazyStandalone)
0043     : DiagEngine(DiagEngine)
0044     , SourceMgr(SourceMgr)
0045     , LangOpts(LangOpts)
0046     , exportFixes(exportFixes)
0047 {
0048     if (!isClazyStandalone) {
0049         // When using clazy as plugin each translation unit fixes goes to a separate YAML file
0050         getTuDiag().Diagnostics.clear();
0051     }
0052 
0053     Owner = DiagEngine.takeClient();
0054     Client = DiagEngine.getClient();
0055     DiagEngine.setClient(this, false);
0056 }
0057 
0058 FixItExporter::~FixItExporter()
0059 {
0060     if (Client)
0061         DiagEngine.setClient(Client, Owner.release() != nullptr);
0062 }
0063 
0064 void FixItExporter::BeginSourceFile(const LangOptions &LangOpts, const Preprocessor *PP)
0065 {
0066     if (Client)
0067         Client->BeginSourceFile(LangOpts, PP);
0068 
0069     const auto id = SourceMgr.getMainFileID();
0070     const auto entry = SourceMgr.getFileEntryForID(id);
0071     getTuDiag().MainSourceFile = static_cast<std::string>(entry->getName());
0072 }
0073 
0074 bool FixItExporter::IncludeInDiagnosticCounts() const
0075 {
0076     return Client ? Client->IncludeInDiagnosticCounts() : true;
0077 }
0078 
0079 void FixItExporter::EndSourceFile()
0080 {
0081     if (Client)
0082         Client->EndSourceFile();
0083 }
0084 
0085 tooling::Diagnostic FixItExporter::ConvertDiagnostic(const Diagnostic &Info)
0086 {
0087     SmallString<256> TmpMessageText;
0088     Info.FormatDiagnostic(TmpMessageText);
0089     // TODO: This returns an empty string: DiagEngine->getDiagnosticIDs()->getWarningOptionForDiag(Info.getID());
0090     // HACK: capture it at the end of the message: Message text [check-name]
0091 
0092     std::string checkName =
0093         static_cast<std::string>(DiagEngine.getDiagnosticIDs()->getWarningOptionForDiag(Info.getID()));
0094     std::string messageText;
0095 
0096     if (checkName.empty()) {
0097         // Non-built-in clang warnings have the [checkName] in the message
0098         messageText = TmpMessageText.slice(0, TmpMessageText.find_last_of('[') - 1).str();
0099 
0100         checkName = TmpMessageText.slice(TmpMessageText.find_last_of('[') + 3,
0101                                          TmpMessageText.find_last_of(']')).str();
0102     } else {
0103          messageText = TmpMessageText.c_str();
0104     }
0105 
0106 
0107     llvm::StringRef CurrentBuildDir; // Not needed?
0108 
0109     tooling::Diagnostic ToolingDiag(checkName,
0110                                     tooling::Diagnostic::Warning,
0111                                     CurrentBuildDir);
0112     // FIXME: Sometimes the file path is an empty string.
0113     if (Info.getLocation().isMacroID()) {
0114         auto MacroLoc = SourceMgr.getFileLoc(Info.getLocation());
0115         ToolingDiag.Message = tooling::DiagnosticMessage(messageText, SourceMgr, MacroLoc);
0116     } else {
0117         ToolingDiag.Message = tooling::DiagnosticMessage(messageText, SourceMgr, Info.getLocation());
0118     }
0119     return ToolingDiag;
0120 }
0121 
0122 tooling::Replacement FixItExporter::ConvertFixIt(const FixItHint &Hint)
0123 {
0124     // TODO: Proper handling of macros
0125     // https://stackoverflow.com/questions/24062989/clang-fails-replacing-a-statement-if-it-contains-a-macro
0126     tooling::Replacement Replacement;
0127     if (Hint.CodeToInsert.empty()) {
0128         if (Hint.InsertFromRange.isValid()) {
0129             clang::SourceLocation b(Hint.InsertFromRange.getBegin()), _e(Hint.InsertFromRange.getEnd());
0130             if (b.isMacroID())
0131                 b = SourceMgr.getSpellingLoc(b);
0132             if (_e.isMacroID())
0133                 _e = SourceMgr.getSpellingLoc(_e);
0134             clang::SourceLocation e(clang::Lexer::getLocForEndOfToken(_e, 0, SourceMgr, LangOpts));
0135             StringRef Text(SourceMgr.getCharacterData(b),
0136                            SourceMgr.getCharacterData(e) - SourceMgr.getCharacterData(b));
0137             return tooling::Replacement(SourceMgr, Hint.RemoveRange, Text);
0138         }
0139         return tooling::Replacement(SourceMgr, Hint.RemoveRange, "");
0140     }
0141     return tooling::Replacement(SourceMgr, Hint.RemoveRange, Hint.CodeToInsert);
0142 }
0143 
0144 void FixItExporter::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, const Diagnostic &Info)
0145 {
0146     // Default implementation (Warnings/errors count).
0147     DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info);
0148 
0149     // Let original client do it's handling
0150     if (Client)
0151         Client->HandleDiagnostic(DiagLevel, Info);
0152 
0153     // Convert and record warning diagnostics and their notes
0154     if (DiagLevel == DiagnosticsEngine::Warning) {
0155         auto ToolingDiag = ConvertDiagnostic(Info);
0156         for (unsigned Idx = 0, Last = Info.getNumFixItHints();
0157              Idx < Last; ++Idx) {
0158             const FixItHint &Hint = Info.getFixItHint(Idx);
0159             const auto replacement = ConvertFixIt(Hint);
0160 #ifdef DEBUG_FIX_IT_EXPORTER
0161             const auto FileName = SourceMgr.getFilename(Info.getLocation());
0162             llvm::errs() << "Handling Fixit #" << Idx << " for " << FileName.str() << "\n";
0163             llvm::errs() << "F: "
0164                       << Hint.RemoveRange.getBegin().printToString(SourceMgr) << ":"
0165                       << Hint.RemoveRange.getEnd().printToString(SourceMgr) << " "
0166                       << Hint.InsertFromRange.getBegin().printToString(SourceMgr) << ":"
0167                       << Hint.InsertFromRange.getEnd().printToString(SourceMgr) << " "
0168                       << Hint.BeforePreviousInsertions << " "
0169                       << Hint.CodeToInsert << "\n";
0170             llvm::errs() << "R: " << replacement.toString() << "\n";
0171 #endif
0172             clang::tooling::Replacements &Replacements = clazy::DiagnosticFix(ToolingDiag, replacement.getFilePath());
0173             llvm::Error error = Replacements.add(ConvertFixIt(Hint));
0174             if (error) {
0175                 Diag(Info.getLocation(), diag::note_fixit_failed);
0176             }
0177         }
0178         getTuDiag().Diagnostics.push_back(ToolingDiag);
0179         m_recordNotes = true;
0180     }
0181     // FIXME: We do not receive notes.
0182     else if (DiagLevel == DiagnosticsEngine::Note && m_recordNotes) {
0183 #ifdef DEBUG_FIX_IT_EXPORTER
0184         const auto FileName = SourceMgr.getFilename(Info.getLocation());
0185         llvm::errs() << "Handling Note for " << FileName.str() << "\n";
0186 #endif
0187         auto diags = getTuDiag().Diagnostics.back();
0188         auto diag = ConvertDiagnostic(Info);
0189         diags.Notes.append(1, diag.Message);
0190     }
0191     else {
0192         m_recordNotes = false;
0193     }
0194 }
0195 
0196 void FixItExporter::Export()
0197 {
0198     auto tuDiag = getTuDiag();
0199     if (!tuDiag.Diagnostics.empty()) {
0200         std::error_code EC;
0201         llvm::raw_fd_ostream OS(exportFixes, EC, llvm::sys::fs::OF_None);
0202         llvm::yaml::Output YAML(OS);
0203         YAML << getTuDiag();
0204     }
0205 }
0206 
0207 void FixItExporter::Diag(SourceLocation Loc, unsigned DiagID)
0208 {
0209     // When producing this diagnostic, we temporarily bypass ourselves,
0210     // clear out any current diagnostic, and let the downstream client
0211     // format the diagnostic.
0212     DiagEngine.setClient(Client, false);
0213     DiagEngine.Clear();
0214     DiagEngine.Report(Loc, DiagID);
0215     DiagEngine.setClient(this, false);
0216 }