From 69effb8d7836e14a08b095ba21ecd3be4930acdf Mon Sep 17 00:00:00 2001 From: tzi4 Date: Tue, 8 Sep 2026 16:57:40 +0300 Subject: [PATCH 1/2] Fix #4270: preserve Qt property accessor references for unusedFunction --- cfg/qt.cfg | 12 +- lib/checkunusedfunctions.cpp | 7 +- lib/checkunusedfunctions.h | 2 +- lib/cppcheck.cpp | 18 +- lib/cppcheck.h | 4 +- lib/preprocessor.cpp | 222 +++++++++++++++++++++++ lib/preprocessor.h | 7 + test/cli/unused_function_test.py | 297 ++++++++++++++++++++++++++++++- 8 files changed, 555 insertions(+), 14 deletions(-) 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..82ca1cdaa19 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..0555324fd22 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; + } + 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/test/cli/unused_function_test.py b/test/cli/unused_function_test.py index 591986f1dde..d1b9c8fc2e3 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 += ['--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', + '--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', + '--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', + '--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', + '--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', + '--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', + '--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', + '--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', + '--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']) From 8825355d017efa5335706558529ea190f3efbbb6 Mon Sep 17 00:00:00 2001 From: tzi4 Date: Wed, 9 Sep 2026 17:08:41 +0300 Subject: [PATCH 2/2] Fix Qt property CI checks --- Makefile | 2 +- lib/cppcheck.cpp | 4 ++-- lib/preprocessor.cpp | 4 ++-- oss-fuzz/Makefile | 2 +- test/cli/unused_function_test.py | 18 +++++++++--------- 5 files changed, 15 insertions(+), 15 deletions(-) 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/lib/cppcheck.cpp b/lib/cppcheck.cpp index 82ca1cdaa19..3d2388e9957 100644 --- a/lib/cppcheck.cpp +++ b/lib/cppcheck.cpp @@ -1271,7 +1271,7 @@ unsigned int CppCheck::checkInternal(const FileWithDetails& file, const std::str } const std::set exportedFunctions = mSettings.checks.isEnabled(Checks::unusedFunction) ? - preprocessor.getExportedFunctions() : std::set{}; + preprocessor.getExportedFunctions() : std::set{}; // Macro-only references can differ even when the simplified tokens match. if (maxConfigs > 1) { @@ -1385,7 +1385,7 @@ void CppCheck::internalError(const std::string &filename, const std::string &msg //--------------------------------------------------------------------------- void CppCheck::checkNormalTokens(const Tokenizer &tokenizer, AnalyzerInformation* analyzerInformation, const std::string& currentConfig, - const std::set& exportedFunctions) + const std::set& exportedFunctions) { const ProgressReporter progressReporter(mErrorLogger, mSettings.reportProgress, tokenizer.list.getSourceFilePath(), "Run checkers"); diff --git a/lib/preprocessor.cpp b/lib/preprocessor.cpp index 0555324fd22..549e42a1d22 100644 --- a/lib/preprocessor.cpp +++ b/lib/preprocessor.cpp @@ -961,7 +961,7 @@ static std::set qtPropertyFunctions(const simplecpp::Token* tok, co std::set functions; tok = qtPropertyAttributes(tok); while (tok && tok->str() != ")") { - const std::string attribute = tok->str(); + const std::string& attribute = tok->str(); tok = tok->next; if (attribute == "CONSTANT" || attribute == "FINAL" || attribute == "REQUIRED" || attribute == "VIRTUAL" || attribute == "OVERRIDE") @@ -1040,7 +1040,7 @@ void Preprocessor::readQtAnnotations(simplecpp::TokenList& tokens) tok = tok->next; continue; } - simplecpp::Token* end = tok->next; + const simplecpp::Token* end = tok->next; unsigned int depth = 0; do { if (end->str() == "(") 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 d1b9c8fc2e3..9d82abc2d9a 100644 --- a/test/cli/unused_function_test.py +++ b/test/cli/unused_function_test.py @@ -80,7 +80,7 @@ def test_unused_functions_qt_property(tmp_path, header, builddir): (tmp_path / 'build').mkdir() args += ['--cppcheck-build-dir=' + str(tmp_path / 'build')] else: - args += ['--no-cppcheck-build-dir'] + 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) @@ -106,7 +106,7 @@ class MyType { int main() { MyType t; } ''') args = ['-q', '--template={id}:{message}', '--enable=unusedFunction', - '--no-cppcheck-build-dir', str(source)] + '-j1', '--no-cppcheck-build-dir', str(source)] if library: args += ['--library=qt'] ret, stdout, stderr = cppcheck(args) @@ -135,7 +135,7 @@ def test_unused_functions_qt_property_configurations(tmp_path): # Both configurations have identical C++ tokens after Q_PROPERTY is erased. ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-j1', '--no-cppcheck-build-dir', str(source)]) assert ret == 0, stdout assert stdout == '' assert stderr == "unusedFunction:The function 'unused' is never used.\n" @@ -168,7 +168,7 @@ class MyType { .replace('@RETURN_TYPE@', '::READ' if property_type == 'READ' else property_type)) ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-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") @@ -188,7 +188,7 @@ def test_unused_functions_qt_property_reset_keyword_name(tmp_path): # 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', - '--no-cppcheck-build-dir', str(source)]) + '-j1', '--no-cppcheck-build-dir', str(source)]) assert ret == 0, stdout assert stderr == "unusedFunction:The function 'property' is never used.\n" @@ -226,7 +226,7 @@ class MyType { '''.replace('@ATTRIBUTES@', attributes)) ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-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) @@ -261,7 +261,7 @@ class MyType { '''.replace('@PROPERTY@', invocation)) ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-j1', '--no-cppcheck-build-dir', str(source)]) assert ret == 0, stdout assert stderr == "unusedFunction:The function 'property' is never used.\n" @@ -283,7 +283,7 @@ class MyType { '''.replace('@PROPERTY@', invocation)) ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-j1', '--no-cppcheck-build-dir', str(source)]) assert ret == 0, stdout assert stderr == "unusedFunction:The function 'value' is never used.\n" @@ -343,7 +343,7 @@ class MyType : public Base { ''') ret, stdout, stderr = cppcheck(['-q', '--template={id}:{message}', '--enable=unusedFunction', '--library=qt', - '--no-cppcheck-build-dir', str(source)]) + '-j1', '--no-cppcheck-build-dir', str(source)]) assert ret == 0, stdout assert stderr == "unusedFunction:The function 'unused' is never used.\n"