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..11ade2b8a87 100644
--- a/lib/preprocessor.cpp
+++ b/lib/preprocessor.cpp
@@ -893,9 +893,130 @@ 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 std::set exporterFunctions(const simplecpp::Token* tok, const std::string& exporter, const Library& library)
+{
+ // Match configured exporter keywords without parsing library-specific grammar.
+ std::set functions;
+ unsigned int depth = 0;
+ for (; tok; tok = tok->next) {
+ if (tok->str() == "(")
+ ++depth;
+ else if (tok->str() == ")") {
+ if (depth == 0)
+ break;
+ --depth;
+ } else if (depth == 0) {
+ if (library.isexportedsuffix(exporter, tok->str()) && tok->previous && tok->previous->name)
+ functions.insert(tok->previous->str());
+ if (!library.isexportedprefix(exporter, tok->str()))
+ continue;
+ const simplecpp::Token* name = tok->next;
+ const bool parenthesized = name && name->str() == "(";
+ if (parenthesized)
+ name = name->next;
+ if (name && name->str() == "::")
+ name = name->next;
+ if (!name || !name->name)
+ continue;
+ while (name->next && name->next->str() == "::" && name->next->next && name->next->next->name)
+ name = name->next->next;
+ if (parenthesized && (!name->next || name->next->str() != ")"))
+ continue;
+ if (name->str() != "true" && name->str() != "false" && name->str() != "default")
+ functions.insert(name->str());
+ }
+ }
+ return functions;
+}
+
+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 = exporterFunctions(type->next->next, "Q_PROPERTY", 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;
+ const auto exported = exporterFunctions(tok->next->next, tok->str(), mSettings.library);
+ functions.insert(exported.begin(), exported.end());
+ }
+ };
+ 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/test/cli/unused_function_test.py b/test/cli/unused_function_test.py
index 591986f1dde..c36ae96f5e0 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,344 @@ 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, reported', [
+ ('READ', 'property', ['unrelated']),
+ ('const Names::READ*', 'property', ['property', 'unrelated']),
+ ('unsigned long', 'READ', ['property', 'unrelated']),
+ ('Box>', 'property', ['property', 'unrelated']),
+])
+def test_unused_functions_qt_property_keyword_names(tmp_path, property_type, property_name, reported):
+ 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() {}
+ void unrelated() {}
+};
+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
+ # Exporter keywords are matched without parsing the property's type/name.
+ # Keyword collisions can hide unused property/NOTIFY methods, while the
+ # actual accessors must stay used and unrelated methods must still warn.
+ assert stderr.splitlines() == ["unusedFunction:The function '{}' is never used.".format(name)
+ for name in reported]
+
+
+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"
+
+
+@pytest.mark.parametrize('metadata', [
+ 'GET value',
+ 'GET (Base::value)',
+ 'value USED',
+ 'NESTED(GET unrelated) GET value',
+])
+def test_unused_functions_library_exporter(tmp_path, metadata):
+ library = tmp_path / 'exporter.cfg'
+ library.write_text('''
+
+
+
+
+ GET
+ USED
+
+
+
+
+''')
+ source = tmp_path / 'test.cpp'
+ source.write_text('''#define EXPORT(...)
+class Base {
+public:
+ int value() const { return 0; }
+};
+class MyType : public Base {
+ EXPORT(@METADATA@)
+public:
+ void unrelated() {}
+};
+int main() { MyType t; }
+'''.replace('@METADATA@', metadata))
+ ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}',
+ '--enable=unusedFunction', '--library=' + str(library),
+ '-j1', '--no-cppcheck-build-dir', str(source)])
+ assert ret == 0, stdout
+ assert stderr == "unusedFunction:The function 'unrelated' is never used.\n"
+
+
def test_unused_functions_j():
args = [
'-q',
@@ -201,4 +540,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'])