| //===--- SarifDiagnostics.cpp - Sarif Diagnostics for Paths -----*- C++ -*-===// |
| // |
| // 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 |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // This file defines the SarifDiagnostics object. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "clang/Analysis/PathDiagnostic.h" |
| #include "clang/Basic/Version.h" |
| #include "clang/Lex/Preprocessor.h" |
| #include "clang/StaticAnalyzer/Core/AnalyzerOptions.h" |
| #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/ADT/StringMap.h" |
| #include "llvm/Support/JSON.h" |
| #include "llvm/Support/Path.h" |
| |
| using namespace llvm; |
| using namespace clang; |
| using namespace ento; |
| |
| namespace { |
| class SarifDiagnostics : public PathDiagnosticConsumer { |
| std::string OutputFile; |
| |
| public: |
| SarifDiagnostics(AnalyzerOptions &, const std::string &Output) |
| : OutputFile(Output) {} |
| ~SarifDiagnostics() override = default; |
| |
| void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, |
| FilesMade *FM) override; |
| |
| StringRef getName() const override { return "SarifDiagnostics"; } |
| PathGenerationScheme getGenerationScheme() const override { return Minimal; } |
| bool supportsLogicalOpControlFlow() const override { return true; } |
| bool supportsCrossFileDiagnostics() const override { return true; } |
| }; |
| } // end anonymous namespace |
| |
| void ento::createSarifDiagnosticConsumer( |
| AnalyzerOptions &AnalyzerOpts, PathDiagnosticConsumers &C, |
| const std::string &Output, const Preprocessor &, |
| const cross_tu::CrossTranslationUnitContext &) { |
| C.push_back(new SarifDiagnostics(AnalyzerOpts, Output)); |
| } |
| |
| static StringRef getFileName(const FileEntry &FE) { |
| StringRef Filename = FE.tryGetRealPathName(); |
| if (Filename.empty()) |
| Filename = FE.getName(); |
| return Filename; |
| } |
| |
| static std::string percentEncodeURICharacter(char C) { |
| // RFC 3986 claims alpha, numeric, and this handful of |
| // characters are not reserved for the path component and |
| // should be written out directly. Otherwise, percent |
| // encode the character and write that out instead of the |
| // reserved character. |
| if (llvm::isAlnum(C) || |
| StringRef::npos != StringRef("-._~:@!$&'()*+,;=").find(C)) |
| return std::string(&C, 1); |
| return "%" + llvm::toHex(StringRef(&C, 1)); |
| } |
| |
| static std::string fileNameToURI(StringRef Filename) { |
| llvm::SmallString<32> Ret = StringRef("file://"); |
| |
| // Get the root name to see if it has a URI authority. |
| StringRef Root = sys::path::root_name(Filename); |
| if (Root.startswith("//")) { |
| // There is an authority, so add it to the URI. |
| Ret += Root.drop_front(2).str(); |
| } else if (!Root.empty()) { |
| // There is no authority, so end the component and add the root to the URI. |
| Ret += Twine("/" + Root).str(); |
| } |
| |
| auto Iter = sys::path::begin(Filename), End = sys::path::end(Filename); |
| assert(Iter != End && "Expected there to be a non-root path component."); |
| // Add the rest of the path components, encoding any reserved characters; |
| // we skip past the first path component, as it was handled it above. |
| std::for_each(++Iter, End, [&Ret](StringRef Component) { |
| // For reasons unknown to me, we may get a backslash with Windows native |
| // paths for the initial backslash following the drive component, which |
| // we need to ignore as a URI path part. |
| if (Component == "\\") |
| return; |
| |
| // Add the separator between the previous path part and the one being |
| // currently processed. |
| Ret += "/"; |
| |
| // URI encode the part. |
| for (char C : Component) { |
| Ret += percentEncodeURICharacter(C); |
| } |
| }); |
| |
| return Ret.str().str(); |
| } |
| |
| static json::Object createArtifactLocation(const FileEntry &FE) { |
| return json::Object{{"uri", fileNameToURI(getFileName(FE))}}; |
| } |
| |
| static json::Object createArtifact(const FileEntry &FE) { |
| return json::Object{{"location", createArtifactLocation(FE)}, |
| {"roles", json::Array{"resultFile"}}, |
| {"length", FE.getSize()}, |
| {"mimeType", "text/plain"}}; |
| } |
| |
| static json::Object createArtifactLocation(const FileEntry &FE, |
| json::Array &Artifacts) { |
| std::string FileURI = fileNameToURI(getFileName(FE)); |
| |
| // See if the Artifacts array contains this URI already. If it does not, |
| // create a new artifact object to add to the array. |
| auto I = llvm::find_if(Artifacts, [&](const json::Value &File) { |
| if (const json::Object *Obj = File.getAsObject()) { |
| if (const json::Object *FileLoc = Obj->getObject("location")) { |
| Optional<StringRef> URI = FileLoc->getString("uri"); |
| return URI && URI->equals(FileURI); |
| } |
| } |
| return false; |
| }); |
| |
| // Calculate the index within the artifact array so it can be stored in |
| // the JSON object. |
| auto Index = static_cast<unsigned>(std::distance(Artifacts.begin(), I)); |
| if (I == Artifacts.end()) |
| Artifacts.push_back(createArtifact(FE)); |
| |
| return json::Object{{"uri", FileURI}, {"index", Index}}; |
| } |
| |
| static json::Object createTextRegion(SourceRange R, const SourceManager &SM) { |
| json::Object Region{ |
| {"startLine", SM.getExpansionLineNumber(R.getBegin())}, |
| {"startColumn", SM.getExpansionColumnNumber(R.getBegin())}, |
| }; |
| if (R.getBegin() == R.getEnd()) { |
| Region["endColumn"] = SM.getExpansionColumnNumber(R.getBegin()); |
| } else { |
| Region["endLine"] = SM.getExpansionLineNumber(R.getEnd()); |
| Region["endColumn"] = SM.getExpansionColumnNumber(R.getEnd()) + 1; |
| } |
| return Region; |
| } |
| |
| static json::Object createPhysicalLocation(SourceRange R, const FileEntry &FE, |
| const SourceManager &SMgr, |
| json::Array &Artifacts) { |
| return json::Object{ |
| {{"artifactLocation", createArtifactLocation(FE, Artifacts)}, |
| {"region", createTextRegion(R, SMgr)}}}; |
| } |
| |
| enum class Importance { Important, Essential, Unimportant }; |
| |
| static StringRef importanceToStr(Importance I) { |
| switch (I) { |
| case Importance::Important: |
| return "important"; |
| case Importance::Essential: |
| return "essential"; |
| case Importance::Unimportant: |
| return "unimportant"; |
| } |
| llvm_unreachable("Fully covered switch is not so fully covered"); |
| } |
| |
| static json::Object createThreadFlowLocation(json::Object &&Location, |
| Importance I) { |
| return json::Object{{"location", std::move(Location)}, |
| {"importance", importanceToStr(I)}}; |
| } |
| |
| static json::Object createMessage(StringRef Text) { |
| return json::Object{{"text", Text.str()}}; |
| } |
| |
| static json::Object createLocation(json::Object &&PhysicalLocation, |
| StringRef Message = "") { |
| json::Object Ret{{"physicalLocation", std::move(PhysicalLocation)}}; |
| if (!Message.empty()) |
| Ret.insert({"message", createMessage(Message)}); |
| return Ret; |
| } |
| |
| static Importance calculateImportance(const PathDiagnosticPiece &Piece) { |
| switch (Piece.getKind()) { |
| case PathDiagnosticPiece::Call: |
| case PathDiagnosticPiece::Macro: |
| case PathDiagnosticPiece::Note: |
| case PathDiagnosticPiece::PopUp: |
| // FIXME: What should be reported here? |
| break; |
| case PathDiagnosticPiece::Event: |
| return Piece.getTagStr() == "ConditionBRVisitor" ? Importance::Important |
| : Importance::Essential; |
| case PathDiagnosticPiece::ControlFlow: |
| return Importance::Unimportant; |
| } |
| return Importance::Unimportant; |
| } |
| |
| static json::Object createThreadFlow(const PathPieces &Pieces, |
| json::Array &Artifacts) { |
| const SourceManager &SMgr = Pieces.front()->getLocation().getManager(); |
| json::Array Locations; |
| for (const auto &Piece : Pieces) { |
| const PathDiagnosticLocation &P = Piece->getLocation(); |
| Locations.push_back(createThreadFlowLocation( |
| createLocation(createPhysicalLocation( |
| P.asRange(), |
| *P.asLocation().getExpansionLoc().getFileEntry(), |
| SMgr, Artifacts), |
| Piece->getString()), |
| calculateImportance(*Piece))); |
| } |
| return json::Object{{"locations", std::move(Locations)}}; |
| } |
| |
| static json::Object createCodeFlow(const PathPieces &Pieces, |
| json::Array &Artifacts) { |
| return json::Object{ |
| {"threadFlows", json::Array{createThreadFlow(Pieces, Artifacts)}}}; |
| } |
| |
| static json::Object createResult(const PathDiagnostic &Diag, |
| json::Array &Artifacts, |
| const StringMap<unsigned> &RuleMapping) { |
| const PathPieces &Path = Diag.path.flatten(false); |
| const SourceManager &SMgr = Path.front()->getLocation().getManager(); |
| |
| auto Iter = RuleMapping.find(Diag.getCheckerName()); |
| assert(Iter != RuleMapping.end() && "Rule ID is not in the array index map?"); |
| |
| return json::Object{ |
| {"message", createMessage(Diag.getVerboseDescription())}, |
| {"codeFlows", json::Array{createCodeFlow(Path, Artifacts)}}, |
| {"locations", |
| json::Array{createLocation(createPhysicalLocation( |
| Diag.getLocation().asRange(), |
| *Diag.getLocation().asLocation().getExpansionLoc().getFileEntry(), |
| SMgr, Artifacts))}}, |
| {"ruleIndex", Iter->getValue()}, |
| {"ruleId", Diag.getCheckerName()}}; |
| } |
| |
| static StringRef getRuleDescription(StringRef CheckName) { |
| return llvm::StringSwitch<StringRef>(CheckName) |
| #define GET_CHECKERS |
| #define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \ |
| .Case(FULLNAME, HELPTEXT) |
| #include "clang/StaticAnalyzer/Checkers/Checkers.inc" |
| #undef CHECKER |
| #undef GET_CHECKERS |
| ; |
| } |
| |
| static StringRef getRuleHelpURIStr(StringRef CheckName) { |
| return llvm::StringSwitch<StringRef>(CheckName) |
| #define GET_CHECKERS |
| #define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \ |
| .Case(FULLNAME, DOC_URI) |
| #include "clang/StaticAnalyzer/Checkers/Checkers.inc" |
| #undef CHECKER |
| #undef GET_CHECKERS |
| ; |
| } |
| |
| static json::Object createRule(const PathDiagnostic &Diag) { |
| StringRef CheckName = Diag.getCheckerName(); |
| json::Object Ret{ |
| {"fullDescription", createMessage(getRuleDescription(CheckName))}, |
| {"name", CheckName}, |
| {"id", CheckName}}; |
| |
| std::string RuleURI = getRuleHelpURIStr(CheckName); |
| if (!RuleURI.empty()) |
| Ret["helpUri"] = RuleURI; |
| |
| return Ret; |
| } |
| |
| static json::Array createRules(std::vector<const PathDiagnostic *> &Diags, |
| StringMap<unsigned> &RuleMapping) { |
| json::Array Rules; |
| llvm::StringSet<> Seen; |
| |
| llvm::for_each(Diags, [&](const PathDiagnostic *D) { |
| StringRef RuleID = D->getCheckerName(); |
| std::pair<llvm::StringSet<>::iterator, bool> P = Seen.insert(RuleID); |
| if (P.second) { |
| RuleMapping[RuleID] = Rules.size(); // Maps RuleID to an Array Index. |
| Rules.push_back(createRule(*D)); |
| } |
| }); |
| |
| return Rules; |
| } |
| |
| static json::Object createTool(std::vector<const PathDiagnostic *> &Diags, |
| StringMap<unsigned> &RuleMapping) { |
| return json::Object{ |
| {"driver", json::Object{{"name", "clang"}, |
| {"fullName", "clang static analyzer"}, |
| {"language", "en-US"}, |
| {"version", getClangFullVersion()}, |
| {"rules", createRules(Diags, RuleMapping)}}}}; |
| } |
| |
| static json::Object createRun(std::vector<const PathDiagnostic *> &Diags) { |
| json::Array Results, Artifacts; |
| StringMap<unsigned> RuleMapping; |
| json::Object Tool = createTool(Diags, RuleMapping); |
| |
| llvm::for_each(Diags, [&](const PathDiagnostic *D) { |
| Results.push_back(createResult(*D, Artifacts, RuleMapping)); |
| }); |
| |
| return json::Object{{"tool", std::move(Tool)}, |
| {"results", std::move(Results)}, |
| {"artifacts", std::move(Artifacts)}}; |
| } |
| |
| void SarifDiagnostics::FlushDiagnosticsImpl( |
| std::vector<const PathDiagnostic *> &Diags, FilesMade *) { |
| // We currently overwrite the file if it already exists. However, it may be |
| // useful to add a feature someday that allows the user to append a run to an |
| // existing SARIF file. One danger from that approach is that the size of the |
| // file can become large very quickly, so decoding into JSON to append a run |
| // may be an expensive operation. |
| std::error_code EC; |
| llvm::raw_fd_ostream OS(OutputFile, EC, llvm::sys::fs::OF_Text); |
| if (EC) { |
| llvm::errs() << "warning: could not create file: " << EC.message() << '\n'; |
| return; |
| } |
| json::Object Sarif{ |
| {"$schema", |
| "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"}, |
| {"version", "2.1.0"}, |
| {"runs", json::Array{createRun(Diags)}}}; |
| OS << llvm::formatv("{0:2}\n", json::Value(std::move(Sarif))); |
| } |