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