| //===--- MacroParenthesesCheck.cpp - clang-tidy----------------------------===// |
| // |
| // 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 |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "MacroParenthesesCheck.h" |
| #include "clang/Frontend/CompilerInstance.h" |
| #include "clang/Lex/PPCallbacks.h" |
| #include "clang/Lex/Preprocessor.h" |
| |
| namespace clang { |
| namespace tidy { |
| namespace bugprone { |
| |
| namespace { |
| class MacroParenthesesPPCallbacks : public PPCallbacks { |
| public: |
| MacroParenthesesPPCallbacks(Preprocessor *PP, MacroParenthesesCheck *Check) |
| : PP(PP), Check(Check) {} |
| |
| void MacroDefined(const Token &MacroNameTok, |
| const MacroDirective *MD) override { |
| replacementList(MacroNameTok, MD->getMacroInfo()); |
| argument(MacroNameTok, MD->getMacroInfo()); |
| } |
| |
| private: |
| /// Replacement list with calculations should be enclosed in parentheses. |
| void replacementList(const Token &MacroNameTok, const MacroInfo *MI); |
| |
| /// Arguments should be enclosed in parentheses. |
| void argument(const Token &MacroNameTok, const MacroInfo *MI); |
| |
| Preprocessor *PP; |
| MacroParenthesesCheck *Check; |
| }; |
| } // namespace |
| |
| /// Is argument surrounded properly with parentheses/braces/squares/commas? |
| static bool isSurroundedLeft(const Token &T) { |
| return T.isOneOf(tok::l_paren, tok::l_brace, tok::l_square, tok::comma, |
| tok::semi); |
| } |
| |
| /// Is argument surrounded properly with parentheses/braces/squares/commas? |
| static bool isSurroundedRight(const Token &T) { |
| return T.isOneOf(tok::r_paren, tok::r_brace, tok::r_square, tok::comma, |
| tok::semi); |
| } |
| |
| /// Is given TokenKind a keyword? |
| static bool isKeyword(const Token &T) { |
| // FIXME: better matching of keywords to avoid false positives. |
| return T.isOneOf(tok::kw_if, tok::kw_case, tok::kw_const, tok::kw_struct); |
| } |
| |
| /// Warning is written when one of these operators are not within parentheses. |
| static bool isWarnOp(const Token &T) { |
| // FIXME: This is an initial list of operators. It can be tweaked later to |
| // get more positives or perhaps avoid some false positive. |
| return T.isOneOf(tok::plus, tok::minus, tok::star, tok::slash, tok::percent, |
| tok::amp, tok::pipe, tok::caret); |
| } |
| |
| /// Is given Token a keyword that is used in variable declarations? |
| static bool isVarDeclKeyword(const Token &T) { |
| return T.isOneOf(tok::kw_bool, tok::kw_char, tok::kw_short, tok::kw_int, |
| tok::kw_long, tok::kw_float, tok::kw_double, tok::kw_const, |
| tok::kw_enum, tok::kw_inline, tok::kw_static, tok::kw_struct, |
| tok::kw_signed, tok::kw_unsigned); |
| } |
| |
| /// Is there a possible variable declaration at Tok? |
| static bool possibleVarDecl(const MacroInfo *MI, const Token *Tok) { |
| if (Tok == MI->tokens_end()) |
| return false; |
| |
| // If we see int/short/struct/etc., just assume this is a variable |
| // declaration. |
| if (isVarDeclKeyword(*Tok)) |
| return true; |
| |
| // Variable declarations start with identifier or coloncolon. |
| if (!Tok->isOneOf(tok::identifier, tok::raw_identifier, tok::coloncolon)) |
| return false; |
| |
| // Skip possible types, etc |
| while (Tok != MI->tokens_end() && |
| Tok->isOneOf(tok::identifier, tok::raw_identifier, tok::coloncolon, |
| tok::star, tok::amp, tok::ampamp, tok::less, |
| tok::greater)) |
| Tok++; |
| |
| // Return true for possible variable declarations. |
| return Tok == MI->tokens_end() || |
| Tok->isOneOf(tok::equal, tok::semi, tok::l_square, tok::l_paren) || |
| isVarDeclKeyword(*Tok); |
| } |
| |
| void MacroParenthesesPPCallbacks::replacementList(const Token &MacroNameTok, |
| const MacroInfo *MI) { |
| // Make sure macro replacement isn't a variable declaration. |
| if (possibleVarDecl(MI, MI->tokens_begin())) |
| return; |
| |
| // Count how deep we are in parentheses/braces/squares. |
| int Count = 0; |
| |
| // SourceLocation for error |
| SourceLocation Loc; |
| |
| for (auto TI = MI->tokens_begin(), TE = MI->tokens_end(); TI != TE; ++TI) { |
| const Token &Tok = *TI; |
| // Replacement list contains keywords, don't warn about it. |
| if (isKeyword(Tok)) |
| return; |
| // When replacement list contains comma/semi don't warn about it. |
| if (Count == 0 && Tok.isOneOf(tok::comma, tok::semi)) |
| return; |
| if (Tok.isOneOf(tok::l_paren, tok::l_brace, tok::l_square)) { |
| ++Count; |
| } else if (Tok.isOneOf(tok::r_paren, tok::r_brace, tok::r_square)) { |
| --Count; |
| // If there are unbalanced parentheses don't write any warning |
| if (Count < 0) |
| return; |
| } else if (Count == 0 && isWarnOp(Tok)) { |
| // Heuristic for macros that are clearly not intended to be enclosed in |
| // parentheses, macro starts with operator. For example: |
| // #define X *10 |
| if (TI == MI->tokens_begin() && (TI + 1) != TE && |
| !Tok.isOneOf(tok::plus, tok::minus)) |
| return; |
| // Don't warn about this macro if the last token is a star. For example: |
| // #define X void * |
| if ((TE - 1)->is(tok::star)) |
| return; |
| |
| Loc = Tok.getLocation(); |
| } |
| } |
| if (Loc.isValid()) { |
| const Token &Last = *(MI->tokens_end() - 1); |
| Check->diag(Loc, "macro replacement list should be enclosed in parentheses") |
| << FixItHint::CreateInsertion(MI->tokens_begin()->getLocation(), "(") |
| << FixItHint::CreateInsertion(Last.getLocation().getLocWithOffset( |
| PP->getSpelling(Last).length()), |
| ")"); |
| } |
| } |
| |
| void MacroParenthesesPPCallbacks::argument(const Token &MacroNameTok, |
| const MacroInfo *MI) { |
| |
| // Skip variable declaration. |
| bool VarDecl = possibleVarDecl(MI, MI->tokens_begin()); |
| |
| // Skip the goto argument with an arbitrary number of subsequent stars. |
| bool FoundGoto = false; |
| |
| for (auto TI = MI->tokens_begin(), TE = MI->tokens_end(); TI != TE; ++TI) { |
| // First token. |
| if (TI == MI->tokens_begin()) |
| continue; |
| |
| // Last token. |
| if ((TI + 1) == MI->tokens_end()) |
| continue; |
| |
| const Token &Prev = *(TI - 1); |
| const Token &Next = *(TI + 1); |
| |
| const Token &Tok = *TI; |
| |
| // There should not be extra parentheses in possible variable declaration. |
| if (VarDecl) { |
| if (Tok.isOneOf(tok::equal, tok::semi, tok::l_square, tok::l_paren)) |
| VarDecl = false; |
| continue; |
| } |
| |
| // There should not be extra parentheses for the goto argument. |
| if (Tok.is(tok::kw_goto)) { |
| FoundGoto = true; |
| continue; |
| } |
| |
| // Only interested in identifiers. |
| if (!Tok.isOneOf(tok::identifier, tok::raw_identifier)) { |
| FoundGoto = false; |
| continue; |
| } |
| |
| // Only interested in macro arguments. |
| if (MI->getParameterNum(Tok.getIdentifierInfo()) < 0) |
| continue; |
| |
| // Argument is surrounded with parentheses/squares/braces/commas. |
| if (isSurroundedLeft(Prev) && isSurroundedRight(Next)) |
| continue; |
| |
| // Don't warn after hash/hashhash or before hashhash. |
| if (Prev.isOneOf(tok::hash, tok::hashhash) || Next.is(tok::hashhash)) |
| continue; |
| |
| // Argument is a struct member. |
| if (Prev.isOneOf(tok::period, tok::arrow, tok::coloncolon, tok::arrowstar, |
| tok::periodstar)) |
| continue; |
| |
| // Argument is a namespace or class. |
| if (Next.is(tok::coloncolon)) |
| continue; |
| |
| // String concatenation. |
| if (isStringLiteral(Prev.getKind()) || isStringLiteral(Next.getKind())) |
| continue; |
| |
| // Type/Var. |
| if (isAnyIdentifier(Prev.getKind()) || isKeyword(Prev) || |
| isAnyIdentifier(Next.getKind()) || isKeyword(Next)) |
| continue; |
| |
| // Initialization. |
| if (Next.is(tok::l_paren)) |
| continue; |
| |
| // Cast. |
| if (Prev.is(tok::l_paren) && Next.is(tok::star) && |
| TI + 2 != MI->tokens_end() && (TI + 2)->is(tok::r_paren)) |
| continue; |
| |
| // Assignment/return, i.e. '=x;' or 'return x;'. |
| if (Prev.isOneOf(tok::equal, tok::kw_return) && Next.is(tok::semi)) |
| continue; |
| |
| // C++ template parameters. |
| if (PP->getLangOpts().CPlusPlus && Prev.isOneOf(tok::comma, tok::less) && |
| Next.isOneOf(tok::comma, tok::greater)) |
| continue; |
| |
| // Namespaces. |
| if (Prev.is(tok::kw_namespace)) |
| continue; |
| |
| // Variadic templates |
| if (MI->isVariadic()) |
| continue; |
| |
| if (!FoundGoto) { |
| Check->diag(Tok.getLocation(), "macro argument should be enclosed in " |
| "parentheses") |
| << FixItHint::CreateInsertion(Tok.getLocation(), "(") |
| << FixItHint::CreateInsertion(Tok.getLocation().getLocWithOffset( |
| PP->getSpelling(Tok).length()), |
| ")"); |
| } |
| |
| FoundGoto = false; |
| } |
| } |
| |
| void MacroParenthesesCheck::registerPPCallbacks( |
| const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { |
| PP->addPPCallbacks(std::make_unique<MacroParenthesesPPCallbacks>(PP, this)); |
| } |
| |
| } // namespace bugprone |
| } // namespace tidy |
| } // namespace clang |