Cppcheck
suppressions.cpp
Go to the documentation of this file.
1 /*
2  * Cppcheck - A tool for static C/C++ code analysis
3  * Copyright (C) 2007-2024 Cppcheck team.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "suppressions.h"
20 
21 #include "errorlogger.h"
22 #include "errortypes.h"
23 #include "path.h"
24 #include "utils.h"
25 #include "token.h"
26 #include "tokenize.h"
27 #include "tokenlist.h"
28 
29 #include <algorithm>
30 #include <cctype> // std::isdigit, std::isalnum, etc
31 #include <cstring>
32 #include <functional> // std::bind, std::placeholders
33 #include <sstream>
34 #include <utility>
35 
36 #include "xml.h"
37 
38 static const char ID_UNUSEDFUNCTION[] = "unusedFunction";
39 static const char ID_CHECKERSREPORT[] = "checkersReport";
40 
41 SuppressionList::ErrorMessage SuppressionList::ErrorMessage::fromErrorMessage(const ::ErrorMessage &msg, const std::set<std::string> &macroNames)
42 {
44  ret.hash = msg.hash;
45  ret.errorId = msg.id;
46  if (!msg.callStack.empty()) {
47  ret.setFileName(msg.callStack.back().getfile(false));
48  ret.lineNumber = msg.callStack.back().line;
49  } else {
51  }
52  ret.certainty = msg.certainty;
53  ret.symbolNames = msg.symbolNames();
54  ret.macroNames = macroNames;
55  return ret;
56 }
57 
58 static bool isAcceptedErrorIdChar(char c)
59 {
60  switch (c) {
61  case '_':
62  case '-':
63  case '.':
64  case '*':
65  return true;
66  default:
67  return c > 0 && std::isalnum(c);
68  }
69 }
70 
71 std::string SuppressionList::parseFile(std::istream &istr)
72 {
73  // Change '\r' to '\n' in the istr
74  std::string filedata;
75  std::string line;
76  while (std::getline(istr, line))
77  filedata += line + "\n";
78  std::replace(filedata.begin(), filedata.end(), '\r', '\n');
79 
80  // Parse filedata..
81  std::istringstream istr2(filedata);
82  while (std::getline(istr2, line)) {
83  // Skip empty lines
84  if (line.empty())
85  continue;
86 
87  // Skip comments
88  if (line.length() > 1 && line[0] == '#')
89  continue;
90  if (line.length() >= 2 && line[0] == '/' && line[1] == '/')
91  continue;
92 
93  const std::string errmsg(addSuppressionLine(line));
94  if (!errmsg.empty())
95  return errmsg;
96  }
97 
98  return "";
99 }
100 
101 
102 std::string SuppressionList::parseXmlFile(const char *filename)
103 {
104  tinyxml2::XMLDocument doc;
105  const tinyxml2::XMLError error = doc.LoadFile(filename);
106  if (error != tinyxml2::XML_SUCCESS)
107  return std::string("failed to load suppressions XML '") + filename + "' (" + tinyxml2::XMLDocument::ErrorIDToName(error) + ").";
108 
109  const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement();
110  if (!rootnode)
111  return std::string("failed to load suppressions XML '") + filename + "' (no root node found).";
112  // TODO: check for proper root node 'suppressions'
113  for (const tinyxml2::XMLElement * e = rootnode->FirstChildElement(); e; e = e->NextSiblingElement()) {
114  if (std::strcmp(e->Name(), "suppress") != 0)
115  return std::string("invalid suppression xml file '") + filename + "', expected 'suppress' element but got a '" + e->Name() + "'.";
116 
117  Suppression s;
118  for (const tinyxml2::XMLElement * e2 = e->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) {
119  const char *text = e2->GetText() ? e2->GetText() : "";
120  if (std::strcmp(e2->Name(), "id") == 0)
121  s.errorId = text;
122  else if (std::strcmp(e2->Name(), "fileName") == 0)
123  s.fileName = text;
124  else if (std::strcmp(e2->Name(), "lineNumber") == 0)
125  s.lineNumber = strToInt<int>(text);
126  else if (std::strcmp(e2->Name(), "symbolName") == 0)
127  s.symbolName = text;
128  else if (*text && std::strcmp(e2->Name(), "hash") == 0)
129  s.hash = strToInt<std::size_t>(text);
130  else
131  return std::string("unknown element '") + e2->Name() + "' in suppressions XML '" + filename + "', expected id/fileName/lineNumber/symbolName/hash.";
132  }
133 
134  const std::string err = addSuppression(std::move(s));
135  if (!err.empty())
136  return err;
137  }
138 
139  return "";
140 }
141 
142 std::vector<SuppressionList::Suppression> SuppressionList::parseMultiSuppressComment(const std::string &comment, std::string *errorMessage)
143 {
144  std::vector<Suppression> suppressions;
145 
146  // If this function is called we assume that comment starts with "cppcheck-suppress[".
147  const std::string::size_type start_position = comment.find('[');
148  const std::string::size_type end_position = comment.find(']', start_position);
149  if (end_position == std::string::npos) {
150  if (errorMessage && errorMessage->empty())
151  *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]";
152  return suppressions;
153  }
154 
155  // parse all suppressions
156  for (std::string::size_type pos = start_position; pos < end_position;) {
157  const std::string::size_type pos1 = pos + 1;
158  pos = comment.find(',', pos1);
159  const std::string::size_type pos2 = (pos < end_position) ? pos : end_position;
160  if (pos1 == pos2)
161  continue;
162 
163  Suppression s;
164  std::istringstream iss(comment.substr(pos1, pos2-pos1));
165 
166  iss >> s.errorId;
167  if (!iss) {
168  if (errorMessage && errorMessage->empty())
169  *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]";
170  suppressions.clear();
171  return suppressions;
172  }
173 
174  const std::string symbolNameString = "symbolName=";
175 
176  while (iss) {
177  std::string word;
178  iss >> word;
179  if (!iss)
180  break;
181  if (word.find_first_not_of("+-*/%#;") == std::string::npos)
182  break;
183  if (startsWith(word, symbolNameString)) {
184  s.symbolName = word.substr(symbolNameString.size());
185  } else {
186  if (errorMessage && errorMessage->empty())
187  *errorMessage = "Bad multi suppression '" + comment + "'. legal format is cppcheck-suppress[errorId, errorId symbolName=arr, ...]";
188  suppressions.clear();
189  return suppressions;
190  }
191  }
192 
193  suppressions.push_back(std::move(s));
194  }
195 
196  return suppressions;
197 }
198 
199 std::string SuppressionList::addSuppressionLine(const std::string &line)
200 {
201  std::istringstream lineStream;
202  SuppressionList::Suppression suppression;
203 
204  // Strip any end of line comments
205  std::string::size_type endpos = std::min(line.find('#'), line.find("//"));
206  if (endpos != std::string::npos) {
207  while (endpos > 0 && std::isspace(line[endpos-1])) {
208  endpos--;
209  }
210  lineStream.str(line.substr(0, endpos));
211  } else {
212  lineStream.str(line);
213  }
214 
215  if (std::getline(lineStream, suppression.errorId, ':')) {
216  if (std::getline(lineStream, suppression.fileName)) {
217  // If there is not a dot after the last colon in "file" then
218  // the colon is a separator and the contents after the colon
219  // is a line number..
220 
221  // Get position of last colon
222  const std::string::size_type pos = suppression.fileName.rfind(':');
223 
224  // if a colon is found and there is no dot after it..
225  if (pos != std::string::npos &&
226  suppression.fileName.find('.', pos) == std::string::npos) {
227  // Try to parse out the line number
228  try {
229  std::istringstream istr1(suppression.fileName.substr(pos+1));
230  istr1 >> suppression.lineNumber;
231  } catch (...) {
233  }
234 
236  suppression.fileName.erase(pos);
237  }
238  }
239  }
240  }
241 
242  suppression.fileName = Path::simplifyPath(suppression.fileName);
243 
244  return addSuppression(std::move(suppression));
245 }
246 
248 {
249  // Check if suppression is already in list
250  auto foundSuppression = std::find_if(mSuppressions.begin(), mSuppressions.end(),
251  std::bind(&Suppression::isSameParameters, &suppression, std::placeholders::_1));
252  if (foundSuppression != mSuppressions.end()) {
253  // Update matched state of existing global suppression
254  if (!suppression.isLocal() && suppression.matched)
255  foundSuppression->matched = suppression.matched;
256  return "";
257  }
258 
259  // Check that errorId is valid..
260  if (suppression.errorId.empty() && suppression.hash == 0)
261  return "Failed to add suppression. No id.";
262 
263  for (std::string::size_type pos = 0; pos < suppression.errorId.length(); ++pos) {
264  if (!isAcceptedErrorIdChar(suppression.errorId[pos])) {
265  return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\"";
266  }
267  if (pos == 0 && std::isdigit(suppression.errorId[pos])) {
268  return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\"";
269  }
270  }
271 
272  if (!isValidGlobPattern(suppression.errorId))
273  return "Failed to add suppression. Invalid glob pattern '" + suppression.errorId + "'.";
274  if (!isValidGlobPattern(suppression.fileName))
275  return "Failed to add suppression. Invalid glob pattern '" + suppression.fileName + "'.";
276 
277  mSuppressions.push_back(std::move(suppression));
278 
279  return "";
280 }
281 
282 std::string SuppressionList::addSuppressions(std::list<Suppression> suppressions)
283 {
284  for (auto &newSuppression : suppressions) {
285  auto errmsg = addSuppression(std::move(newSuppression));
286  if (!errmsg.empty())
287  return errmsg;
288  }
289  return "";
290 }
291 
293 {
294  mFileName = Path::simplifyPath(std::move(s));
295 }
296 
297 bool SuppressionList::Suppression::parseComment(std::string comment, std::string *errorMessage)
298 {
299  if (comment.size() < 2)
300  return false;
301 
302  if (comment.find(';') != std::string::npos)
303  comment.erase(comment.find(';'));
304 
305  if (comment.find("//", 2) != std::string::npos)
306  comment.erase(comment.find("//",2));
307 
308  if (comment.compare(comment.size() - 2, 2, "*/") == 0)
309  comment.erase(comment.size() - 2, 2);
310 
311  const std::set<std::string> cppchecksuppress{
312  "cppcheck-suppress",
313  "cppcheck-suppress-begin",
314  "cppcheck-suppress-end",
315  "cppcheck-suppress-file",
316  "cppcheck-suppress-macro"
317  };
318 
319  std::istringstream iss(comment.substr(2));
320  std::string word;
321  iss >> word;
322  if (!cppchecksuppress.count(word))
323  return false;
324 
325  iss >> errorId;
326  if (!iss)
327  return false;
328 
329  const std::string symbolNameString = "symbolName=";
330 
331  while (iss) {
332  iss >> word;
333  if (!iss)
334  break;
335  if (word.find_first_not_of("+-*/%#;") == std::string::npos)
336  break;
337  if (startsWith(word, symbolNameString))
338  symbolName = word.substr(symbolNameString.size());
339  else if (errorMessage && errorMessage->empty())
340  *errorMessage = "Bad suppression attribute '" + word + "'. You can write comments in the comment after a ; or //. Valid suppression attributes; symbolName=sym";
341  }
342  return true;
343 }
344 
346 {
347  if (hash > 0 && hash != errmsg.hash)
348  return false;
349  if (!errorId.empty() && !matchglob(errorId, errmsg.errorId))
350  return false;
351  if (type == SuppressionList::Type::macro) {
352  if (errmsg.macroNames.count(macroName) == 0)
353  return false;
354  } else {
355  if (!fileName.empty() && !matchglob(fileName, errmsg.getFileName()))
356  return false;
357  if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) {
358  if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber)
359  return false;
360  }
361  if ((SuppressionList::Type::block == type) && ((errmsg.lineNumber < lineBegin) || (errmsg.lineNumber > lineEnd)))
362  return false;
363  }
364  if (!symbolName.empty()) {
365  for (std::string::size_type pos = 0; pos < errmsg.symbolNames.size();) {
366  const std::string::size_type pos2 = errmsg.symbolNames.find('\n',pos);
367  std::string symname;
368  if (pos2 == std::string::npos) {
369  symname = errmsg.symbolNames.substr(pos);
370  pos = pos2;
371  } else {
372  symname = errmsg.symbolNames.substr(pos,pos2-pos);
373  pos = pos2+1;
374  }
375  if (matchglob(symbolName, symname))
376  return true;
377  }
378  return false;
379  }
380  return true;
381 }
382 
384 {
385  if (!isSuppressed(errmsg))
386  return false;
387  matched = true;
388  checked = true;
389  return true;
390 }
391 
393 {
394  std::string ret;
395  if (!errorId.empty())
396  ret = errorId;
397  if (!fileName.empty())
398  ret += " fileName=" + fileName;
399  if (lineNumber != NO_LINE)
400  ret += " lineNumber=" + std::to_string(lineNumber);
401  if (!symbolName.empty())
402  ret += " symbolName=" + symbolName;
403  if (hash > 0)
404  ret += " hash=" + std::to_string(hash);
405  if (startsWith(ret," "))
406  return ret.substr(1);
407  return ret;
408 }
409 
411 {
412  const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression");
413  bool returnValue = false;
414  for (Suppression &s : mSuppressions) {
415  if (!global && !s.isLocal())
416  continue;
417  if (unmatchedSuppression && s.errorId != errmsg.errorId)
418  continue;
419  if (s.isMatch(errmsg))
420  returnValue = true;
421  }
422  return returnValue;
423 }
424 
426 {
427  for (Suppression &s : mSuppressions) {
428  if (!global && !s.isLocal())
429  continue;
430  if (s.errorId != errmsg.errorId) // Error id must match exactly
431  continue;
432  if (s.isMatch(errmsg))
433  return true;
434  }
435  return false;
436 }
437 
438 bool SuppressionList::isSuppressed(const ::ErrorMessage &errmsg, const std::set<std::string>& macroNames)
439 {
440  if (mSuppressions.empty())
441  return false;
443 }
444 
445 void SuppressionList::dump(std::ostream & out) const
446 {
447  out << " <suppressions>" << std::endl;
448  for (const Suppression &suppression : mSuppressions) {
449  out << " <suppression";
450  out << " errorId=\"" << ErrorLogger::toxml(suppression.errorId) << '"';
451  if (!suppression.fileName.empty())
452  out << " fileName=\"" << ErrorLogger::toxml(suppression.fileName) << '"';
453  if (suppression.lineNumber != Suppression::NO_LINE)
454  out << " lineNumber=\"" << suppression.lineNumber << '"';
455  if (!suppression.symbolName.empty())
456  out << " symbolName=\"" << ErrorLogger::toxml(suppression.symbolName) << '\"';
457  if (suppression.hash > 0)
458  out << " hash=\"" << suppression.hash << '\"';
459  if (suppression.lineBegin != Suppression::NO_LINE)
460  out << " lineBegin=\"" << suppression.lineBegin << '"';
461  if (suppression.lineEnd != Suppression::NO_LINE)
462  out << " lineEnd=\"" << suppression.lineEnd << '"';
463  if (suppression.type == SuppressionList::Type::file)
464  out << " type=\"file\"";
465  else if (suppression.type == SuppressionList::Type::block)
466  out << " type=\"block\"";
467  else if (suppression.type == SuppressionList::Type::blockBegin)
468  out << " type=\"blockBegin\"";
469  else if (suppression.type == SuppressionList::Type::blockEnd)
470  out << " type=\"blockEnd\"";
471  else if (suppression.type == SuppressionList::Type::macro)
472  out << " type=\"macro\"";
473  out << " />" << std::endl;
474  }
475  out << " </suppressions>" << std::endl;
476 }
477 
478 std::list<SuppressionList::Suppression> SuppressionList::getUnmatchedLocalSuppressions(const std::string &file, const bool unusedFunctionChecking) const
479 {
480  std::string tmpFile = Path::simplifyPath(file);
481  std::list<Suppression> result;
482  for (const Suppression &s : mSuppressions) {
483  if (s.matched || ((s.lineNumber != Suppression::NO_LINE) && !s.checked))
484  continue;
485  if (s.type == SuppressionList::Type::macro)
486  continue;
487  if (s.hash > 0)
488  continue;
489  if (s.errorId == ID_CHECKERSREPORT)
490  continue;
491  if (!unusedFunctionChecking && s.errorId == ID_UNUSEDFUNCTION)
492  continue;
493  if (tmpFile.empty() || !s.isLocal() || s.fileName != tmpFile)
494  continue;
495  result.push_back(s);
496  }
497  return result;
498 }
499 
500 std::list<SuppressionList::Suppression> SuppressionList::getUnmatchedGlobalSuppressions(const bool unusedFunctionChecking) const
501 {
502  std::list<Suppression> result;
503  for (const Suppression &s : mSuppressions) {
504  if (s.matched || ((s.lineNumber != Suppression::NO_LINE) && !s.checked))
505  continue;
506  if (s.hash > 0)
507  continue;
508  if (!unusedFunctionChecking && s.errorId == ID_UNUSEDFUNCTION)
509  continue;
510  if (s.errorId == ID_CHECKERSREPORT)
511  continue;
512  if (s.isLocal())
513  continue;
514  result.push_back(s);
515  }
516  return result;
517 }
518 
519 const std::list<SuppressionList::Suppression> &SuppressionList::getSuppressions() const
520 {
521  return mSuppressions;
522 }
523 
525  int currLineNr = -1;
526  int currFileIdx = -1;
527  for (const Token *tok = tokenizer.tokens(); tok; tok = tok->next()) {
528  if (currFileIdx != tok->fileIndex() || currLineNr != tok->linenr()) {
529  currLineNr = tok->linenr();
530  currFileIdx = tok->fileIndex();
531  for (auto &suppression : mSuppressions) {
532  if (suppression.type == SuppressionList::Type::unique) {
533  if (!suppression.checked && (suppression.lineNumber == currLineNr) && (suppression.fileName == tokenizer.list.file(tok))) {
534  suppression.checked = true;
535  }
536  } else if (suppression.type == SuppressionList::Type::block) {
537  if ((!suppression.checked && (suppression.lineBegin <= currLineNr) && (suppression.lineEnd >= currLineNr) && (suppression.fileName == tokenizer.list.file(tok)))) {
538  suppression.checked = true;
539  }
540  } else if (!suppression.checked && suppression.fileName == tokenizer.list.file(tok)) {
541  suppression.checked = true;
542  }
543  }
544  }
545  }
546 }
547 
548 bool SuppressionList::reportUnmatchedSuppressions(const std::list<SuppressionList::Suppression> &unmatched, ErrorLogger &errorLogger)
549 {
550  bool err = false;
551  // Report unmatched suppressions
552  for (const SuppressionList::Suppression &s : unmatched) {
553  // don't report "unmatchedSuppression" as unmatched
554  if (s.errorId == "unmatchedSuppression")
555  continue;
556 
557  // check if this unmatched suppression is suppressed
558  bool suppressed = false;
559  for (const SuppressionList::Suppression &s2 : unmatched) {
560  if (s2.errorId == "unmatchedSuppression") {
561  if ((s2.fileName.empty() || s2.fileName == "*" || s2.fileName == s.fileName) &&
562  (s2.lineNumber == SuppressionList::Suppression::NO_LINE || s2.lineNumber == s.lineNumber)) {
563  suppressed = true;
564  break;
565  }
566  }
567  }
568 
569  if (suppressed)
570  continue;
571 
572  std::list<::ErrorMessage::FileLocation> callStack;
573  if (!s.fileName.empty())
574  callStack.emplace_back(s.fileName, s.lineNumber, 0);
575  errorLogger.reportErr(::ErrorMessage(std::move(callStack), emptyString, Severity::information, "Unmatched suppression: " + s.errorId, "unmatchedSuppression", Certainty::normal));
576  err = true;
577  }
578  return err;
579 }
This is an interface, which the class responsible of error logging should implement.
Definition: errorlogger.h:214
virtual void reportErr(const ErrorMessage &msg)=0
Information about found errors and warnings is directed here.
static std::string toxml(const std::string &str)
Convert XML-sensitive characters into XML entities.
static std::string simplifyPath(std::string originalPath)
Simplify path "foo/bar/.." => "foo".
Definition: path.cpp:83
static std::vector< Suppression > parseMultiSuppressComment(const std::string &comment, std::string *errorMessage)
Parse multi inline suppression in comment.
void dump(std::ostream &out) const
Create an xml dump of suppressions.
std::string addSuppressionLine(const std::string &line)
Don't show the given error.
void markUnmatchedInlineSuppressionsAsChecked(const Tokenizer &tokenizer)
Marks Inline Suppressions as checked if source line is in the token stream.
std::list< Suppression > getUnmatchedGlobalSuppressions(const bool unusedFunctionChecking) const
Returns list of unmatched global (glob pattern) suppressions.
const std::list< Suppression > & getSuppressions() const
Returns list of all suppressions.
std::string addSuppression(Suppression suppression)
Don't show this error.
std::list< Suppression > mSuppressions
List of error which the user doesn't want to see.
Definition: suppressions.h:258
std::string parseXmlFile(const char *filename)
Don't show errors listed in the file.
std::string addSuppressions(std::list< Suppression > suppressions)
Combine list of suppressions into the current suppressions.
bool isSuppressed(const ErrorMessage &errmsg, bool global=true)
Returns true if this message should not be shown to the user.
bool isSuppressedExplicitly(const ErrorMessage &errmsg, bool global=true)
Returns true if this message is "explicitly" suppressed.
static bool reportUnmatchedSuppressions(const std::list< SuppressionList::Suppression > &unmatched, ErrorLogger &errorLogger)
Report unmatched suppressions.
std::string parseFile(std::istream &istr)
Don't show errors listed in the file.
std::list< Suppression > getUnmatchedLocalSuppressions(const std::string &file, const bool unusedFunctionChecking) const
Returns list of unmatched local (per-file) suppressions.
const std::string & file(const Token *tok) const
get filename for given token
The token list that the TokenList generates is a linked-list of this class.
Definition: token.h:150
Token * next()
Definition: token.h:830
The main purpose is to tokenize the source code.
Definition: tokenize.h:46
const Token * tokens() const
Definition: tokenize.h:592
TokenList list
Token list: stores all tokens.
Definition: tokenize.h:590
static const std::string emptyString
Definition: config.h:127
static void replace(std::string &source, const std::unordered_map< std::string, std::string > &substitutionMap)
@ information
Checking information.
@ error
Programming error.
void setFileName(std::string s)
const std::string & getFileName() const
Definition: suppressions.h:53
static SuppressionList::ErrorMessage fromErrorMessage(const ::ErrorMessage &msg, const std::set< std::string > &macroNames)
std::set< std::string > macroNames
Definition: suppressions.h:59
bool isSuppressed(const ErrorMessage &errmsg) const
bool isSameParameters(const Suppression &other) const
Definition: suppressions.h:128
bool parseComment(std::string comment, std::string *errorMessage)
Parse inline suppression in comment.
std::string getText() const
bool isMatch(const ErrorMessage &errmsg)
static const char ID_CHECKERSREPORT[]
static const char ID_UNUSEDFUNCTION[]
static bool isAcceptedErrorIdChar(char c)
bool matchglob(const std::string &pattern, const std::string &name)
Definition: utils.cpp:54
bool isValidGlobPattern(const std::string &pattern)
Definition: utils.cpp:41
bool startsWith(const std::string &str, const char start[], std::size_t startlen)
Definition: utils.h:94