diff --git a/Makefile b/Makefile
index 5810fcc9aa3..10447a44679 100644
--- a/Makefile
+++ b/Makefile
@@ -642,7 +642,7 @@ $(libcppdir)/pathmatch.o: lib/pathmatch.cpp lib/config.h lib/path.h lib/pathmatc
$(libcppdir)/platform.o: lib/platform.cpp externals/tinyxml2/tinyxml2.h lib/config.h lib/mathlib.h lib/path.h lib/platform.h lib/standards.h lib/utils.h lib/xml.h
$(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/platform.cpp
-$(libcppdir)/preprocessor.o: lib/preprocessor.cpp externals/simplecpp/simplecpp.h lib/checkers.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/preprocessor.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h
+$(libcppdir)/preprocessor.o: lib/preprocessor.cpp externals/simplecpp/simplecpp.h lib/checkers.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/preprocessor.h lib/settings.h lib/smallvector.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/vfvalue.h
$(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/preprocessor.cpp
$(libcppdir)/programmemory.o: lib/programmemory.cpp lib/astutils.h lib/calculate.h lib/checkers.h lib/config.h lib/errortypes.h lib/infer.h lib/library.h lib/mathlib.h lib/platform.h lib/programmemory.h lib/settings.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/tokenlist.h lib/utils.h lib/valueflow.h lib/valueptr.h lib/vfvalue.h
diff --git a/cfg/qt.cfg b/cfg/qt.cfg
index 95dc022cf57..902599f9ef0 100644
--- a/cfg/qt.cfg
+++ b/cfg/qt.cfg
@@ -43,11 +43,16 @@
the READ/WRITE/NOTIFY parts are optional -->
- READ
-
READ
WRITE
NOTIFY
+ RESET
+ BINDABLE
+ DESIGNABLE
+ SCRIPTABLE
+ STORED
+ USER
+ EDITABLE
@@ -5468,7 +5473,8 @@
-
+
+
diff --git a/lib/checkunusedfunctions.cpp b/lib/checkunusedfunctions.cpp
index 4177a13a013..6e8fd60036e 100644
--- a/lib/checkunusedfunctions.cpp
+++ b/lib/checkunusedfunctions.cpp
@@ -65,7 +65,7 @@ static bool isRecursiveCall(const Token* ftok)
return ftok->function() && ftok->function() == Scope::nestedInFunction(ftok->scope());
}
-void CheckUnusedFunctions::parseTokens(const Tokenizer &tokenizer, const Library &library)
+void CheckUnusedFunctions::parseTokens(const Tokenizer &tokenizer, const Library &library, const std::set& exportedFunctions)
{
const char * const FileName = tokenizer.list.getFiles().front().c_str();
@@ -129,6 +129,11 @@ void CheckUnusedFunctions::parseTokens(const Tokenizer &tokenizer, const Library
}
}
+ for (const std::string& name : exportedFunctions) {
+ mFunctions[name].usedOtherFile = true;
+ mFunctionCalls.insert(name);
+ }
+
// Function usage..
const Token *lambdaEndToken = nullptr;
for (const Token *tok = tokenizer.tokens(); tok; tok = tok->next()) {
diff --git a/lib/checkunusedfunctions.h b/lib/checkunusedfunctions.h
index 1b560cfb7cb..ac253569d85 100644
--- a/lib/checkunusedfunctions.h
+++ b/lib/checkunusedfunctions.h
@@ -45,7 +45,7 @@ class CPPCHECKLIB CheckUnusedFunctions {
// Parse current tokens and determine..
// * Check what functions are used
// * What functions are declared
- void parseTokens(const Tokenizer &tokenizer, const Library &library);
+ void parseTokens(const Tokenizer &tokenizer, const Library &library, const std::set& exportedFunctions = {});
std::string analyzerInfo(const Tokenizer &tokenizer) const;
diff --git a/lib/cppcheck.cpp b/lib/cppcheck.cpp
index 8e32e3e3152..3d2388e9957 100644
--- a/lib/cppcheck.cpp
+++ b/lib/cppcheck.cpp
@@ -1143,7 +1143,7 @@ unsigned int CppCheck::checkInternal(const FileWithDetails& file, const std::str
filesDeleter.addFile(dumpFile);
}
- std::set hashes;
+ std::set>> hashes;
int checkCount = 0;
bool hasValidConfig = false;
std::list configurationError;
@@ -1270,9 +1270,12 @@ unsigned int CppCheck::checkInternal(const FileWithDetails& file, const std::str
mSuppressions.nomsg.markUnmatchedInlineSuppressionsAsChecked(tokenizer.list);
}
- // Skip if we already met the same simplified token list
+ const std::set exportedFunctions = mSettings.checks.isEnabled(Checks::unusedFunction) ?
+ preprocessor.getExportedFunctions() : std::set{};
+
+ // Macro-only references can differ even when the simplified tokens match.
if (maxConfigs > 1) {
- const std::size_t hash = tokenizer.list.calculateHash();
+ const auto hash = std::make_pair(tokenizer.list.calculateHash(), exportedFunctions);
if (hashes.find(hash) != hashes.end()) {
if (mSettings.debugwarnings)
purgedConfigurationMessage(file.spath(), currentConfig);
@@ -1282,7 +1285,7 @@ unsigned int CppCheck::checkInternal(const FileWithDetails& file, const std::str
}
// Check normal tokens
- checkNormalTokens(tokenizer, analyzerInformation.get(), currentConfig);
+ checkNormalTokens(tokenizer, analyzerInformation.get(), currentConfig, exportedFunctions);
} catch (const InternalError &e) {
ErrorMessage errmsg = ErrorMessage::fromInternalError(e, &tokenizer.list, file.spath());
mErrorLogger.reportErr(errmsg);
@@ -1381,7 +1384,8 @@ void CppCheck::internalError(const std::string &filename, const std::string &msg
// CppCheck - A function that checks a normal token list
//---------------------------------------------------------------------------
-void CppCheck::checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation* analyzerInformation, const std::string& currentConfig)
+void CppCheck::checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation* analyzerInformation, const std::string& currentConfig,
+ const std::set& exportedFunctions)
{
const ProgressReporter progressReporter(mErrorLogger, mSettings.reportProgress, tokenizer.list.getSourceFilePath(), "Run checkers");
@@ -1420,10 +1424,10 @@ void CppCheck::checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation
}
if (mSettings.checks.isEnabled(Checks::unusedFunction) && !mSettings.buildDir.empty()) {
- unusedFunctionsChecker.parseTokens(tokenizer, mSettings.library);
+ unusedFunctionsChecker.parseTokens(tokenizer, mSettings.library, exportedFunctions);
}
if (mUnusedFunctionsCheck && mSettings.useSingleJob() && mSettings.buildDir.empty()) {
- mUnusedFunctionsCheck->parseTokens(tokenizer, mSettings.library);
+ mUnusedFunctionsCheck->parseTokens(tokenizer, mSettings.library, exportedFunctions);
}
if (mSettings.clang) {
diff --git a/lib/cppcheck.h b/lib/cppcheck.h
index 4ccd68193ea..dd40e190ce6 100644
--- a/lib/cppcheck.h
+++ b/lib/cppcheck.h
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
#include
#include
@@ -215,7 +216,8 @@ class CPPCHECKLIB CppCheck {
* @param tokenizer tokenizer instance
* @param analyzerInformation the analyzer information
*/
- void checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation* analyzerInformation, const std::string& currentConfig);
+ void checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation* analyzerInformation, const std::string& currentConfig,
+ const std::set& exportedFunctions = {});
/**
* Execute addons
diff --git a/lib/preprocessor.cpp b/lib/preprocessor.cpp
index 325ee4c5c55..549e42a1d22 100644
--- a/lib/preprocessor.cpp
+++ b/lib/preprocessor.cpp
@@ -28,6 +28,7 @@
#include "settings.h"
#include "standards.h"
#include "suppressions.h"
+#include "token.h"
#include "utils.h"
#include
@@ -893,9 +894,230 @@ simplecpp::TokenList Preprocessor::preprocess(const std::string &cfgStr, std::ve
if (!mSettings.keepComments)
tokens2.removeComments();
+ mExportedFunctions.clear();
+ mExportedLocations.clear();
+ if (mSettings.library.isexporter("Q_PROPERTY"))
+ readQtAnnotations(tokens2);
+
return tokens2;
}
+static const simplecpp::Token* qtPropertyAttributes(const simplecpp::Token* tok)
+{
+ while (tok && (tok->str() == "const" || tok->str() == "volatile"))
+ tok = tok->next;
+ if (!tok)
+ return nullptr;
+ if (Token::isStandardType(tok->str())) {
+ do {
+ tok = tok->next;
+ } while (tok && Token::isStandardType(tok->str()));
+ } else {
+ if (tok->str() == "::")
+ tok = tok->next;
+ if (!tok || !tok->name)
+ return nullptr;
+ for (;;) {
+ tok = tok->next;
+ if (tok && tok->str() == "<") {
+ unsigned int depth = 1;
+ unsigned int parentheses = 0;
+ do {
+ tok = tok->next;
+ if (!tok)
+ return nullptr;
+ if (tok->str() == "(")
+ ++parentheses;
+ else if (tok->str() == ")" && parentheses)
+ --parentheses;
+ else if (!parentheses) {
+ if (tok->str() == "<")
+ ++depth;
+ else if (tok->str() == ">")
+ --depth;
+ else if (tok->str() == ">>") {
+ if (depth < 2)
+ return nullptr;
+ depth -= 2;
+ }
+ }
+ } while (depth);
+ tok = tok->next;
+ }
+ if (!tok || tok->str() != "::" || !tok->next || !tok->next->name)
+ break;
+ tok = tok->next;
+ }
+ }
+ while (tok && (tok->str() == "*" || tok->str() == "&" || tok->str() == "&&" ||
+ tok->str() == "const" || tok->str() == "volatile"))
+ tok = tok->next;
+ // The property name is not a function reference.
+ return tok && tok->name ? tok->next : nullptr;
+}
+
+static std::set qtPropertyFunctions(const simplecpp::Token* tok, const Library& library)
+{
+ std::set functions;
+ tok = qtPropertyAttributes(tok);
+ while (tok && tok->str() != ")") {
+ const std::string& attribute = tok->str();
+ tok = tok->next;
+ if (attribute == "CONSTANT" || attribute == "FINAL" || attribute == "REQUIRED" ||
+ attribute == "VIRTUAL" || attribute == "OVERRIDE")
+ continue;
+ if (!tok)
+ return {};
+ if (attribute == "REVISION") {
+ if (tok->number)
+ tok = tok->next;
+ else if (tok->str() == "(") {
+ do {
+ tok = tok->next;
+ } while (tok && (tok->number || tok->str() == ","));
+ if (!tok || tok->str() != ")")
+ return {};
+ tok = tok->next;
+ } else
+ return {};
+ } else if (attribute == "MEMBER") {
+ if (!tok->name)
+ return {};
+ tok = tok->next;
+ } else if (library.isexportedprefix("Q_PROPERTY", attribute)) {
+ const bool parenthesized = tok->str() == "(";
+ if (parenthesized)
+ tok = tok->next;
+ if (!tok)
+ return {};
+ if (tok->str() == "::")
+ tok = tok->next;
+ if (!tok)
+ return {};
+ if (!tok->name)
+ return {};
+ std::string function = tok->str();
+ tok = tok->next;
+ while (tok && tok->str() == "::" && tok->next && tok->next->name) {
+ function = tok->next->str();
+ tok = tok->next->next;
+ }
+ if (function != "true" && function != "false" && function != "default")
+ functions.insert(function);
+ if (parenthesized) {
+ if (!tok || tok->str() != ")")
+ return {};
+ tok = tok->next;
+ }
+ if (tok && tok->str() == "(") {
+ tok = tok->next;
+ if (!tok || tok->str() != ")")
+ return {};
+ tok = tok->next;
+ }
+ } else
+ return {};
+ }
+ return tok ? functions : std::set{};
+}
+
+void Preprocessor::readQtAnnotations(simplecpp::TokenList& tokens)
+{
+ std::set locations;
+ for (const simplecpp::MacroUsage& usage : mMacroUsage) {
+ if (usage.macroName == "QT_ANNOTATE_CLASS")
+ locations.insert(usage.useLocation);
+ }
+ for (simplecpp::Token* tok = tokens.front(); tok;) {
+ if (tok->str() != "__cppcheck_qt_annotation__" ||
+ (tok->macro != "QT_ANNOTATE_CLASS" && locations.find(tok->location) == locations.end()) ||
+ !tok->next || tok->next->str() != "(") {
+ tok = tok->next;
+ continue;
+ }
+ const simplecpp::Token* tag = tok->next->next;
+ if (!tag || tag->str() != "\"cppcheck-qt-annotation\"" || !tag->next || tag->next->str() != ",") {
+ tok = tok->next;
+ continue;
+ }
+ const simplecpp::Token* end = tok->next;
+ unsigned int depth = 0;
+ do {
+ if (end->str() == "(")
+ ++depth;
+ else if (end->str() == ")")
+ --depth;
+ end = end->next;
+ } while (end && depth);
+ if (depth) {
+ tok = tok->next;
+ continue;
+ }
+ const simplecpp::Token* type = tag->next->next;
+ if (type && type->str() == "qt_property" && type->next && type->next->str() == ",") {
+ const auto functions = qtPropertyFunctions(type->next->next, mSettings.library);
+ mExportedFunctions.insert(functions.begin(), functions.end());
+ mExportedLocations.insert(tok->location);
+ }
+ // Qt's default annotation hook expands to nothing. Retain that C++
+ // token stream after recording the property metadata, including for
+ // annotations other than qt_property.
+ while (tok != end) {
+ simplecpp::Token* next = tok->next;
+ tokens.deleteToken(tok);
+ tok = next;
+ }
+ }
+}
+
+std::set Preprocessor::getExportedFunctions() const
+{
+ std::set locations;
+ for (const simplecpp::MacroUsage& usage : mMacroUsage) {
+ if (mSettings.library.isexporter(usage.macroName) &&
+ mExportedLocations.find(usage.useLocation) == mExportedLocations.end())
+ locations.insert(usage.useLocation);
+ }
+
+ std::set functions = mExportedFunctions;
+ if (locations.empty())
+ return functions;
+
+ // Source definitions can erase exporter macros without passing through the
+ // annotation hook. Inspect only invocations expanded in this configuration
+ // and not already handled by the hook, excluding inactive branches.
+ const auto collect = [&](const simplecpp::TokenList& tokens) {
+ for (const simplecpp::Token* tok = tokens.cfront(); tok; tok = tok->next) {
+ if (locations.find(tok->location) == locations.end() ||
+ !mSettings.library.isexporter(tok->str()) || !tok->next || tok->next->str() != "(")
+ continue;
+ if (tok->str() == "Q_PROPERTY") {
+ const auto accessors = qtPropertyFunctions(tok->next->next, mSettings.library);
+ functions.insert(accessors.begin(), accessors.end());
+ continue;
+ }
+ unsigned int depth = 1;
+ for (const simplecpp::Token* arg = tok->next->next; arg; arg = arg->next) {
+ if (arg->str() == "(")
+ ++depth;
+ else if (arg->str() == ")") {
+ if (--depth == 0)
+ break;
+ } else if (depth == 1) {
+ if (mSettings.library.isexportedprefix(tok->str(), arg->str()) && arg->next && arg->next->name)
+ functions.insert(arg->next->str());
+ if (mSettings.library.isexportedsuffix(tok->str(), arg->str()) && arg->previous && arg->previous->name)
+ functions.insert(arg->previous->str());
+ }
+ }
+ }
+ };
+ collect(mTokens);
+ for (const auto& fileData : mFileCache)
+ collect(fileData->tokens);
+ return functions;
+}
+
std::string Preprocessor::getcode(const std::string &cfgStr, std::vector &files, const bool writeLocations)
{
simplecpp::OutputList outputList;
diff --git a/lib/preprocessor.h b/lib/preprocessor.h
index 91a0b7e7d37..22e47d4e606 100644
--- a/lib/preprocessor.h
+++ b/lib/preprocessor.h
@@ -134,6 +134,9 @@ class CPPCHECKLIB WARN_UNUSED Preprocessor {
simplecpp::TokenList preprocess(const std::string &cfgStr, std::vector &files, simplecpp::OutputList& outputList);
+ /** Function references in library exporter macros expanded by the last preprocess(). */
+ std::set getExportedFunctions() const;
+
std::string getcode(const std::string &cfgStr, std::vector &files, bool writeLocations);
/**
@@ -167,6 +170,8 @@ class CPPCHECKLIB WARN_UNUSED Preprocessor {
private:
+ void readQtAnnotations(simplecpp::TokenList& tokens);
+
/**
* Include file types.
*/
@@ -193,6 +198,8 @@ class CPPCHECKLIB WARN_UNUSED Preprocessor {
/** simplecpp tracking info */
std::list mMacroUsage;
std::list mIfCond;
+ std::set mExportedFunctions;
+ std::set mExportedLocations;
};
/// @}
diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile
index e6966747958..e78ffe9a7f4 100644
--- a/oss-fuzz/Makefile
+++ b/oss-fuzz/Makefile
@@ -312,7 +312,7 @@ $(libcppdir)/pathmatch.o: ../lib/pathmatch.cpp ../lib/config.h ../lib/path.h ../
$(libcppdir)/platform.o: ../lib/platform.cpp ../externals/tinyxml2/tinyxml2.h ../lib/config.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/standards.h ../lib/utils.h ../lib/xml.h
$(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/platform.cpp
-$(libcppdir)/preprocessor.o: ../lib/preprocessor.cpp ../externals/simplecpp/simplecpp.h ../lib/checkers.h ../lib/config.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/preprocessor.h ../lib/settings.h ../lib/standards.h ../lib/suppressions.h ../lib/utils.h
+$(libcppdir)/preprocessor.o: ../lib/preprocessor.cpp ../externals/simplecpp/simplecpp.h ../lib/checkers.h ../lib/config.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/preprocessor.h ../lib/settings.h ../lib/smallvector.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/vfvalue.h
$(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/preprocessor.cpp
$(libcppdir)/programmemory.o: ../lib/programmemory.cpp ../lib/astutils.h ../lib/calculate.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/infer.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/programmemory.h ../lib/settings.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenlist.h ../lib/utils.h ../lib/valueflow.h ../lib/valueptr.h ../lib/vfvalue.h
diff --git a/test/cli/unused_function_test.py b/test/cli/unused_function_test.py
index 591986f1dde..9d82abc2d9a 100644
--- a/test/cli/unused_function_test.py
+++ b/test/cli/unused_function_test.py
@@ -3,6 +3,7 @@
import os
import json
+import re
import sys
import pytest
from testutils import cppcheck
@@ -53,6 +54,300 @@ def test_unused_functions():
__test_unused_functions(['-j1', '--no-cppcheck-build-dir'])
+@pytest.mark.parametrize('header', [False, True])
+@pytest.mark.parametrize('builddir', [False, True])
+def test_unused_functions_qt_property(tmp_path, header, builddir):
+ # The accessors are called through Qt's meta-object system, not from C++.
+ code = '''class MyType {
+ Q_PROPERTY(int property
+ READ value WRITE setValue NOTIFY valueChanged)
+public:
+ int value() const { return 0; }
+ void setValue(int) {}
+ void valueChanged() {}
+ void property() {}
+ void unused() {}
+};
+'''
+ source = tmp_path / 'test.cpp'
+ if header:
+ (tmp_path / 'type.h').write_text(code)
+ source.write_text('#include "type.h"\nint main() { MyType t; }\n')
+ else:
+ source.write_text(code + 'int main() { MyType t; }\n')
+ args = ['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', str(source)]
+ if builddir:
+ (tmp_path / 'build').mkdir()
+ args += ['--cppcheck-build-dir=' + str(tmp_path / 'build')]
+ else:
+ args += ['-j1', '--no-cppcheck-build-dir']
+ # Also check the saved whole-program data when the build directory is reused.
+ for _ in range(2 if builddir else 1):
+ ret, stdout, stderr = cppcheck(args)
+ assert ret == 0, stdout
+ assert stdout == ''
+ assert stderr == ("unusedFunction:The function 'property' is never used.\n"
+ "unusedFunction:The function 'unused' is never used.\n")
+
+
+@pytest.mark.parametrize('library', [False, True])
+def test_unused_functions_inactive_qt_property(tmp_path, library):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''#define Q_PROPERTY(...)
+class MyType {
+#if 0
+ Q_PROPERTY(int value READ inactive)
+#endif
+ Q_PROPERTY(int value READ active)
+public:
+ int inactive() const { return 0; }
+ int active() const { return 0; }
+};
+int main() { MyType t; }
+''')
+ args = ['-q', '--template={id}:{message}', '--enable=unusedFunction',
+ '-j1', '--no-cppcheck-build-dir', str(source)]
+ if library:
+ args += ['--library=qt']
+ ret, stdout, stderr = cppcheck(args)
+ assert ret == 0, stdout
+ expected = ["unusedFunction:The function 'inactive' is never used."]
+ if not library:
+ expected += ["unusedFunction:The function 'active' is never used."]
+ assert sorted(stderr.splitlines()) == sorted(expected)
+
+
+def test_unused_functions_qt_property_configurations(tmp_path):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''class MyType {
+#ifdef PROPERTY_VARIANT
+ Q_PROPERTY(int value READ first)
+#else
+ Q_PROPERTY(int value READ second)
+#endif
+public:
+ int first() const { return 0; }
+ int second() const { return 0; }
+ void unused() {}
+};
+int main() { MyType t; }
+''')
+ # Both configurations have identical C++ tokens after Q_PROPERTY is erased.
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stdout == ''
+ assert stderr == "unusedFunction:The function 'unused' is never used.\n"
+
+
+@pytest.mark.parametrize('property_type, property_name', [
+ ('READ', 'property'),
+ ('const Names::READ*', 'property'),
+ ('unsigned long', 'READ'),
+ ('Box>', 'property'),
+])
+def test_unused_functions_qt_property_keyword_names(tmp_path, property_type, property_name):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''class READ {};
+class WRITE {};
+template struct Pair {};
+template struct Box {};
+namespace Names { class READ {}; }
+class MyType {
+ Q_PROPERTY(@TYPE@ @PROPERTY@ READ value WRITE READ NOTIFY valueChanged)
+public:
+ @RETURN_TYPE@ value() const { return {}; }
+ void READ(@RETURN_TYPE@) {}
+ void valueChanged() {}
+ void property() {}
+ void NOTIFY() {}
+};
+int main() { MyType t; }
+'''.replace('@TYPE@', property_type).replace('@PROPERTY@', property_name)
+ .replace('@RETURN_TYPE@', '::READ' if property_type == 'READ' else property_type))
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == ("unusedFunction:The function 'property' is never used.\n"
+ "unusedFunction:The function 'NOTIFY' is never used.\n")
+
+
+def test_unused_functions_qt_property_reset_keyword_name(tmp_path):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''class MyType {
+ Q_PROPERTY(int property READ value RESET READ)
+public:
+ int value() const { return 0; }
+ void READ() {}
+ void property() {}
+};
+int main() { MyType t; }
+''')
+ # The reset method named READ must not be read as another READ attribute.
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == "unusedFunction:The function 'property' is never used.\n"
+
+
+@pytest.mark.parametrize('attributes, used', [
+ ('READ value RESET resetValue', ['value', 'resetValue']),
+ ('MEMBER field READ value', ['value']),
+ ('MEMBER field WRITE setValue', ['setValue']),
+ ('READ value REVISION 2 DESIGNABLE false SCRIPTABLE true STORED false USER true', ['value']),
+ ('READ value REVISION(1, 2)', ['value']),
+ ('READ default WRITE default BINDABLE bindValue', ['bindValue']),
+ ('READ value CONSTANT FINAL', ['value']),
+ ('READ value REQUIRED', ['value']),
+ ('READ value VIRTUAL', ['value']),
+ ('READ value OVERRIDE', ['value']),
+] + [('READ value ' + attribute + ' enabled', ['value', 'enabled'])
+ for attribute in ['DESIGNABLE', 'SCRIPTABLE', 'STORED', 'USER', 'EDITABLE']]
+ + [('READ value DESIGNABLE enabled()', ['value', 'enabled'])])
+def test_unused_functions_qt_property_attributes(tmp_path, attributes, used):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''template struct QBindable {};
+class Other { public: void field() {} };
+class MyType {
+ Q_PROPERTY(int property @ATTRIBUTES@)
+public:
+ int field;
+ int value() const { return 0; }
+ void setValue(int) {}
+ void resetValue() {}
+ QBindable bindValue() { return {}; }
+ bool enabled() const { return true; }
+ void property() {}
+};
+int main() { MyType t; Other other; }
+'''.replace('@ATTRIBUTES@', attributes))
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ reported = re.findall(r"unusedFunction:The function '([^']+)' is never used\.", stderr)
+ assert set(reported) == {'field', 'value', 'setValue', 'resetValue', 'bindValue', 'enabled', 'property'} - set(used)
+ assert len(reported) == len(stderr.splitlines())
+
+
+@pytest.mark.parametrize('definition, invocation', [
+ ('#define PROPERTY Q_PROPERTY(int property READ value)', 'PROPERTY'),
+ ('#define PROPERTY(type, name, get) Q_PROPERTY(type name READ get)', 'PROPERTY(int, property, value)'),
+ ('#define PROPERTY Q_PROPERTY', 'PROPERTY(int property READ value)'),
+ ('#define GETTER value', 'Q_PROPERTY(int property READ GETTER)'),
+ ('#define PROPERTY(get) Q_PROPERTY(int property READ get)\n#define WRAPPER(get) PROPERTY(get)', 'WRAPPER(value)'),
+])
+@pytest.mark.parametrize('header_definition', [False, True])
+def test_unused_functions_qt_property_macros(tmp_path, definition, invocation, header_definition):
+ # Model Qt's annotation extension point, including a source-level definition
+ # of Q_PROPERTY that supersedes the library definition.
+ (tmp_path / 'qt.h').write_text('''#ifndef QT_ANNOTATE_CLASS
+#define QT_ANNOTATE_CLASS(type, ...)
+#endif
+#define Q_PROPERTY(...) QT_ANNOTATE_CLASS(qt_property, __VA_ARGS__)
+''')
+ source = tmp_path / 'test.cpp'
+ source.write_text(('#include "qt.h"\n' if header_definition else '') + definition + '''
+class MyType {
+ @PROPERTY@
+public:
+ int value() const { return 0; }
+ void property() {}
+};
+int main() { MyType t; }
+'''.replace('@PROPERTY@', invocation))
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == "unusedFunction:The function 'property' is never used.\n"
+
+
+@pytest.mark.parametrize('definition, invocation', [
+ ('#define IGNORE(...)', 'IGNORE(Q_PROPERTY(int property READ value))'),
+ ('#define TEXT(...) #__VA_ARGS__', 'const char* text = TEXT(Q_PROPERTY(int property READ value));'),
+ ('', 'QT_ANNOTATE_CLASS(qt_enums, value)'),
+])
+def test_unused_functions_qt_property_discarded_metadata(tmp_path, definition, invocation):
+ source = tmp_path / 'test.cpp'
+ source.write_text(definition + '''
+class MyType {
+ @PROPERTY@
+public:
+ int value() const { return 0; }
+};
+int main() { MyType t; }
+'''.replace('@PROPERTY@', invocation))
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == "unusedFunction:The function 'value' is never used.\n"
+
+
+def test_qt_annotations_preserve_user_code(tmp_path):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''#define CALL __cppcheck_qt_annotation__(1, 2)
+#define QT_ANNOTATE_CLASS(type, ...) int userHook;
+void __cppcheck_qt_annotation__(int, int) {}
+class MyType {
+ Q_PROPERTY(int property READ value)
+public:
+ int value() const { return 0; }
+};
+int main() { CALL; }
+''')
+ ret, stdout, stderr = cppcheck(['-q', '-E', '--library=qt', str(source)])
+ assert ret == 0, stderr
+ tokens = ' '.join(stdout.split())
+ assert 'int userHook ;' in tokens
+ assert '__cppcheck_qt_annotation__ ( 1 , 2 )' in tokens
+
+
+@pytest.mark.parametrize('replacement', [
+ 'QT_ANNOTATE_CLASS(qt_other, ignored) __cppcheck_qt_annotation__(1, 2)',
+ '__cppcheck_qt_annotation__(1, 2) QT_ANNOTATE_CLASS(qt_other, ignored)',
+ 'QT_ANNOTATE_CLASS(qt_other, ignored) __cppcheck_qt_annotation__(1, 2) QT_ANNOTATE_CLASS(qt_another, ignored)',
+])
+def test_qt_annotations_share_macro_location(tmp_path, replacement):
+ source = tmp_path / 'test.cpp'
+ source.write_text('#define MIXED() ' + replacement + '''
+void __cppcheck_qt_annotation__(int, int) {}
+int main() { MIXED(); }
+''')
+ # Object-like expansion can attach the outer macro's name and location to
+ # both a Qt annotation and unrelated user tokens. The internal tag matters.
+ ret, stdout, stderr = cppcheck(['-q', '-E', '--library=qt', str(source)])
+ assert ret == 0, stderr
+ tokens = ' '.join(stdout.split())
+ assert 'int main ( ) { __cppcheck_qt_annotation__ ( 1 , 2 ) ; }' in tokens
+ assert 'qt_other' not in tokens
+ assert 'qt_another' not in tokens
+
+
+def test_unused_functions_qt_property_qualified_accessor(tmp_path):
+ source = tmp_path / 'test.cpp'
+ source.write_text('''class Base {
+public:
+ int value() const { return 0; }
+};
+class MyType : public Base {
+ Q_PROPERTY(int property READ (Base::value))
+public:
+ void unused() {}
+};
+int main() { MyType t; }
+''')
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=qt',
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == "unusedFunction:The function 'unused' is never used.\n"
+
+
def test_unused_functions_j():
args = [
'-q',
@@ -201,4 +496,4 @@ def test_unused_functions_compdb_buildir_j_thread(tmpdir):
def test_unused_functions_compdb_builddir_j_process(tmpdir):
build_dir = os.path.join(tmpdir, 'b1')
os.mkdir(build_dir)
- __test_unused_functions_compdb(tmpdir, ['-j2', '--cppcheck-build-dir={}'.format(build_dir), '--executor=process'])
\ No newline at end of file
+ __test_unused_functions_compdb(tmpdir, ['-j2', '--cppcheck-build-dir={}'.format(build_dir), '--executor=process'])