Marek Kurdej | 92ecb51 | 2017-03-23 16:32:06 +0000 | [diff] [blame] | 1 | //===--- tools/extra/clang-tidy/ClangTidyDiagnosticConsumer.cpp ----------=== //
|
| 2 | //
|
| 3 | // The LLVM Compiler Infrastructure
|
| 4 | //
|
| 5 | // This file is distributed under the University of Illinois Open Source
|
| 6 | // License. See LICENSE.TXT for details.
|
| 7 | //
|
| 8 | //===----------------------------------------------------------------------===//
|
| 9 | ///
|
| 10 | /// \file This file implements ClangTidyDiagnosticConsumer, ClangTidyContext
|
| 11 | /// and ClangTidyError classes.
|
| 12 | ///
|
| 13 | /// This tool uses the Clang Tooling infrastructure, see
|
| 14 | /// http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
|
| 15 | /// for details on setting it up with LLVM source tree.
|
| 16 | ///
|
| 17 | //===----------------------------------------------------------------------===//
|
| 18 |
|
| 19 | #include "ClangTidyDiagnosticConsumer.h"
|
| 20 | #include "ClangTidyOptions.h"
|
| 21 | #include "clang/AST/ASTDiagnostic.h"
|
| 22 | #include "clang/Basic/DiagnosticOptions.h"
|
| 23 | #include "clang/Frontend/DiagnosticRenderer.h"
|
| 24 | #include "llvm/ADT/SmallString.h"
|
| 25 | #include <tuple>
|
| 26 | #include <vector>
|
| 27 | using namespace clang;
|
| 28 | using namespace tidy;
|
| 29 |
|
| 30 | namespace {
|
| 31 | class ClangTidyDiagnosticRenderer : public DiagnosticRenderer {
|
| 32 | public:
|
| 33 | ClangTidyDiagnosticRenderer(const LangOptions &LangOpts,
|
| 34 | DiagnosticOptions *DiagOpts,
|
| 35 | ClangTidyError &Error)
|
| 36 | : DiagnosticRenderer(LangOpts, DiagOpts), Error(Error) {}
|
| 37 |
|
| 38 | protected:
|
| 39 | void emitDiagnosticMessage(SourceLocation Loc, PresumedLoc PLoc,
|
| 40 | DiagnosticsEngine::Level Level, StringRef Message,
|
| 41 | ArrayRef<CharSourceRange> Ranges,
|
| 42 | const SourceManager *SM,
|
| 43 | DiagOrStoredDiag Info) override {
|
| 44 | // Remove check name from the message.
|
| 45 | // FIXME: Remove this once there's a better way to pass check names than
|
| 46 | // appending the check name to the message in ClangTidyContext::diag and
|
| 47 | // using getCustomDiagID.
|
| 48 | std::string CheckNameInMessage = " [" + Error.DiagnosticName + "]";
|
| 49 | if (Message.endswith(CheckNameInMessage))
|
| 50 | Message = Message.substr(0, Message.size() - CheckNameInMessage.size());
|
| 51 |
|
| 52 | auto TidyMessage = Loc.isValid()
|
| 53 | ? tooling::DiagnosticMessage(Message, *SM, Loc)
|
| 54 | : tooling::DiagnosticMessage(Message);
|
| 55 | if (Level == DiagnosticsEngine::Note) {
|
| 56 | Error.Notes.push_back(TidyMessage);
|
| 57 | return;
|
| 58 | }
|
| 59 | assert(Error.Message.Message.empty() && "Overwriting a diagnostic message");
|
| 60 | Error.Message = TidyMessage;
|
| 61 | }
|
| 62 |
|
| 63 | void emitDiagnosticLoc(SourceLocation Loc, PresumedLoc PLoc,
|
| 64 | DiagnosticsEngine::Level Level,
|
| 65 | ArrayRef<CharSourceRange> Ranges,
|
| 66 | const SourceManager &SM) override {}
|
| 67 |
|
| 68 | void emitCodeContext(SourceLocation Loc, DiagnosticsEngine::Level Level,
|
| 69 | SmallVectorImpl<CharSourceRange> &Ranges,
|
| 70 | ArrayRef<FixItHint> Hints,
|
| 71 | const SourceManager &SM) override {
|
| 72 | assert(Loc.isValid());
|
| 73 | for (const auto &FixIt : Hints) {
|
| 74 | CharSourceRange Range = FixIt.RemoveRange;
|
| 75 | assert(Range.getBegin().isValid() && Range.getEnd().isValid() &&
|
| 76 | "Invalid range in the fix-it hint.");
|
| 77 | assert(Range.getBegin().isFileID() && Range.getEnd().isFileID() &&
|
| 78 | "Only file locations supported in fix-it hints.");
|
| 79 |
|
| 80 | tooling::Replacement Replacement(SM, Range, FixIt.CodeToInsert);
|
| 81 | llvm::Error Err = Error.Fix[Replacement.getFilePath()].add(Replacement);
|
| 82 | // FIXME: better error handling (at least, don't let other replacements be
|
| 83 | // applied).
|
| 84 | if (Err) {
|
| 85 | llvm::errs() << "Fix conflicts with existing fix! "
|
| 86 | << llvm::toString(std::move(Err)) << "\n";
|
| 87 | assert(false && "Fix conflicts with existing fix!");
|
| 88 | }
|
| 89 | }
|
| 90 | }
|
| 91 |
|
| 92 | void emitIncludeLocation(SourceLocation Loc, PresumedLoc PLoc,
|
| 93 | const SourceManager &SM) override {}
|
| 94 |
|
| 95 | void emitImportLocation(SourceLocation Loc, PresumedLoc PLoc,
|
| 96 | StringRef ModuleName,
|
| 97 | const SourceManager &SM) override {}
|
| 98 |
|
| 99 | void emitBuildingModuleLocation(SourceLocation Loc, PresumedLoc PLoc,
|
| 100 | StringRef ModuleName,
|
| 101 | const SourceManager &SM) override {}
|
| 102 |
|
| 103 | void endDiagnostic(DiagOrStoredDiag D,
|
| 104 | DiagnosticsEngine::Level Level) override {
|
| 105 | assert(!Error.Message.Message.empty() && "Message has not been set");
|
| 106 | }
|
| 107 |
|
| 108 | private:
|
| 109 | ClangTidyError &Error;
|
| 110 | };
|
| 111 | } // end anonymous namespace
|
| 112 |
|
| 113 | ClangTidyError::ClangTidyError(StringRef CheckName,
|
| 114 | ClangTidyError::Level DiagLevel,
|
| 115 | StringRef BuildDirectory, bool IsWarningAsError)
|
| 116 | : tooling::Diagnostic(CheckName, DiagLevel, BuildDirectory),
|
| 117 | IsWarningAsError(IsWarningAsError) {}
|
| 118 |
|
| 119 | // Returns true if GlobList starts with the negative indicator ('-'), removes it
|
| 120 | // from the GlobList.
|
| 121 | static bool ConsumeNegativeIndicator(StringRef &GlobList) {
|
| 122 | GlobList = GlobList.trim(' ');
|
| 123 | if (GlobList.startswith("-")) {
|
| 124 | GlobList = GlobList.substr(1);
|
| 125 | return true;
|
| 126 | }
|
| 127 | return false;
|
| 128 | }
|
| 129 | // Converts first glob from the comma-separated list of globs to Regex and
|
| 130 | // removes it and the trailing comma from the GlobList.
|
| 131 | static llvm::Regex ConsumeGlob(StringRef &GlobList) {
|
| 132 | StringRef UntrimmedGlob = GlobList.substr(0, GlobList.find(','));
|
| 133 | StringRef Glob = UntrimmedGlob.trim(' ');
|
| 134 | GlobList = GlobList.substr(UntrimmedGlob.size() + 1);
|
| 135 | SmallString<128> RegexText("^");
|
| 136 | StringRef MetaChars("()^$|*+?.[]\\{}");
|
| 137 | for (char C : Glob) {
|
| 138 | if (C == '*')
|
| 139 | RegexText.push_back('.');
|
| 140 | else if (MetaChars.find(C) != StringRef::npos)
|
| 141 | RegexText.push_back('\\');
|
| 142 | RegexText.push_back(C);
|
| 143 | }
|
| 144 | RegexText.push_back('$');
|
| 145 | return llvm::Regex(RegexText);
|
| 146 | }
|
| 147 |
|
| 148 | GlobList::GlobList(StringRef Globs)
|
| 149 | : Positive(!ConsumeNegativeIndicator(Globs)), Regex(ConsumeGlob(Globs)),
|
| 150 | NextGlob(Globs.empty() ? nullptr : new GlobList(Globs)) {}
|
| 151 |
|
| 152 | bool GlobList::contains(StringRef S, bool Contains) {
|
| 153 | if (Regex.match(S))
|
| 154 | Contains = Positive;
|
| 155 |
|
| 156 | if (NextGlob)
|
| 157 | Contains = NextGlob->contains(S, Contains);
|
| 158 | return Contains;
|
| 159 | }
|
| 160 |
|
| 161 | ClangTidyContext::ClangTidyContext(
|
| 162 | std::unique_ptr<ClangTidyOptionsProvider> OptionsProvider)
|
| 163 | : DiagEngine(nullptr), OptionsProvider(std::move(OptionsProvider)),
|
| 164 | Profile(nullptr) {
|
| 165 | // Before the first translation unit we can get errors related to command-line
|
| 166 | // parsing, use empty string for the file name in this case.
|
| 167 | setCurrentFile("");
|
| 168 | }
|
| 169 |
|
| 170 | DiagnosticBuilder ClangTidyContext::diag(
|
| 171 | StringRef CheckName, SourceLocation Loc, StringRef Description,
|
| 172 | DiagnosticIDs::Level Level /* = DiagnosticIDs::Warning*/) {
|
| 173 | assert(Loc.isValid());
|
| 174 | unsigned ID = DiagEngine->getDiagnosticIDs()->getCustomDiagID(
|
| 175 | Level, (Description + " [" + CheckName + "]").str());
|
| 176 | CheckNamesByDiagnosticID.try_emplace(ID, CheckName);
|
| 177 | return DiagEngine->Report(Loc, ID);
|
| 178 | }
|
| 179 |
|
| 180 | void ClangTidyContext::setDiagnosticsEngine(DiagnosticsEngine *Engine) {
|
| 181 | DiagEngine = Engine;
|
| 182 | }
|
| 183 |
|
| 184 | void ClangTidyContext::setSourceManager(SourceManager *SourceMgr) {
|
| 185 | DiagEngine->setSourceManager(SourceMgr);
|
| 186 | }
|
| 187 |
|
| 188 | void ClangTidyContext::setCurrentFile(StringRef File) {
|
| 189 | CurrentFile = File;
|
| 190 | CurrentOptions = getOptionsForFile(CurrentFile);
|
| 191 | CheckFilter.reset(new GlobList(*getOptions().Checks));
|
| 192 | WarningAsErrorFilter.reset(new GlobList(*getOptions().WarningsAsErrors));
|
| 193 | }
|
| 194 |
|
| 195 | void ClangTidyContext::setASTContext(ASTContext *Context) {
|
| 196 | DiagEngine->SetArgToStringFn(&FormatASTNodeDiagnosticArgument, Context);
|
| 197 | LangOpts = Context->getLangOpts();
|
| 198 | }
|
| 199 |
|
| 200 | const ClangTidyGlobalOptions &ClangTidyContext::getGlobalOptions() const {
|
| 201 | return OptionsProvider->getGlobalOptions();
|
| 202 | }
|
| 203 |
|
| 204 | const ClangTidyOptions &ClangTidyContext::getOptions() const {
|
| 205 | return CurrentOptions;
|
| 206 | }
|
| 207 |
|
| 208 | ClangTidyOptions ClangTidyContext::getOptionsForFile(StringRef File) const {
|
| 209 | // Merge options on top of getDefaults() as a safeguard against options with
|
| 210 | // unset values.
|
| 211 | return ClangTidyOptions::getDefaults().mergeWith(
|
| 212 | OptionsProvider->getOptions(File));
|
| 213 | }
|
| 214 |
|
| 215 | void ClangTidyContext::setCheckProfileData(ProfileData *P) { Profile = P; }
|
| 216 |
|
| 217 | GlobList &ClangTidyContext::getChecksFilter() {
|
| 218 | assert(CheckFilter != nullptr);
|
| 219 | return *CheckFilter;
|
| 220 | }
|
| 221 |
|
| 222 | GlobList &ClangTidyContext::getWarningAsErrorFilter() {
|
| 223 | assert(WarningAsErrorFilter != nullptr);
|
| 224 | return *WarningAsErrorFilter;
|
| 225 | }
|
| 226 |
|
| 227 | /// \brief Store a \c ClangTidyError.
|
| 228 | void ClangTidyContext::storeError(const ClangTidyError &Error) {
|
| 229 | Errors.push_back(Error);
|
| 230 | }
|
| 231 |
|
| 232 | StringRef ClangTidyContext::getCheckName(unsigned DiagnosticID) const {
|
| 233 | llvm::DenseMap<unsigned, std::string>::const_iterator I =
|
| 234 | CheckNamesByDiagnosticID.find(DiagnosticID);
|
| 235 | if (I != CheckNamesByDiagnosticID.end())
|
| 236 | return I->second;
|
| 237 | return "";
|
| 238 | }
|
| 239 |
|
| 240 | ClangTidyDiagnosticConsumer::ClangTidyDiagnosticConsumer(ClangTidyContext &Ctx)
|
| 241 | : Context(Ctx), LastErrorRelatesToUserCode(false),
|
| 242 | LastErrorPassesLineFilter(false), LastErrorWasIgnored(false) {
|
| 243 | IntrusiveRefCntPtr<DiagnosticOptions> DiagOpts = new DiagnosticOptions();
|
| 244 | Diags.reset(new DiagnosticsEngine(
|
| 245 | IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs), &*DiagOpts, this,
|
| 246 | /*ShouldOwnClient=*/false));
|
| 247 | Context.setDiagnosticsEngine(Diags.get());
|
| 248 | }
|
| 249 |
|
| 250 | void ClangTidyDiagnosticConsumer::finalizeLastError() {
|
| 251 | if (!Errors.empty()) {
|
| 252 | ClangTidyError &Error = Errors.back();
|
| 253 | if (!Context.getChecksFilter().contains(Error.DiagnosticName) &&
|
| 254 | Error.DiagLevel != ClangTidyError::Error) {
|
| 255 | ++Context.Stats.ErrorsIgnoredCheckFilter;
|
| 256 | Errors.pop_back();
|
| 257 | } else if (!LastErrorRelatesToUserCode) {
|
| 258 | ++Context.Stats.ErrorsIgnoredNonUserCode;
|
| 259 | Errors.pop_back();
|
| 260 | } else if (!LastErrorPassesLineFilter) {
|
| 261 | ++Context.Stats.ErrorsIgnoredLineFilter;
|
| 262 | Errors.pop_back();
|
| 263 | } else {
|
| 264 | ++Context.Stats.ErrorsDisplayed;
|
| 265 | }
|
| 266 | }
|
| 267 | LastErrorRelatesToUserCode = false;
|
| 268 | LastErrorPassesLineFilter = false;
|
| 269 | }
|
| 270 |
|
| 271 | static bool LineIsMarkedWithNOLINT(SourceManager &SM, SourceLocation Loc) {
|
| 272 | bool Invalid;
|
| 273 | const char *CharacterData = SM.getCharacterData(Loc, &Invalid);
|
| 274 | if (Invalid)
|
| 275 | return false;
|
| 276 |
|
| 277 | // Check if there's a NOLINT on this line.
|
| 278 | const char *P = CharacterData;
|
| 279 | while (*P != '\0' && *P != '\r' && *P != '\n')
|
| 280 | ++P;
|
| 281 | StringRef RestOfLine(CharacterData, P - CharacterData + 1);
|
| 282 | // FIXME: Handle /\bNOLINT\b(\([^)]*\))?/ as cpplint.py does.
|
| 283 | if (RestOfLine.find("NOLINT") != StringRef::npos)
|
| 284 | return true;
|
| 285 |
|
| 286 | // Check if there's a NOLINTNEXTLINE on the previous line.
|
| 287 | const char *BufBegin =
|
| 288 | SM.getCharacterData(SM.getLocForStartOfFile(SM.getFileID(Loc)), &Invalid);
|
| 289 | if (Invalid || P == BufBegin)
|
| 290 | return false;
|
| 291 |
|
| 292 | // Scan backwards over the current line.
|
| 293 | P = CharacterData;
|
| 294 | while (P != BufBegin && *P != '\n')
|
| 295 | --P;
|
| 296 |
|
| 297 | // If we reached the begin of the file there is no line before it.
|
| 298 | if (P == BufBegin)
|
| 299 | return false;
|
| 300 |
|
| 301 | // Skip over the newline.
|
| 302 | --P;
|
| 303 | const char *LineEnd = P;
|
| 304 |
|
| 305 | // Now we're on the previous line. Skip to the beginning of it.
|
| 306 | while (P != BufBegin && *P != '\n')
|
| 307 | --P;
|
| 308 |
|
| 309 | RestOfLine = StringRef(P, LineEnd - P + 1);
|
| 310 | if (RestOfLine.find("NOLINTNEXTLINE") != StringRef::npos)
|
| 311 | return true;
|
| 312 |
|
| 313 | return false;
|
| 314 | }
|
| 315 |
|
| 316 | static bool LineIsMarkedWithNOLINTinMacro(SourceManager &SM,
|
| 317 | SourceLocation Loc) {
|
| 318 | while (true) {
|
| 319 | if (LineIsMarkedWithNOLINT(SM, Loc))
|
| 320 | return true;
|
| 321 | if (!Loc.isMacroID())
|
| 322 | return false;
|
| 323 | Loc = SM.getImmediateExpansionRange(Loc).first;
|
| 324 | }
|
| 325 | return false;
|
| 326 | }
|
| 327 |
|
| 328 | void ClangTidyDiagnosticConsumer::HandleDiagnostic(
|
| 329 | DiagnosticsEngine::Level DiagLevel, const Diagnostic &Info) {
|
| 330 | if (LastErrorWasIgnored && DiagLevel == DiagnosticsEngine::Note)
|
| 331 | return;
|
| 332 |
|
| 333 | if (Info.getLocation().isValid() && DiagLevel != DiagnosticsEngine::Error &&
|
| 334 | DiagLevel != DiagnosticsEngine::Fatal &&
|
| 335 | LineIsMarkedWithNOLINTinMacro(Diags->getSourceManager(),
|
| 336 | Info.getLocation())) {
|
| 337 | ++Context.Stats.ErrorsIgnoredNOLINT;
|
| 338 | // Ignored a warning, should ignore related notes as well
|
| 339 | LastErrorWasIgnored = true;
|
| 340 | return;
|
| 341 | }
|
| 342 |
|
| 343 | LastErrorWasIgnored = false;
|
| 344 | // Count warnings/errors.
|
| 345 | DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info);
|
| 346 |
|
| 347 | if (DiagLevel == DiagnosticsEngine::Note) {
|
| 348 | assert(!Errors.empty() &&
|
| 349 | "A diagnostic note can only be appended to a message.");
|
| 350 | } else {
|
| 351 | finalizeLastError();
|
| 352 | StringRef WarningOption =
|
| 353 | Context.DiagEngine->getDiagnosticIDs()->getWarningOptionForDiag(
|
| 354 | Info.getID());
|
| 355 | std::string CheckName = !WarningOption.empty()
|
| 356 | ? ("clang-diagnostic-" + WarningOption).str()
|
| 357 | : Context.getCheckName(Info.getID()).str();
|
| 358 |
|
| 359 | if (CheckName.empty()) {
|
| 360 | // This is a compiler diagnostic without a warning option. Assign check
|
| 361 | // name based on its level.
|
| 362 | switch (DiagLevel) {
|
| 363 | case DiagnosticsEngine::Error:
|
| 364 | case DiagnosticsEngine::Fatal:
|
| 365 | CheckName = "clang-diagnostic-error";
|
| 366 | break;
|
| 367 | case DiagnosticsEngine::Warning:
|
| 368 | CheckName = "clang-diagnostic-warning";
|
| 369 | break;
|
| 370 | default:
|
| 371 | CheckName = "clang-diagnostic-unknown";
|
| 372 | break;
|
| 373 | }
|
| 374 | }
|
| 375 |
|
| 376 | ClangTidyError::Level Level = ClangTidyError::Warning;
|
| 377 | if (DiagLevel == DiagnosticsEngine::Error ||
|
| 378 | DiagLevel == DiagnosticsEngine::Fatal) {
|
| 379 | // Force reporting of Clang errors regardless of filters and non-user
|
| 380 | // code.
|
| 381 | Level = ClangTidyError::Error;
|
| 382 | LastErrorRelatesToUserCode = true;
|
| 383 | LastErrorPassesLineFilter = true;
|
| 384 | }
|
| 385 | bool IsWarningAsError =
|
| 386 | DiagLevel == DiagnosticsEngine::Warning &&
|
| 387 | Context.getWarningAsErrorFilter().contains(CheckName);
|
| 388 | Errors.emplace_back(CheckName, Level, Context.getCurrentBuildDirectory(),
|
| 389 | IsWarningAsError);
|
| 390 | }
|
| 391 |
|
| 392 | ClangTidyDiagnosticRenderer Converter(
|
| 393 | Context.getLangOpts(), &Context.DiagEngine->getDiagnosticOptions(),
|
| 394 | Errors.back());
|
| 395 | SmallString<100> Message;
|
| 396 | Info.FormatDiagnostic(Message);
|
| 397 | SourceManager *Sources = nullptr;
|
| 398 | if (Info.hasSourceManager())
|
| 399 | Sources = &Info.getSourceManager();
|
| 400 | Converter.emitDiagnostic(Info.getLocation(), DiagLevel, Message,
|
| 401 | Info.getRanges(), Info.getFixItHints(), Sources);
|
| 402 |
|
| 403 | checkFilters(Info.getLocation());
|
| 404 | }
|
| 405 |
|
| 406 | bool ClangTidyDiagnosticConsumer::passesLineFilter(StringRef FileName,
|
| 407 | unsigned LineNumber) const {
|
| 408 | if (Context.getGlobalOptions().LineFilter.empty())
|
| 409 | return true;
|
| 410 | for (const FileFilter &Filter : Context.getGlobalOptions().LineFilter) {
|
| 411 | if (FileName.endswith(Filter.Name)) {
|
| 412 | if (Filter.LineRanges.empty())
|
| 413 | return true;
|
| 414 | for (const FileFilter::LineRange &Range : Filter.LineRanges) {
|
| 415 | if (Range.first <= LineNumber && LineNumber <= Range.second)
|
| 416 | return true;
|
| 417 | }
|
| 418 | return false;
|
| 419 | }
|
| 420 | }
|
| 421 | return false;
|
| 422 | }
|
| 423 |
|
| 424 | void ClangTidyDiagnosticConsumer::checkFilters(SourceLocation Location) {
|
| 425 | // Invalid location may mean a diagnostic in a command line, don't skip these.
|
| 426 | if (!Location.isValid()) {
|
| 427 | LastErrorRelatesToUserCode = true;
|
| 428 | LastErrorPassesLineFilter = true;
|
| 429 | return;
|
| 430 | }
|
| 431 |
|
| 432 | const SourceManager &Sources = Diags->getSourceManager();
|
| 433 | if (!*Context.getOptions().SystemHeaders &&
|
| 434 | Sources.isInSystemHeader(Location))
|
| 435 | return;
|
| 436 |
|
| 437 | // FIXME: We start with a conservative approach here, but the actual type of
|
| 438 | // location needed depends on the check (in particular, where this check wants
|
| 439 | // to apply fixes).
|
| 440 | FileID FID = Sources.getDecomposedExpansionLoc(Location).first;
|
| 441 | const FileEntry *File = Sources.getFileEntryForID(FID);
|
| 442 |
|
| 443 | // -DMACRO definitions on the command line have locations in a virtual buffer
|
| 444 | // that doesn't have a FileEntry. Don't skip these as well.
|
| 445 | if (!File) {
|
| 446 | LastErrorRelatesToUserCode = true;
|
| 447 | LastErrorPassesLineFilter = true;
|
| 448 | return;
|
| 449 | }
|
| 450 |
|
| 451 | StringRef FileName(File->getName());
|
| 452 | LastErrorRelatesToUserCode = LastErrorRelatesToUserCode ||
|
| 453 | Sources.isInMainFile(Location) ||
|
| 454 | getHeaderFilter()->match(FileName);
|
| 455 |
|
| 456 | unsigned LineNumber = Sources.getExpansionLineNumber(Location);
|
| 457 | LastErrorPassesLineFilter =
|
| 458 | LastErrorPassesLineFilter || passesLineFilter(FileName, LineNumber);
|
| 459 | }
|
| 460 |
|
| 461 | llvm::Regex *ClangTidyDiagnosticConsumer::getHeaderFilter() {
|
| 462 | if (!HeaderFilter)
|
| 463 | HeaderFilter.reset(
|
| 464 | new llvm::Regex(*Context.getOptions().HeaderFilterRegex));
|
| 465 | return HeaderFilter.get();
|
| 466 | }
|
| 467 |
|
| 468 | void ClangTidyDiagnosticConsumer::removeIncompatibleErrors(
|
| 469 | SmallVectorImpl<ClangTidyError> &Errors) const {
|
| 470 | // Each error is modelled as the set of intervals in which it applies
|
| 471 | // replacements. To detect overlapping replacements, we use a sweep line
|
| 472 | // algorithm over these sets of intervals.
|
| 473 | // An event here consists of the opening or closing of an interval. During the
|
| 474 | // process, we maintain a counter with the amount of open intervals. If we
|
| 475 | // find an endpoint of an interval and this counter is different from 0, it
|
| 476 | // means that this interval overlaps with another one, so we set it as
|
| 477 | // inapplicable.
|
| 478 | struct Event {
|
| 479 | // An event can be either the begin or the end of an interval.
|
| 480 | enum EventType {
|
| 481 | ET_Begin = 1,
|
| 482 | ET_End = -1,
|
| 483 | };
|
| 484 |
|
| 485 | Event(unsigned Begin, unsigned End, EventType Type, unsigned ErrorId,
|
| 486 | unsigned ErrorSize)
|
| 487 | : Type(Type), ErrorId(ErrorId) {
|
| 488 | // The events are going to be sorted by their position. In case of draw:
|
| 489 | //
|
| 490 | // * If an interval ends at the same position at which other interval
|
| 491 | // begins, this is not an overlapping, so we want to remove the ending
|
| 492 | // interval before adding the starting one: end events have higher
|
| 493 | // priority than begin events.
|
| 494 | //
|
| 495 | // * If we have several begin points at the same position, we will mark as
|
| 496 | // inapplicable the ones that we process later, so the first one has to
|
| 497 | // be the one with the latest end point, because this one will contain
|
| 498 | // all the other intervals. For the same reason, if we have several end
|
| 499 | // points in the same position, the last one has to be the one with the
|
| 500 | // earliest begin point. In both cases, we sort non-increasingly by the
|
| 501 | // position of the complementary.
|
| 502 | //
|
| 503 | // * In case of two equal intervals, the one whose error is bigger can
|
| 504 | // potentially contain the other one, so we want to process its begin
|
| 505 | // points before and its end points later.
|
| 506 | //
|
| 507 | // * Finally, if we have two equal intervals whose errors have the same
|
| 508 | // size, none of them will be strictly contained inside the other.
|
| 509 | // Sorting by ErrorId will guarantee that the begin point of the first
|
| 510 | // one will be processed before, disallowing the second one, and the
|
| 511 | // end point of the first one will also be processed before,
|
| 512 | // disallowing the first one.
|
| 513 | if (Type == ET_Begin)
|
| 514 | Priority = std::make_tuple(Begin, Type, -End, -ErrorSize, ErrorId);
|
| 515 | else
|
| 516 | Priority = std::make_tuple(End, Type, -Begin, ErrorSize, ErrorId);
|
| 517 | }
|
| 518 |
|
| 519 | bool operator<(const Event &Other) const {
|
| 520 | return Priority < Other.Priority;
|
| 521 | }
|
| 522 |
|
| 523 | // Determines if this event is the begin or the end of an interval.
|
| 524 | EventType Type;
|
| 525 | // The index of the error to which the interval that generated this event
|
| 526 | // belongs.
|
| 527 | unsigned ErrorId;
|
| 528 | // The events will be sorted based on this field.
|
| 529 | std::tuple<unsigned, EventType, int, int, unsigned> Priority;
|
| 530 | };
|
| 531 |
|
| 532 | // Compute error sizes.
|
| 533 | std::vector<int> Sizes;
|
| 534 | for (const auto &Error : Errors) {
|
| 535 | int Size = 0;
|
| 536 | for (const auto &FileAndReplaces : Error.Fix) {
|
| 537 | for (const auto &Replace : FileAndReplaces.second)
|
| 538 | Size += Replace.getLength();
|
| 539 | }
|
| 540 | Sizes.push_back(Size);
|
| 541 | }
|
| 542 |
|
| 543 | // Build events from error intervals.
|
| 544 | std::map<std::string, std::vector<Event>> FileEvents;
|
| 545 | for (unsigned I = 0; I < Errors.size(); ++I) {
|
| 546 | for (const auto &FileAndReplace : Errors[I].Fix) {
|
| 547 | for (const auto &Replace : FileAndReplace.second) {
|
| 548 | unsigned Begin = Replace.getOffset();
|
| 549 | unsigned End = Begin + Replace.getLength();
|
| 550 | const std::string &FilePath = Replace.getFilePath();
|
| 551 | // FIXME: Handle empty intervals, such as those from insertions.
|
| 552 | if (Begin == End)
|
| 553 | continue;
|
| 554 | auto &Events = FileEvents[FilePath];
|
| 555 | Events.emplace_back(Begin, End, Event::ET_Begin, I, Sizes[I]);
|
| 556 | Events.emplace_back(Begin, End, Event::ET_End, I, Sizes[I]);
|
| 557 | }
|
| 558 | }
|
| 559 | }
|
| 560 |
|
| 561 | std::vector<bool> Apply(Errors.size(), true);
|
| 562 | for (auto &FileAndEvents : FileEvents) {
|
| 563 | std::vector<Event> &Events = FileAndEvents.second;
|
| 564 | // Sweep.
|
| 565 | std::sort(Events.begin(), Events.end());
|
| 566 | int OpenIntervals = 0;
|
| 567 | for (const auto &Event : Events) {
|
| 568 | if (Event.Type == Event::ET_End)
|
| 569 | --OpenIntervals;
|
| 570 | // This has to be checked after removing the interval from the count if it
|
| 571 | // is an end event, or before adding it if it is a begin event.
|
| 572 | if (OpenIntervals != 0)
|
| 573 | Apply[Event.ErrorId] = false;
|
| 574 | if (Event.Type == Event::ET_Begin)
|
| 575 | ++OpenIntervals;
|
| 576 | }
|
| 577 | assert(OpenIntervals == 0 && "Amount of begin/end points doesn't match");
|
| 578 | }
|
| 579 |
|
| 580 | for (unsigned I = 0; I < Errors.size(); ++I) {
|
| 581 | if (!Apply[I]) {
|
| 582 | Errors[I].Fix.clear();
|
| 583 | Errors[I].Notes.emplace_back(
|
| 584 | "this fix will not be applied because it overlaps with another fix");
|
| 585 | }
|
| 586 | }
|
| 587 | }
|
| 588 |
|
| 589 | namespace {
|
| 590 | struct LessClangTidyError {
|
| 591 | bool operator()(const ClangTidyError &LHS, const ClangTidyError &RHS) const {
|
| 592 | const tooling::DiagnosticMessage &M1 = LHS.Message;
|
| 593 | const tooling::DiagnosticMessage &M2 = RHS.Message;
|
| 594 |
|
| 595 | return std::tie(M1.FilePath, M1.FileOffset, M1.Message) <
|
| 596 | std::tie(M2.FilePath, M2.FileOffset, M2.Message);
|
| 597 | }
|
| 598 | };
|
| 599 | struct EqualClangTidyError {
|
| 600 | bool operator()(const ClangTidyError &LHS, const ClangTidyError &RHS) const {
|
| 601 | LessClangTidyError Less;
|
| 602 | return !Less(LHS, RHS) && !Less(RHS, LHS);
|
| 603 | }
|
| 604 | };
|
| 605 | } // end anonymous namespace
|
| 606 |
|
| 607 | // Flushes the internal diagnostics buffer to the ClangTidyContext.
|
| 608 | void ClangTidyDiagnosticConsumer::finish() {
|
| 609 | finalizeLastError();
|
| 610 |
|
| 611 | std::sort(Errors.begin(), Errors.end(), LessClangTidyError());
|
| 612 | Errors.erase(std::unique(Errors.begin(), Errors.end(), EqualClangTidyError()),
|
| 613 | Errors.end());
|
| 614 | removeIncompatibleErrors(Errors);
|
| 615 |
|
| 616 | for (const ClangTidyError &Error : Errors)
|
| 617 | Context.storeError(Error);
|
| 618 | Errors.clear();
|
| 619 | }
|