/* * Cppcheck - A tool for static C/C++ code analysis * Copyright (C) 2007-2023 Cppcheck team. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "suppressions.h" #include "errorlogger.h" #include "mathlib.h" #include "path.h" #include "utils.h" #include "token.h" #include "tokenize.h" #include "tokenlist.h" #include #include // std::isdigit, std::isalnum, etc #include #include #include // std::bind, std::placeholders #include // IWYU pragma: keep #include #include static bool isAcceptedErrorIdChar(char c) { switch (c) { case '_': case '-': case '.': return true; default: return std::isalnum(c); } } std::string Suppressions::parseFile(std::istream &istr) { // Change '\r' to '\n' in the istr std::string filedata; std::string line; while (std::getline(istr, line)) filedata += line + "\n"; std::replace(filedata.begin(), filedata.end(), '\r', '\n'); // Parse filedata.. std::istringstream istr2(filedata); while (std::getline(istr2, line)) { // Skip empty lines if (line.empty()) continue; // Skip comments if (line.length() > 1 && line[0] == '#') continue; if (line.length() >= 2 && line[0] == '/' && line[1] == '/') continue; const std::string errmsg(addSuppressionLine(line)); if (!errmsg.empty()) return errmsg; } return ""; } std::string Suppressions::parseXmlFile(const char *filename) { tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename); if (error == tinyxml2::XML_ERROR_FILE_NOT_FOUND) return "File not found"; if (error != tinyxml2::XML_SUCCESS) return "Failed to parse XML file"; const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement(); for (const tinyxml2::XMLElement * e = rootnode->FirstChildElement(); e; e = e->NextSiblingElement()) { if (std::strcmp(e->Name(), "suppress") != 0) return "Invalid suppression xml file format, expected element but got a \"" + std::string(e->Name()) + '\"'; Suppression s; for (const tinyxml2::XMLElement * e2 = e->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) { const char *text = e2->GetText() ? e2->GetText() : ""; if (std::strcmp(e2->Name(), "id") == 0) s.errorId = text; else if (std::strcmp(e2->Name(), "fileName") == 0) s.fileName = text; else if (std::strcmp(e2->Name(), "lineNumber") == 0) s.lineNumber = std::atoi(text); else if (std::strcmp(e2->Name(), "symbolName") == 0) s.symbolName = text; else if (*text && std::strcmp(e2->Name(), "hash") == 0) std::istringstream(text) >> s.hash; else return "Unknown suppression element \"" + std::string(e2->Name()) + "\", expected id/fileName/lineNumber/symbolName/hash"; } const std::string err = addSuppression(s); if (!err.empty()) return err; } return ""; } std::vector Suppressions::parseMultiSuppressComment(const std::string &comment, std::string *errorMessage) { std::vector suppressions; // If this function is called we assume that comment starts with "cppcheck-suppress[". const std::string::size_type start_position = comment.find("["); const std::string::size_type end_position = comment.find("]", start_position); if (end_position == std::string::npos) { if (errorMessage && errorMessage->empty()) *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]"; return suppressions; } // parse all suppressions for (std::string::size_type pos = start_position; pos < end_position;) { const std::string::size_type pos1 = pos + 1; pos = comment.find(",", pos1); const std::string::size_type pos2 = (pos < end_position) ? pos : end_position; if (pos1 == pos2) continue; Suppression s; std::istringstream iss(comment.substr(pos1, pos2-pos1)); iss >> s.errorId; if (!iss) { if (errorMessage && errorMessage->empty()) *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]"; suppressions.clear(); return suppressions; } while (iss) { std::string word; iss >> word; if (!iss) break; if (word.find_first_not_of("+-*/%#;") == std::string::npos) break; if (word.compare(0, 11, "symbolName=") == 0) { s.symbolName = word.substr(11); } else { if (errorMessage && errorMessage->empty()) *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]"; suppressions.clear(); return suppressions; } } suppressions.push_back(std::move(s)); } return suppressions; } std::string Suppressions::addSuppressionLine(const std::string &line) { std::istringstream lineStream; Suppressions::Suppression suppression; // Strip any end of line comments std::string::size_type endpos = std::min(line.find("#"), line.find("//")); if (endpos != std::string::npos) { while (endpos > 0 && std::isspace(line[endpos-1])) { endpos--; } lineStream.str(line.substr(0, endpos)); } else { lineStream.str(line); } if (std::getline(lineStream, suppression.errorId, ':')) { if (std::getline(lineStream, suppression.fileName)) { // If there is not a dot after the last colon in "file" then // the colon is a separator and the contents after the colon // is a line number.. // Get position of last colon const std::string::size_type pos = suppression.fileName.rfind(':'); // if a colon is found and there is no dot after it.. if (pos != std::string::npos && suppression.fileName.find('.', pos) == std::string::npos) { // Try to parse out the line number try { std::istringstream istr1(suppression.fileName.substr(pos+1)); istr1 >> suppression.lineNumber; } catch (...) { suppression.lineNumber = Suppressions::Suppression::NO_LINE; } if (suppression.lineNumber != Suppressions::Suppression::NO_LINE) { suppression.fileName.erase(pos); } } } } suppression.fileName = Path::simplifyPath(suppression.fileName); return addSuppression(suppression); } std::string Suppressions::addSuppression(const Suppressions::Suppression &suppression) { // Check if suppression is already in list auto foundSuppression = std::find_if(mSuppressions.begin(), mSuppressions.end(), std::bind(&Suppression::isSameParameters, &suppression, std::placeholders::_1)); if (foundSuppression != mSuppressions.end()) { // Update matched state of existing global suppression if (!suppression.isLocal() && suppression.matched) foundSuppression->matched = suppression.matched; return ""; } // Check that errorId is valid.. if (suppression.errorId.empty() && suppression.hash == 0) return "Failed to add suppression. No id."; if (suppression.errorId != "*") { for (std::string::size_type pos = 0; pos < suppression.errorId.length(); ++pos) { if (suppression.errorId[pos] < 0 || !isAcceptedErrorIdChar(suppression.errorId[pos])) { return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\""; } if (pos == 0 && std::isdigit(suppression.errorId[pos])) { return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\""; } } } if (!isValidGlobPattern(suppression.errorId)) return "Failed to add suppression. Invalid glob pattern '" + suppression.errorId + "'."; if (!isValidGlobPattern(suppression.fileName)) return "Failed to add suppression. Invalid glob pattern '" + suppression.fileName + "'."; mSuppressions.push_back(suppression); return ""; } std::string Suppressions::addSuppressions(const std::list &suppressions) { for (const auto &newSuppression : suppressions) { auto errmsg = addSuppression(newSuppression); if (!errmsg.empty()) return errmsg; } return ""; } void Suppressions::ErrorMessage::setFileName(std::string s) { mFileName = Path::simplifyPath(std::move(s)); } bool Suppressions::Suppression::parseComment(std::string comment, std::string *errorMessage) { if (comment.size() < 2) return false; if (comment.find(';') != std::string::npos) comment.erase(comment.find(';')); if (comment.find("//", 2) != std::string::npos) comment.erase(comment.find("//",2)); if (comment.compare(comment.size() - 2, 2, "*/") == 0) comment.erase(comment.size() - 2, 2); std::istringstream iss(comment.substr(2)); std::string word; iss >> word; if (word != "cppcheck-suppress") return false; iss >> errorId; if (!iss) return false; while (iss) { iss >> word; if (!iss) break; if (word.find_first_not_of("+-*/%#;") == std::string::npos) break; if (word.compare(0,11,"symbolName=")==0) symbolName = word.substr(11); else if (errorMessage && errorMessage->empty()) *errorMessage = "Bad suppression attribute '" + word + "'. You can write comments in the comment after a ; or //. Valid suppression attributes; symbolName=sym"; } return true; } bool Suppressions::Suppression::isSuppressed(const Suppressions::ErrorMessage &errmsg) const { if (hash > 0 && hash != errmsg.hash) return false; if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return false; if (!fileName.empty() && !matchglob(fileName, errmsg.getFileName())) return false; if (lineNumber != NO_LINE && lineNumber != errmsg.lineNumber) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) return false; } if (!symbolName.empty()) { for (std::string::size_type pos = 0; pos < errmsg.symbolNames.size();) { const std::string::size_type pos2 = errmsg.symbolNames.find('\n',pos); std::string symname; if (pos2 == std::string::npos) { symname = errmsg.symbolNames.substr(pos); pos = pos2; } else { symname = errmsg.symbolNames.substr(pos,pos2-pos); pos = pos2+1; } if (matchglob(symbolName, symname)) return true; } return false; } return true; } bool Suppressions::Suppression::isMatch(const Suppressions::ErrorMessage &errmsg) { if (!isSuppressed(errmsg)) return false; matched = true; checked = true; return true; } std::string Suppressions::Suppression::getText() const { std::string ret; if (!errorId.empty()) ret = errorId; if (!fileName.empty()) ret += " fileName=" + fileName; if (lineNumber != NO_LINE) ret += " lineNumber=" + MathLib::toString(lineNumber); if (!symbolName.empty()) ret += " symbolName=" + symbolName; if (hash > 0) ret += " hash=" + MathLib::toString(hash); if (ret.compare(0,1," ")==0) return ret.substr(1); return ret; } bool Suppressions::isSuppressed(const Suppressions::ErrorMessage &errmsg) { const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression"); for (Suppression &s : mSuppressions) { if (unmatchedSuppression && s.errorId != errmsg.errorId) continue; if (s.isMatch(errmsg)) return true; } return false; } bool Suppressions::isSuppressedLocal(const Suppressions::ErrorMessage &errmsg) { const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression"); for (Suppression &s : mSuppressions) { if (!s.isLocal()) continue; if (unmatchedSuppression && s.errorId != errmsg.errorId) continue; if (s.isMatch(errmsg)) return true; } return false; } void Suppressions::dump(std::ostream & out) const { out << " " << std::endl; for (const Suppression &suppression : mSuppressions) { out << " 0) out << " hash=\"" << suppression.hash << '\"'; out << " />" << std::endl; } out << " " << std::endl; } std::list Suppressions::getUnmatchedLocalSuppressions(const std::string &file, const bool unusedFunctionChecking) const { std::string tmpFile = Path::simplifyPath(file); std::list result; for (const Suppression &s : mSuppressions) { if (s.matched || ((s.lineNumber != Suppression::NO_LINE) && !s.checked)) continue; if (s.hash > 0) continue; if (!unusedFunctionChecking && s.errorId == "unusedFunction") continue; if (tmpFile.empty() || !s.isLocal() || s.fileName != tmpFile) continue; result.push_back(s); } return result; } std::list Suppressions::getUnmatchedGlobalSuppressions(const bool unusedFunctionChecking) const { std::list result; for (const Suppression &s : mSuppressions) { if (s.matched || ((s.lineNumber != Suppression::NO_LINE) && !s.checked)) continue; if (s.hash > 0) continue; if (!unusedFunctionChecking && s.errorId == "unusedFunction") continue; if (s.isLocal()) continue; result.push_back(s); } return result; } const std::list &Suppressions::getSuppressions() const { return mSuppressions; } void Suppressions::markUnmatchedInlineSuppressionsAsChecked(const Tokenizer &tokenizer) { int currLineNr = -1; int currFileIdx = -1; for (const Token *tok = tokenizer.tokens(); tok; tok = tok->next()) { if (currFileIdx != tok->fileIndex() || currLineNr != tok->linenr()) { currLineNr = tok->linenr(); currFileIdx = tok->fileIndex(); for (auto &suppression : mSuppressions) { if (!suppression.checked && (suppression.lineNumber == currLineNr) && (suppression.fileName == tokenizer.list.file(tok))) { suppression.checked = true; } } } } }