NamespaceCommentCheck.cpp 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. //===--- NamespaceCommentCheck.cpp - clang-tidy ---------------------------===//
  2. //
  3. // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
  4. // See https://llvm.org/LICENSE.txt for license information.
  5. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  6. //
  7. //===----------------------------------------------------------------------===//
  8. #include "NamespaceCommentCheck.h"
  9. #include "../utils/LexerUtils.h"
  10. #include "clang/AST/ASTContext.h"
  11. #include "clang/ASTMatchers/ASTMatchers.h"
  12. #include "clang/Basic/SourceLocation.h"
  13. #include "clang/Basic/TokenKinds.h"
  14. #include "clang/Lex/Lexer.h"
  15. #include "llvm/ADT/StringExtras.h"
  16. #include <optional>
  17. using namespace clang::ast_matchers;
  18. namespace clang::tidy::readability {
  19. NamespaceCommentCheck::NamespaceCommentCheck(StringRef Name,
  20. ClangTidyContext *Context)
  21. : ClangTidyCheck(Name, Context),
  22. NamespaceCommentPattern("^/[/*] *(end (of )?)? *(anonymous|unnamed)? *"
  23. "namespace( +([a-zA-Z0-9_:]+))?\\.? *(\\*/)?$",
  24. llvm::Regex::IgnoreCase),
  25. ShortNamespaceLines(Options.get("ShortNamespaceLines", 1u)),
  26. SpacesBeforeComments(Options.get("SpacesBeforeComments", 1u)) {}
  27. void NamespaceCommentCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  28. Options.store(Opts, "ShortNamespaceLines", ShortNamespaceLines);
  29. Options.store(Opts, "SpacesBeforeComments", SpacesBeforeComments);
  30. }
  31. void NamespaceCommentCheck::registerMatchers(MatchFinder *Finder) {
  32. Finder->addMatcher(namespaceDecl().bind("namespace"), this);
  33. }
  34. static bool locationsInSameFile(const SourceManager &Sources,
  35. SourceLocation Loc1, SourceLocation Loc2) {
  36. return Loc1.isFileID() && Loc2.isFileID() &&
  37. Sources.getFileID(Loc1) == Sources.getFileID(Loc2);
  38. }
  39. static std::optional<std::string>
  40. getNamespaceNameAsWritten(SourceLocation &Loc, const SourceManager &Sources,
  41. const LangOptions &LangOpts) {
  42. // Loc should be at the begin of the namespace decl (usually, `namespace`
  43. // token). We skip the first token right away, but in case of `inline
  44. // namespace` or `namespace a::inline b` we can see both `inline` and
  45. // `namespace` keywords, which we just ignore. Nested parens/squares before
  46. // the opening brace can result from attributes.
  47. std::string Result;
  48. int Nesting = 0;
  49. while (std::optional<Token> T = utils::lexer::findNextTokenSkippingComments(
  50. Loc, Sources, LangOpts)) {
  51. Loc = T->getLocation();
  52. if (T->is(tok::l_brace))
  53. break;
  54. if (T->isOneOf(tok::l_square, tok::l_paren)) {
  55. ++Nesting;
  56. } else if (T->isOneOf(tok::r_square, tok::r_paren)) {
  57. --Nesting;
  58. } else if (Nesting == 0) {
  59. if (T->is(tok::raw_identifier)) {
  60. StringRef ID = T->getRawIdentifier();
  61. if (ID != "namespace" && ID != "inline")
  62. Result.append(std::string(ID));
  63. } else if (T->is(tok::coloncolon)) {
  64. Result.append("::");
  65. } else { // Any other kind of token is unexpected here.
  66. return std::nullopt;
  67. }
  68. }
  69. }
  70. return Result;
  71. }
  72. void NamespaceCommentCheck::check(const MatchFinder::MatchResult &Result) {
  73. const auto *ND = Result.Nodes.getNodeAs<NamespaceDecl>("namespace");
  74. const SourceManager &Sources = *Result.SourceManager;
  75. // Ignore namespaces inside macros and namespaces split across files.
  76. if (ND->getBeginLoc().isMacroID() ||
  77. !locationsInSameFile(Sources, ND->getBeginLoc(), ND->getRBraceLoc()))
  78. return;
  79. // Don't require closing comments for namespaces spanning less than certain
  80. // number of lines.
  81. unsigned StartLine = Sources.getSpellingLineNumber(ND->getBeginLoc());
  82. unsigned EndLine = Sources.getSpellingLineNumber(ND->getRBraceLoc());
  83. if (EndLine - StartLine + 1 <= ShortNamespaceLines)
  84. return;
  85. // Find next token after the namespace closing brace.
  86. SourceLocation AfterRBrace = Lexer::getLocForEndOfToken(
  87. ND->getRBraceLoc(), /*Offset=*/0, Sources, getLangOpts());
  88. SourceLocation Loc = AfterRBrace;
  89. SourceLocation LBraceLoc = ND->getBeginLoc();
  90. // Currently for nested namespace (n1::n2::...) the AST matcher will match foo
  91. // then bar instead of a single match. So if we got a nested namespace we have
  92. // to skip the next ones.
  93. for (const auto &EndOfNameLocation : Ends) {
  94. if (Sources.isBeforeInTranslationUnit(ND->getLocation(), EndOfNameLocation))
  95. return;
  96. }
  97. std::optional<std::string> NamespaceNameAsWritten =
  98. getNamespaceNameAsWritten(LBraceLoc, Sources, getLangOpts());
  99. if (!NamespaceNameAsWritten)
  100. return;
  101. if (NamespaceNameAsWritten->empty() != ND->isAnonymousNamespace()) {
  102. // Apparently, we didn't find the correct namespace name. Give up.
  103. return;
  104. }
  105. Ends.push_back(LBraceLoc);
  106. Token Tok;
  107. // Skip whitespace until we find the next token.
  108. while (Lexer::getRawToken(Loc, Tok, Sources, getLangOpts()) ||
  109. Tok.is(tok::semi)) {
  110. Loc = Loc.getLocWithOffset(1);
  111. }
  112. if (!locationsInSameFile(Sources, ND->getRBraceLoc(), Loc))
  113. return;
  114. bool NextTokenIsOnSameLine = Sources.getSpellingLineNumber(Loc) == EndLine;
  115. // If we insert a line comment before the token in the same line, we need
  116. // to insert a line break.
  117. bool NeedLineBreak = NextTokenIsOnSameLine && Tok.isNot(tok::eof);
  118. SourceRange OldCommentRange(AfterRBrace, AfterRBrace);
  119. std::string Message = "%0 not terminated with a closing comment";
  120. // Try to find existing namespace closing comment on the same line.
  121. if (Tok.is(tok::comment) && NextTokenIsOnSameLine) {
  122. StringRef Comment(Sources.getCharacterData(Loc), Tok.getLength());
  123. SmallVector<StringRef, 7> Groups;
  124. if (NamespaceCommentPattern.match(Comment, &Groups)) {
  125. StringRef NamespaceNameInComment = Groups.size() > 5 ? Groups[5] : "";
  126. StringRef Anonymous = Groups.size() > 3 ? Groups[3] : "";
  127. if ((ND->isAnonymousNamespace() && NamespaceNameInComment.empty()) ||
  128. (*NamespaceNameAsWritten == NamespaceNameInComment &&
  129. Anonymous.empty())) {
  130. // Check if the namespace in the comment is the same.
  131. // FIXME: Maybe we need a strict mode, where we always fix namespace
  132. // comments with different format.
  133. return;
  134. }
  135. // Otherwise we need to fix the comment.
  136. NeedLineBreak = Comment.startswith("/*");
  137. OldCommentRange =
  138. SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
  139. Message =
  140. (llvm::Twine(
  141. "%0 ends with a comment that refers to a wrong namespace '") +
  142. NamespaceNameInComment + "'")
  143. .str();
  144. } else if (Comment.startswith("//")) {
  145. // Assume that this is an unrecognized form of a namespace closing line
  146. // comment. Replace it.
  147. NeedLineBreak = false;
  148. OldCommentRange =
  149. SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
  150. Message = "%0 ends with an unrecognized comment";
  151. }
  152. // If it's a block comment, just move it to the next line, as it can be
  153. // multi-line or there may be other tokens behind it.
  154. }
  155. std::string NamespaceNameForDiag =
  156. ND->isAnonymousNamespace() ? "anonymous namespace"
  157. : ("namespace '" + *NamespaceNameAsWritten + "'");
  158. std::string Fix(SpacesBeforeComments, ' ');
  159. Fix.append("// namespace");
  160. if (!ND->isAnonymousNamespace())
  161. Fix.append(" ").append(*NamespaceNameAsWritten);
  162. if (NeedLineBreak)
  163. Fix.append("\n");
  164. // Place diagnostic at an old comment, or closing brace if we did not have it.
  165. SourceLocation DiagLoc =
  166. OldCommentRange.getBegin() != OldCommentRange.getEnd()
  167. ? OldCommentRange.getBegin()
  168. : ND->getRBraceLoc();
  169. diag(DiagLoc, Message) << NamespaceNameForDiag
  170. << FixItHint::CreateReplacement(
  171. CharSourceRange::getCharRange(OldCommentRange),
  172. Fix);
  173. diag(ND->getLocation(), "%0 starts here", DiagnosticIDs::Note)
  174. << NamespaceNameForDiag;
  175. }
  176. } // namespace clang::tidy::readability