/* * Cppcheck - A tool for static C/C++ code analysis * Copyright (C) 2007-2026 Cppcheck team. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * Test suite for the DataFlow analysis (lib/dataflow.cpp). * * Design rules for this file: * * - Every test corresponds to exactly one task from the implementation plan. * - Each test must FAIL before the fix is applied and PASS after. * - The analysis is run at CheckLevel::normal (the level DataFlow is active). * - Tests look for token "x" at a specific source line and check its values. * - False-positive regression tests are added as trac tickets are resolved; * those tests verify that the analysis does NOT report a value. * * Helper methods: * - testValueOfXKnown(code, linenr, value) → x has Known integer value * - testValueOfXPossible(code, linenr, value) → x has Possible integer value * - testValueOfXImpossible(code, linenr, value) → x has Impossible integer value * - testValueOfXNone(code, linenr) → x has no values at all */ #include "errortypes.h" #include "fixture.h" #include "helpers.h" #include "mathlib.h" #include "settings.h" #include "token.h" #include "vfvalue.h" #include #include #include #include #include #include class TestDataFlow : public TestFixture { public: TestDataFlow() : TestFixture("TestDataFlow") {} private: // Settings with CheckLevel::normal so DataFlow (not ValueFlow) is used. // Requirement: the new analysis runs at normal check level. /*const*/ Settings settings = settingsBuilder().checkLevel(Settings::CheckLevel::normal).build(); void run() override { // Phase 1 — Forward: basic integer constant tracking TEST_CASE(constantPropagation); // Phase 2 — Forward: arithmetic and constant folding TEST_CASE(arithmetic); // Phase 3 — Forward: branches (if/else) TEST_CASE(forwardBranches); // Phase 4 — Forward: loops TEST_CASE(forwardLoops); // Phase 5 — Forward: function calls (conservative) TEST_CASE(forwardFunctionCalls); // Phase B1 — Backward: basic constraint propagation TEST_CASE(backwardConstraints); // Phase R — Relational operator constraints (< > <= >=) TEST_CASE(relationalConstraints); // Phase OT — Operator token value annotation TEST_CASE(operatorTokenValues); // Phase UI — Unsigned integer impossible-value constraints TEST_CASE(unsignedImpossible); // Phase SW — Switch/case value propagation TEST_CASE(switchVariable); // Phase LB — For-loop variable bounds inside loop body TEST_CASE(loopBounds); // Phase BW — Bitwise operation value annotation TEST_CASE(bitwiseOps); // Phase TT — Type truncation on assignment TEST_CASE(typeTruncation); // Robustness: no crash or hang on pathological inputs TEST_CASE(nocrash); // Phase 6 — Complexity abort: no false positives TEST_CASE(complexityAbort); // Phase N — Null pointer tracking TEST_CASE(nullPointer); // Phase U — Uninitialized variable tracking TEST_CASE(uninitVariable); // Phase F — Float/double value tracking TEST_CASE(floatPropagation); // Phase S — String literal non-null tracking TEST_CASE(stringLiteralNonNull); // Phase U2 — Enhanced uninit tracking (survives function calls) TEST_CASE(uninitAfterCall); // Phase M — Struct/class member field tracking TEST_CASE(memberFieldPropagation); // Phase M2 — Member-access expression carries the field value // (the '.' token must be annotated so division-by-zero checkers find it) TEST_CASE(memberFieldDivisionByZero); // Phase MN — Struct member pointer null condition constraints TEST_CASE(memberPtrCondition); // Phase C — Container size tracking TEST_CASE(containerSize); // Phase Cast — Cast expression value propagation TEST_CASE(castValuePropagation); // Literal constant annotation — integer/float literals must be annotated // with their Known values so that checkers (checkZeroDivision, arrayIndex) // can find them via getValue() in normal check level. TEST_CASE(literalAnnotation); // False-positive regression tests (grows as trac tickets are resolved) TEST_CASE(falsePositiveRegression); } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- // removeImpossible: filter impossible values out of a value list. // Used by valueOfTok() to find the single "useful" non-impossible value. static std::list removeImpossible(std::list values) { values.remove_if(std::mem_fn(&ValueFlow::Value::isImpossible)); return values; } // removeSymbolicTok: filter symbolic and tok values out of a value list. static std::list removeSymbolicTok(std::list values) { values.remove_if([](const ValueFlow::Value& v) { return v.isSymbolicValue() || v.isTokValue(); }); return values; } // tokenValues: returns all ValueFlow::Value objects on the first token // matched by pattern `tokstr` (via Token::findmatch). This enables // assertions on operator tokens (e.g. "+", "-") and arbitrary patterns, // not just on named variables. #define tokenValues(...) tokenValues_(__FILE__, __LINE__, __VA_ARGS__) std::list tokenValues_(const char* file, int line, const char code[], const char tokstr[], const Settings *s = nullptr) { SimpleTokenizer tokenizer(s ? *s : settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); const Token *tok = Token::findmatch(tokenizer.tokens(), tokstr); return tok ? tok->values() : std::list(); } // tokenValues overload: filter by ValueType after fetching. std::list tokenValues_(const char* file, int line, const char code[], const char tokstr[], ValueFlow::Value::ValueType vt) { std::list values = tokenValues_(file, line, code, tokstr); values.remove_if([vt](const ValueFlow::Value& v) { return v.valueType != vt; }); return values; } // valueOfTok: returns the single non-impossible, non-tok value on the // first token matched by `tokstr`, or a default Value() if there is not // exactly one such value. Mirrors the same helper in testvalueflow.cpp. #define valueOfTok(...) valueOfTok_(__FILE__, __LINE__, __VA_ARGS__) ValueFlow::Value valueOfTok_(const char* file, int line, const char code[], const char tokstr[], const Settings *s = nullptr) { std::list values = removeImpossible(tokenValues_(file, line, code, tokstr, s)); values.remove_if([](const ValueFlow::Value& v) { return v.isTokValue(); }); return values.size() == 1U ? values.front() : ValueFlow::Value(); } // getErrorPathForX: returns the error-path string attached to all values // on the first "x" token at `linenr`. Format per step: "linenr,msg\n". // Enables regression testing of diagnostic reasoning chains. #define getErrorPathForX(...) getErrorPathForX_(__FILE__, __LINE__, __VA_ARGS__) std::string getErrorPathForX_(const char* file, int line, const char code[], unsigned int linenr) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token *tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; std::ostringstream ostr; for (const ValueFlow::Value &v : tok->values()) { for (const ErrorPathItem &ep : v.errorPath) { ostr << ep.first->linenr() << ',' << ep.second << '\n'; } } return ostr.str(); } return ""; } // __FILE__/__LINE__ delegating macros for value-checking helpers. // When an assertion fails, the error pinpoints the call site in the test // body rather than inside the helper implementation. /// Requirement: x at linenr has a Known integer value equal to `value`. #define testValueOfXKnown(...) testValueOfXKnown_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXKnown_(const char* file, int line, const char code[], unsigned int linenr, int value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isKnown() && v.intvalue == value; })) return true; } return false; } /// Requirement: x at linenr has a Possible integer value equal to `value`. #define testValueOfXPossible(...) testValueOfXPossible_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXPossible_(const char* file, int line, const char code[], unsigned int linenr, int value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isPossible() && v.intvalue == value; })) return true; } return false; } /// Requirement: x at linenr has an Impossible integer value equal to `value`. #define testValueOfXImpossible(...) testValueOfXImpossible_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXImpossible_(const char* file, int line, const char code[], unsigned int linenr, int value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isImpossible() && v.intvalue == value; })) return true; } return false; } /// Requirement: x at linenr has an Inconclusive integer value equal to `value`. /// Inconclusive differs from Possible: it means the analysis is unsure /// whether the value is reachable, not just that multiple values exist. #define testValueOfXInconclusive(...) testValueOfXInconclusive_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXInconclusive_(const char* file, int line, const char code[], unsigned int linenr, int value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isInconclusive() && v.intvalue == value; })) return true; } return false; } /// Requirement: x at linenr has a UNINIT value (uninitialized at that point). /// Phase U: CheckUninitVar reads UNINIT values placed on tokens by the /// analysis; this helper verifies that annotation is happening. #define testValueOfXUninit(...) testValueOfXUninit_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXUninit_(const char* file, int line, const char code[], unsigned int linenr) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [](const ValueFlow::Value& v) { return v.isUninitValue(); })) return true; } return false; } /// Overload: same as above but uses a caller-supplied Settings instance. /// Needed for FP tests that require a library (e.g. std.cfg) to recognise /// noreturn functions such as exit(). bool testValueOfXUninit_(const char* file, int line, const char code[], unsigned int linenr, const Settings& s) { SimpleTokenizer tokenizer(s, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [](const ValueFlow::Value& v) { return v.isUninitValue(); })) return true; } return false; } /// Requirement: the cast token '(' at linenr has a Known integer value /// equal to `value`. Phase Cast: verifies cast-expression token annotation. #define testCastKnown(...) testCastKnown_(__FILE__, __LINE__, __VA_ARGS__) bool testCastKnown_(const char* file, int line, const char code[], unsigned int linenr, int value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "(" || !tok->isCast() || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isKnown() && v.intvalue == value; })) return true; } return false; } /// Requirement: x at linenr has NO integer values at all (neither Known, /// Possible, nor Impossible). #define testValueOfXNone(...) testValueOfXNone_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXNone_(const char* file, int line, const char code[], unsigned int linenr) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; // If any integer value is present, the "none" assertion fails. if (std::any_of(tok->values().cbegin(), tok->values().cend(), [](const ValueFlow::Value& v) { return v.isIntValue(); })) return false; return true; // found the token, no integer values } return true; // token not found — trivially no values } // ----------------------------------------------------------------------- // Phase 1 — Forward: basic integer constant tracking // ----------------------------------------------------------------------- void constantPropagation() { // Task 1.1: simple assignment from integer literal // After "int x = 5;" the next use of x should have Known value 5. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " (void)x;\n" // 3 ← x is 5 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } // Task 1.2a: re-assignment — x has the new value after the second assignment. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " x = 10;\n" // 3 " (void)x;\n" // 4 ← x is 10 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 4, 10)); ASSERT(!testValueOfXKnown(code, 4, 5)); } // Task 1.2b: re-assignment to unknown value — x has no known value. { const char code[] = "void f(int y) {\n" // 1 " int x = 5;\n" // 2 " x = y;\n" // 3 " (void)x;\n" // 4 ← no known value "}\n"; ASSERT(!testValueOfXKnown(code, 4, 5)); } // Task 1.3: copy propagation — value flows through intermediate variables. // We test that x on the RHS of "int y = x;" retains the value 5. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " int y = x;\n" // 3 ← x used here; should be 5 "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } // Task 1.4: non-integer variable — x must NOT receive a known value // when it is assigned from an untracked type (float cast). { const char code[] = "void f(float f) {\n" // 1 " int x = (int)f;\n" // 2 " (void)x;\n" // 3 ← no known value "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } } // ----------------------------------------------------------------------- // Phase 2 — Forward: arithmetic and constant folding // ----------------------------------------------------------------------- void arithmetic() { // Task 2.1: arithmetic on two literals. { const char code[] = "void f() {\n" // 1 " int x = 3 + 4;\n" // 2 " (void)x;\n" // 3 ← x is 7 "}\n"; ASSERT(testValueOfXKnown(code, 3, 7)); } { const char code[] = "void f() {\n" // 1 " int x = 10 - 3;\n" // 2 " (void)x;\n" // 3 ← x is 7 "}\n"; ASSERT(testValueOfXKnown(code, 3, 7)); } { const char code[] = "void f() {\n" // 1 " int x = 3 * 4;\n" // 2 " (void)x;\n" // 3 ← x is 12 "}\n"; ASSERT(testValueOfXKnown(code, 3, 12)); } { const char code[] = "void f() {\n" // 1 " int x = 10 / 2;\n" // 2 " (void)x;\n" // 3 ← x is 5 "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } // Task 2.2: arithmetic involving a tracked variable. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " int y = x + 3;\n" // 3 ← x is 5 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } // Task 2.3: compound assignment +=. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " x += 3;\n" // 3 " (void)x;\n" // 4 ← x is 8 "}\n"; ASSERT(testValueOfXKnown(code, 4, 8)); } // Task 2.3: compound assignment -=. { const char code[] = "void f() {\n" // 1 " int x = 10;\n" // 2 " x -= 3;\n" // 3 " (void)x;\n" // 4 ← x is 7 "}\n"; ASSERT(testValueOfXKnown(code, 4, 7)); } // Task 2.4: post-increment. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " x++;\n" // 3 " (void)x;\n" // 4 ← x is 6 "}\n"; ASSERT(testValueOfXKnown(code, 4, 6)); } // Task 2.4: pre-decrement. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " --x;\n" // 3 " (void)x;\n" // 4 ← x is 4 "}\n"; ASSERT(testValueOfXKnown(code, 4, 4)); } // Task 2.5: division by zero — no crash, no value assigned. { const char code[] = "void f() {\n" // 1 " int x = 10 / 0;\n" // 2 " (void)x;\n" // 3 ← no known value "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } } // ----------------------------------------------------------------------- // Phase 3 — Forward: branches (if/else) // ----------------------------------------------------------------------- void forwardBranches() { // Task 3.1: both branches assign the same value → Known after merge. { const char code[] = "void f(int c) {\n" // 1 " int x;\n" // 2 " if (c) { x = 1; } else { x = 1; }\n" // 3 " (void)x;\n" // 4 ← x is 1 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 4, 1)); } // Task 3.2: branches assign DIFFERENT values → two Possible values. // The analysis must NOT report a single Known value after the merge. // Both Possible values must be present. { const char code[] = "void f(int c) {\n" // 1 " int x;\n" // 2 " if (c) { x = 1; } else { x = 2; }\n" // 3 " (void)x;\n" // 4 "}\n"; ASSERT(testValueOfXPossible(code, 4, 1)); ASSERT(testValueOfXPossible(code, 4, 2)); ASSERT(!testValueOfXKnown(code, 4, 1)); ASSERT(!testValueOfXKnown(code, 4, 2)); } // Task 3.3: one-sided if (no else) → both the original and the // branch-modified values are Possible after the if. { const char code[] = "void f(int c) {\n" // 1 " int x = 5;\n" // 2 " if (c) { x = 10; }\n" // 3 " (void)x;\n" // 4 "}\n"; ASSERT(testValueOfXPossible(code, 4, 5)); ASSERT(testValueOfXPossible(code, 4, 10)); } // Task 3.4: if with return in the then-branch → the surviving path // (no-return) continues with x=10 as a Known value. { const char code[] = "void f(int c) {\n" // 1 " int x = 5;\n" // 2 " if (c) { return; }\n" // 3 " x = 10;\n" // 4 " (void)x;\n" // 5 ← x is 10 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 5, 10)); } // Task 3.5: condition constraint inside the then-block. // After "if (x == 5)" the variable x is Known 5 inside the block. { const char code[] = "void f(int x) {\n" // 1 " if (x == 5) {\n" // 2 " (void)x;\n" // 3 ← x is 5 (Known) inside block " }\n" // 4 "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } } // ----------------------------------------------------------------------- // Phase 4 — Forward: loops // ----------------------------------------------------------------------- void forwardLoops() { // Task 4.1: variable modified in loop body → unknown after the loop. // After a while loop that increments x, x cannot be known. { const char code[] = "void f(int c) {\n" // 1 " int x = 5;\n" // 2 " while (c) { x++; }\n" // 3 " (void)x;\n" // 4 ← x is NOT known "}\n"; ASSERT(!testValueOfXKnown(code, 4, 5)); ASSERT(!testValueOfXKnown(code, 4, 6)); } // Task 4.2: loop-invariant variable survives the loop with Known value. { const char code[] = "void f(int n) {\n" // 1 " int x = 5;\n" // 2 " for (int i = 0; i < n; i++) {}\n" // 3 " (void)x;\n" // 4 ← x is still 5 "}\n"; ASSERT(testValueOfXKnown(code, 4, 5)); } // Task NW1: while loop with null-guard && condition. // After "while (x && x->n > 0)", x might be null at the loop exit // (the loop can exit because x became null OR because x->n <= 0). // Requirement: x must carry a Possible null (0) value at line 6. { const char code[] = "struct S { int n; struct S *next; };\n" // 1 "int foo(struct S *x) {\n" // 2 " while (x && x->n > 0) {\n" // 3 " x = x->next;\n" // 4 " }\n" // 5 " (void)x;\n" // 6 "}\n"; ASSERT(testValueOfXPossible(code, 6, 0)); } // Task NW2: simple while (x) → after the loop, x is Known null. // Requirement: x must carry a Known null (0) value at line 6. { const char code[] = "struct S { struct S *next; };\n" // 1 "void foo(struct S *x) {\n" // 2 " while (x) {\n" // 3 " x = x->next;\n" // 4 " }\n" // 5 " (void)x;\n" // 6 "}\n"; ASSERT(testValueOfXKnown(code, 6, 0)); } // Task NW3: do-while loop with null-guard && condition. // After "do { x = x->next; } while (x && x->n > 0)", the loop exits // when the condition is false — which happens when x is null. // Requirement: x must carry a Possible null (0) value after the loop. { const char code[] = "struct S { int n; struct S* next; };\n" // 1 "int foo(struct S* x) {\n" // 2 " do {\n" // 3 " x = x->next;\n" // 4 " } while (x && x->n > 0);\n" // 5 " (void)x;\n" // 6 "}\n"; ASSERT(testValueOfXPossible(code, 6, 0)); } } // ----------------------------------------------------------------------- // Phase 5 — Forward: function calls (conservative) // ----------------------------------------------------------------------- void forwardFunctionCalls() { // Task 5.1: local scalar variables survive a function call. // x is a local int; foo() has no parameters and cannot access x, // so x remains known as 5 after the call. { const char code[] = "void foo();\n" // 1 "void f() {\n" // 2 " int x = 5;\n" // 3 " foo();\n" // 4 " (void)x;\n" // 5 ← x is still 5 (local scalar) "}\n"; ASSERT(testValueOfXKnown(code, 5, 5)); } // Task 5.2: variables assigned AFTER a call are still tracked. { const char code[] = "void foo();\n" // 1 "void f() {\n" // 2 " foo();\n" // 3 " int x = 5;\n" // 4 " (void)x;\n" // 5 ← x is 5 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 5, 5)); } } // ----------------------------------------------------------------------- // Phase B1 — Backward: basic constraint propagation // ----------------------------------------------------------------------- void backwardConstraints() { // Task B1.1: condition "if (x == 5)" propagates x's value backward // to uses of x that appear BEFORE the condition. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← backward: x is possibly 5 " if (x == 5) {}\n" // 3 "}\n"; ASSERT(testValueOfXPossible(code, 2, 5)); } // Task B1.2: "if (x != 0)" propagates the impossible value 0 backward. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← backward: x is impossible 0 " if (x != 0) {}\n" // 3 "}\n"; ASSERT(testValueOfXImpossible(code, 2, 0)); } // Task B1.3: an assignment to x BETWEEN the use and the condition // severs the backward chain — x at line 2 must NOT get the constraint. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← must NOT have value 5 " x = 10;\n" // 3 severs backward chain " if (x == 5) {}\n" // 4 "}\n"; ASSERT(!testValueOfXPossible(code, 2, 5)); } // Task B1.4: constraint propagates to multiple uses before the condition. { const char code[] = "void g(int);\n" // 1 "void f(int x) {\n" // 2 " g(x);\n" // 3 ← backward: x possibly 5 " (void)x;\n" // 4 ← backward: x possibly 5 " if (x == 5) {}\n" // 5 "}\n"; ASSERT(testValueOfXPossible(code, 4, 5)); } } // ----------------------------------------------------------------------- // Phase R — Relational operator constraints (< > <= >=) // ----------------------------------------------------------------------- // // When a condition uses a relational operator, the analysis constrains x // inside the true branch. For "if (x > 5)" the minimum value of x in // the then-block is 6; for "if (x < 5)" the maximum is 4. These // constraints are expressed as Possible values at the boundary. // // Requirement: false negatives are preferable to false positives — if the // analysis cannot determine the exact constraint it must not emit a wrong // Known value. void relationalConstraints() { // R1: "if (x > 5)" → inside the block x is possibly 6 (the minimum // value that satisfies x > 5 for integers). { const char code[] = "void f(int x) {\n" // 1 " if (x > 5) {\n" // 2 " (void)x;\n" // 3 ← x possibly 6 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 6)); } // R2: "if (x < 5)" → inside the block x is possibly 4 (the maximum // value that satisfies x < 5 for integers). { const char code[] = "void f(int x) {\n" // 1 " if (x < 5) {\n" // 2 " (void)x;\n" // 3 ← x possibly 4 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 4)); } // R3: "if (x >= 5)" → x is possibly 5 inside the block (exact lower bound). { const char code[] = "void f(int x) {\n" // 1 " if (x >= 5) {\n" // 2 " (void)x;\n" // 3 ← x possibly 5 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 5)); } // R4: "if (x <= 5)" → x is possibly 5 inside the block (exact upper bound). { const char code[] = "void f(int x) {\n" // 1 " if (x <= 5) {\n" // 2 " (void)x;\n" // 3 ← x possibly 5 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 5)); } // R5: reversed operands "if (5 < x)" is equivalent to "if (x > 5)". { const char code[] = "void f(int x) {\n" // 1 " if (5 < x) {\n" // 2 " (void)x;\n" // 3 ← x possibly 6 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 6)); } // R6: relational constraint does NOT produce a Known value — the // analysis must not infer x == 6 just because x > 5. { const char code[] = "void f(int x) {\n" // 1 " if (x > 5) {\n" // 2 " (void)x;\n" // 3 ← x must NOT be Known 6 " }\n" // 4 "}\n"; ASSERT(!testValueOfXKnown(code, 3, 6)); } // R7: backward propagation of relational constraint. // Before "if (x > 5)" the variable x possibly satisfies x > 5. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 6 " if (x > 5) {}\n" // 3 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 2, 6)); } } // ----------------------------------------------------------------------- // Phase OT — Operator token value annotation // ----------------------------------------------------------------------- // // Checkers such as checkZeroDivision and arrayIndex examine the value on // the *operator token* itself (e.g. the "/" token) to decide whether a // division by zero is happening. DataFlow must annotate operator tokens // with the computed value when both operands are known constants, not // only the receiving variable. // // These tests use valueOfTok() which returns the single non-impossible, // non-tok value on the first token matching a pattern — mirroring how // testvalueflow.cpp tests arithmetic operator annotation. void operatorTokenValues() { // OT1: "3 + 4" — the "+" token must carry value 7. { const char code[] = "void f() { int x = 3 + 4; }"; TODO_ASSERT_EQUALS(7, 0, valueOfTok(code, "+").intvalue); } // OT2: "10 - 3" — the "-" token must carry value 7. { const char code[] = "void f() { int x = 10 - 3; }"; TODO_ASSERT_EQUALS(7, 0, valueOfTok(code, "-").intvalue); } // OT3: "3 * 4" — the "*" token must carry value 12. { const char code[] = "void f() { int x = 3 * 4; }"; TODO_ASSERT_EQUALS(12, 0, valueOfTok(code, "*").intvalue); } // OT4: "10 / 2" — the "/" token must carry value 5. { const char code[] = "void f() { int x = 10 / 2; }"; TODO_ASSERT_EQUALS(5, 0, valueOfTok(code, "/").intvalue); } // OT5: literal integer token must carry its own Known value. // tokenValues() on "42" must include a Known 42. // (This is already tested by literalAnnotation; here we confirm // the new tokenValues() primitive returns the right result.) { const char code[] = "void f() { return 42; }"; const auto vals = tokenValues(code, "42"); ASSERT(std::any_of(vals.begin(), vals.end(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.isKnown() && v.intvalue == 42; })); } // OT6: zero literal in divisor position must carry Known 0 via tokenValues(). // This mirrors the literalAnnotation test but exercises the new helper. { const char code[] = "int f(int x) { return x / 0; }"; const auto vals = tokenValues(code, "0"); ASSERT(std::any_of(vals.begin(), vals.end(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.isKnown() && v.intvalue == 0; })); } } // ----------------------------------------------------------------------- // Phase UI — Unsigned integer impossible-value constraints // ----------------------------------------------------------------------- // // An unsigned integer type (uint8_t, uint16_t, uint32_t, unsigned int) // can never hold a negative value. ValueFlow annotates such variables // with an Impossible(-1) value (representing "< 0 is impossible") so // that checkers can suppress spurious "x < 0 is always false" checks. // // Requirement: false negatives are preferable — if DataFlow cannot // determine the type is unsigned it must not emit an incorrect constraint. void unsignedImpossible() { // UI1: uint32_t variable has Impossible(-1) — it can never be negative. { const char code[] = "#include \n" // 1 "void f(uint32_t x) {\n" // 2 " (void)x;\n" // 3 ← x is Impossible(-1) "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXImpossible(code, 3, -1)); } // UI2: signed int does NOT get the Impossible(-1) constraint. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← no Impossible(-1) "}\n"; ASSERT(!testValueOfXImpossible(code, 2, -1)); } // UI3: unsigned int parameter also has Impossible(-1). { const char code[] = "void f(unsigned int x) {\n" // 1 " (void)x;\n" // 2 ← x is Impossible(-1) "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXImpossible(code, 2, -1)); } } // ----------------------------------------------------------------------- // Phase SW — Switch/case value propagation // ----------------------------------------------------------------------- // // Inside a switch case block, the switched variable is Known equal to the // case label value. This is the same constraint that ValueFlow's // valueFlowSwitchVariable applies. // // Requirement: the value is only Known inside the case block, not after // the switch statement completes (where multiple cases may have executed). void switchVariable() { // SW1: inside "case 5:" the switched variable x is Known 5. { const char code[] = "void f(int x) {\n" // 1 " switch (x) {\n" // 2 " case 5:\n" // 3 " (void)x;\n" // 4 ← x is Known (or Possible) 5 " break;\n" // 5 " }\n" // 6 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 4, 5)); } // SW2: after the switch, x must NOT be Known 5 (other cases exist). { const char code[] = "void f(int x) {\n" // 1 " switch (x) {\n" // 2 " case 5: break;\n" // 3 " case 10: break;\n" // 4 " }\n" // 5 " (void)x;\n" // 6 ← x is NOT Known 5 "}\n"; ASSERT(!testValueOfXKnown(code, 6, 5)); } // SW3: default case does not constrain x to a specific value. { const char code[] = "void f(int x) {\n" // 1 " switch (x) {\n" // 2 " default:\n" // 3 " (void)x;\n" // 4 ← x has no specific Known value " break;\n" // 5 " }\n" // 6 "}\n"; ASSERT(!testValueOfXKnown(code, 4, 0)); } } // ----------------------------------------------------------------------- // Phase LB — For-loop variable bounds inside the loop body // ----------------------------------------------------------------------- // // For a simple counted loop "for (int x = 0; x < N; x++)" the analysis // should recognize that inside the body x is in [0, N-1]. ValueFlow's // valueFlowForLoop emits both the minimum (0) and the maximum (N-1) as // Possible values on the loop variable token inside the body. // // Requirement: if N is a compile-time constant the bounds are exact; if N // is unknown the analysis must not emit a false Known value. void loopBounds() { // LB1: loop variable x ranges over [0, 9] — both bounds are Possible // inside the body when the limit is a known literal. { const char code[] = "void f() {\n" // 1 " for (int x = 0; x < 10; x++) {\n" // 2 " (void)x;\n" // 3 ← x possibly 0 and 9 " }\n" // 4 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 0)); TODO_ASSERT_EQUALS(true, false, testValueOfXPossible(code, 3, 9)); } // LB2: loop variable must NOT be Known 0 after the loop — it has // exited with value 10 (post-condition of the for loop). { const char code[] = "void f() {\n" // 1 " for (int x = 0; x < 10; x++) {}\n" // 2 " (void)x;\n" // 3 ← x is NOT Known 0 "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } // LB3: loop with unknown limit — x must not be given a Known bound. { const char code[] = "void f(int n) {\n" // 1 " for (int x = 0; x < n; x++) {\n" // 2 " (void)x;\n" // 3 ← x must NOT be Known 0 " }\n" // 4 "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } } // ----------------------------------------------------------------------- // Phase BW — Bitwise operation value annotation // ----------------------------------------------------------------------- // // DataFlow should fold constant bitwise expressions and annotate the // result token. Checkers use bitwise results for range narrowing // (e.g. "x & 0xFF" is in [0, 255]). // // Requirement: for constant operands the Known value of the expression // must be correct. For non-constant operands no Known value is emitted. void bitwiseOps() { // BW1: bitwise AND of two constants → Known result. { const char code[] = "void f() {\n" // 1 " int x = 0xFF & 0x0F;\n" // 2 " (void)x;\n" // 3 ← x is 0x0F (15) "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, 0x0F)); } // BW2: bitwise OR of two constants → Known result. { const char code[] = "void f() {\n" // 1 " int x = 0xF0 | 0x0F;\n" // 2 " (void)x;\n" // 3 ← x is 0xFF (255) "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, 0xFF)); } // BW3: left shift of constant → Known result. { const char code[] = "void f() {\n" // 1 " int x = 1 << 4;\n" // 2 " (void)x;\n" // 3 ← x is 16 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, 16)); } // BW4: right shift of constant → Known result. { const char code[] = "void f() {\n" // 1 " int x = 64 >> 2;\n" // 2 " (void)x;\n" // 3 ← x is 16 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, 16)); } // BW5: bitwise AND with non-constant operand — no Known value. { const char code[] = "void f(int n) {\n" // 1 " int x = n & 0xFF;\n" // 2 " (void)x;\n" // 3 ← no Known value "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } } // ----------------------------------------------------------------------- // Phase TT — Type truncation on assignment // ----------------------------------------------------------------------- // // Assigning a value to a narrower type truncates the stored value. // "unsigned char x = 0x123" stores 0x23 (low byte). // "signed char x = 0xfe" stores -2 (sign-extension of 0xfe in 8 bits). // // Requirement: the truncated Known value must be emitted, not the // pre-truncation value, to avoid false positives in range checks. void typeTruncation() { // TT1: unsigned char truncates to low 8 bits. { const char code[] = "void f() {\n" // 1 " unsigned char x = 0x123;\n" // 2 " (void)x;\n" // 3 ← x is 0x23 (35) "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, 0x23)); } // TT2: signed char with value 0xfe is -2 after sign extension. { const char code[] = "void f() {\n" // 1 " signed char x = 0xfe;\n" // 2 " (void)x;\n" // 3 ← x is -2 "}\n"; TODO_ASSERT_EQUALS(true, false, testValueOfXKnown(code, 3, -2)); } // TT3: int assigned from int literal that fits — no truncation, value // is exact. This must continue to work after any truncation logic. { const char code[] = "void f() {\n" // 1 " int x = 42;\n" // 2 " (void)x;\n" // 3 ← x is 42 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 3, 42)); } } // ----------------------------------------------------------------------- // Phase 6 — Complexity abort // ----------------------------------------------------------------------- void complexityAbort() { // Deeply nested if-statements: the analysis aborts and produces NO // values at deep nesting levels. It must not produce a false positive. // (requirement 4: abort rather than risk FP) { const char code[] = "void f(int a, int b, int c, int d, int e) {\n" // 1 " int x = 0;\n" // 2 " if (a) {\n" // 3 " if (b) {\n" // 4 " if (c) {\n" // 5 " if (d) {\n" // 6 " if (e) {\n" // 7 " x = 99;\n" // 8 " }\n" // 9 " }\n" // 10 " }\n" // 11 " }\n" // 12 " }\n" // 13 " (void)x;\n" // 14 "}\n"; // After aborting, x must not be Known 99 (that would be a FP // since x is only 99 on one deeply-nested path). ASSERT(!testValueOfXKnown(code, 14, 99)); } } // ----------------------------------------------------------------------- // Phase N — Null pointer tracking // ----------------------------------------------------------------------- // // Null pointers are represented as a ValueFlow::Value with intvalue==0, // identical to the main ValueFlow analysis. CheckNullPointer uses // Token::getValue(0) to detect null values, so no checker changes are // needed — only the annotation must be correct. void nullPointer() { // N1: simple assignment p = nullptr → p is Known null (0) at the use. { const char code[] = "void f() {\n" // 1 " int *x = nullptr;\n" // 2 " (void)*x;\n" // 3 ← x is Known 0 (null) "}\n"; ASSERT(testValueOfXKnown(code, 3, 0)); } // N2: condition "if (x == nullptr)" → x is Known null inside then-block. { const char code[] = "void f(int *x) {\n" // 1 " if (x == nullptr) {\n" // 2 " (void)*x;\n" // 3 ← x is Known 0 inside " }\n" // 4 "}\n"; ASSERT(testValueOfXKnown(code, 3, 0)); } // N3: condition "if (x != nullptr)" → x is definitely NOT null inside // then-block (Impossible 0). { const char code[] = "void f(int *x) {\n" // 1 " if (x != nullptr) {\n" // 2 " (void)*x;\n" // 3 ← x is Impossible 0 " }\n" // 4 "}\n"; ASSERT(testValueOfXImpossible(code, 3, 0)); } // N4: condition "if (!x)" → x IS null in then-block (Known 0). { const char code[] = "void f(int *x) {\n" // 1 " if (!x) {\n" // 2 " (void)*x;\n" // 3 ← x is Known 0 " }\n" // 4 "}\n"; ASSERT(testValueOfXKnown(code, 3, 0)); } // N5: condition "if (x)" → x is NOT null in then-block (Impossible 0). { const char code[] = "void f(int *x) {\n" // 1 " if (x) {\n" // 2 " (void)*x;\n" // 3 ← x is Impossible 0 " }\n" // 4 "}\n"; ASSERT(testValueOfXImpossible(code, 3, 0)); } // N6: backward propagation of null constraint from condition to earlier use. // if (x == nullptr) {} → before the condition, x possibly IS null. { const char code[] = "void f(int *x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 0 " if (x == nullptr) {}\n" // 3 "}\n"; ASSERT(testValueOfXPossible(code, 2, 0)); } // N7: assignment to a non-null value clears the null tracking. // After "x = &y", x should not have a Known null value. { const char code[] = "void f(int *x, int *y) {\n" // 1 " x = nullptr;\n" // 2 " x = y;\n" // 3 " (void)*x;\n" // 4 ← x is no longer null "}\n"; ASSERT(!testValueOfXKnown(code, 4, 0)); } // N8: forward Possible(0) from "if (x)" carries Value::condition // pointing at the condition token (line 2) so that // CheckNullPointer emits nullPointerRedundantCheck with a note. { const char code[] = "void f(int *x) {\n" // 1 " if (x) {}\n" // 2 ← condition token " (void)*x;\n" // 3 ← Possible(0) w/ cond "}\n"; ASSERT(getErrorPathForX(code, 3).find("2,") != std::string::npos); } // N9: backward Possible(0) from "if (x == nullptr)" carries // Value::condition pointing at the condition token (line 3). { const char code[] = "void f(int *x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 0 " if (x == nullptr) {}\n" // 3 ← condition token "}\n"; ASSERT(getErrorPathForX(code, 2).find("3,") != std::string::npos); } // N10: backward Possible(0) from "if (x)" direct pointer truth-test. // The check implies x might have been null before. { const char code[] = "void f(int *x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 0 " if (x) {}\n" // 3 "}\n"; ASSERT(testValueOfXPossible(code, 2, 0)); } // N11: backward Possible(0) from "if (x != nullptr)" pointer inequality. { const char code[] = "void f(int *x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 0 " if (x != nullptr) {}\n" // 3 "}\n"; ASSERT(testValueOfXPossible(code, 2, 0)); } // N12: backward Possible(0) from "if (x)" carries Value::condition // pointing at the condition token (line 3). { const char code[] = "void f(int *x) {\n" // 1 " (void)x;\n" // 2 ← backward: x possibly 0 " if (x) {}\n" // 3 ← condition token "}\n"; ASSERT(getErrorPathForX(code, 2).find("3,") != std::string::npos); } // N13 (ticket #3054): partially guarded pointer — (x || q) && (*x || *q). // The guard (x || q) only guarantees at least one pointer is non-null; // it does NOT exclusively protect x. If x is null and q is non-null, // (x || q) is true, so *x is evaluated and causes a null dereference. // x inside *x must carry Possible(0) with Value::condition pointing at // the && on line 2 (the insufficient guard). { const char code[] = "void f(const int* x, const int* q) {\n" // 1 " if ((x || q) &&\n" // 2 ← && here " (*x || *q)) {}\n" // 3 ← *x here "}\n"; // 4 ASSERT(testValueOfXPossible(code, 3, 0)); ASSERT(getErrorPathForX(code, 3).find("2,") != std::string::npos); } // N14: false positive suppression — x appears in the '||' only as part // of a complex expression (*x > 0), not as a direct pointer truth-test. // Phase UA-PG must NOT inject Possible(0) for x in the RHS of '&&'. // Reproduces a false positive from real-world code where a pointer was // used in a member-access chain on one side of '||'. { const char code[] = "void f(int *x, int *q, bool b) {\n" // 1 " if ((q || *x > 0) &&\n" // 2 ← x in || via *x (not direct truth-test) " *x == 1) {}\n" // 3 ← x must NOT be Possible(0) "}\n"; // 4 ASSERT(!testValueOfXPossible(code, 3, 0)); } // N15: try/catch — catch block terminates with return → no false positive. // After "try { p = new T; } catch (...) { p = nullptr; return; }", // the surviving path is the try-succeeded path where p is non-null. // The analysis must NOT carry the catch-block null value past the catch. { const char code[] = "void f() {\n" // 1 " int *x = nullptr;\n" // 2 " try { x = new int(1); }\n" // 3 " catch (...) { x = nullptr; return; }\n" // 4 " (void)*x;\n" // 5 ← x must NOT be Known null "}\n"; ASSERT(!testValueOfXKnown(code, 5, 0)); } } // ----------------------------------------------------------------------- // Phase U — Uninitialized variable tracking // ----------------------------------------------------------------------- // // Local variables declared without an initializer receive a UNINIT value // in the forward state. Reads before any assignment are annotated with // UNINIT so that CheckUninitVar can flag the uninitialized use. void uninitVariable() { // U1: uninitialized integer — x is UNINIT at the use site. { const char code[] = "void f() {\n" // 1 " int x;\n" // 2 ← declared without init " (void)x;\n" // 3 ← x is UNINIT here "}\n"; ASSERT(testValueOfXUninit(code, 3)); } // U2: initialized integer — x must NOT be marked UNINIT. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 ← has initializer " (void)x;\n" // 3 ← x is NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 3)); } // U3: assignment clears UNINIT — after "x = 5", x is no longer uninit. { const char code[] = "void f() {\n" // 1 " int x;\n" // 2 " x = 5;\n" // 3 ← assignment clears UNINIT " (void)x;\n" // 4 ← x is NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 4)); } // U4: uninitialized pointer — x (a pointer) is UNINIT at the use site. { const char code[] = "void f() {\n" // 1 " int *x;\n" // 2 ← pointer without init " (void)*x;\n" // 3 ← x is UNINIT here "}\n"; ASSERT(testValueOfXUninit(code, 3)); } // U5: function argument is NOT marked uninit — it was initialized by caller. { const char code[] = "void f(int x) {\n" // 1 " (void)x;\n" // 2 ← parameter, NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 2)); } // U6: variable used as a function argument before any assignment is UNINIT. // After the call, state is conservatively cleared (the callee might // have modified the variable through a pointer), so we only check the // argument read inside the call, not a subsequent use. { const char code[] = "void g(int);\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 " g(x);\n" // 4 ← x is UNINIT (arg read) "}\n"; ASSERT(testValueOfXUninit(code, 4)); } // U7: variable assigned only inside a conditional branch within a while // loop — possibly uninit after the loop because even when the loop // runs, the branch may never be taken. // Phase NW3 must re-inject UNINIT(Possible) for variables in // ctx.uninits that have only conditional (non-top-level) assignments // inside the loop body. { const char code[] = "void f(int c) {\n" // 1 " int x;\n" // 2 ← declared without init " while (c) {\n" // 3 " if (c > 5) {\n" // 4 ← conditional block " x = c;\n" // 5 ← only assignment, conditional " }\n" // 6 " c = 0;\n" // 7 " }\n" // 8 " (void)x;\n" // 9 ← x is possibly UNINIT here "}\n"; ASSERT(testValueOfXUninit(code, 9)); } // U8: stream read ("s >> x") clears UNINIT — x is not uninit after the read. { const char code[] = "struct Stream {};\n" // 1 "Stream& operator>>(Stream&, double&);\n" // 2 "void f() {\n" // 3 " double x;\n" // 4 ← no init " Stream s;\n" // 5 " s >> x;\n" // 6 ← stream read " (void)x;\n" // 7 ← NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // U9: else-if chain where only the assigning branch falls through. // The else-if and else branches both return, so code after the // if-else-if-else can only be reached when the first branch ran // (where x was assigned). x must NOT be marked uninit after the // chain. // // After tokenizer simplification "else if" becomes "else { if ... }", // so blockTerminates must detect that an if-else where both branches // terminate causes the enclosing else block to terminate too. { const char code[] = "void f(int c) {\n" // 1 " int x;\n" // 2 ← declared without init " if (c == 1) {\n" // 3 " x = 1;\n" // 4 ← assigned here " } else if (c == 2) {\n" // 5 " return;\n" // 6 ← returns, x not assigned " } else {\n" // 7 " return;\n" // 8 ← returns, x not assigned " }\n" // 9 " (void)x;\n" // 10 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 10)); } // U10: uninit pointer used inside an if-condition — Phase UA must // annotate condition tokens so checkers can detect the uninit use. // Reproduces the false negative in test1.cpp: bufToken is declared // without an initializer and then dereferenced via x->method() inside // an if-condition. The main token-walker skips condition tokens, so // Phase UA must annotate them explicitly. { const char code[] = "struct S { int* f(); };\n" // 1 "void g();\n" // 2 "void foo() {\n" // 3 " S *x;\n" // 4 ← uninit pointer " if (g()) {\n" // 5 ← empty branch; x not assigned " } else {\n" // 6 " return;\n" // 7 " }\n" // 8 " if (!x->f())\n" // 9 ← x must be UNINIT here " return;\n" // 10 "}\n"; ASSERT(testValueOfXUninit(code, 9)); } // U11: assignment inside an if-condition clears UNINIT — the assignment // "x = expr" in "if ((x = expr) != 0)" always executes before // branching, so x is initialized on both branches. // Reproduces the false positive: "Uninitialized variable: Result" // reported for code like "if ((Result = a - b) != 0) return Result;" { const char code[] = "int getValue();\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 ← declared without init " if ((x = getValue()) != 0)\n" // 4 ← x assigned in condition " (void)x;\n" // 5 ← x must NOT be UNINIT here "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U12 (FP): pointer initialized in for-loop init clause must NOT be // reported as UNINIT after the loop. // "for (x = p; *x != '\\0'; x++) {}" assigns x unconditionally // in the init clause, so x is always initialized when the loop // is reached. { const char code[] = "void f(char *p) {\n" // 1 " char *x;\n" // 2 ← declared without init " for (x = p; *x != '\\0'; x++) {\n" // 3 ← x assigned in init " }\n" // 4 " *x = '\\0';\n" // 5 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U13 (FP): variable assigned at top level of "for (;;)" body must NOT // be reported as UNINIT after the loop, even after a function // call (which triggers Phase U2 re-injection from ctx.uninits). // "for (;;)" is an infinite loop — the body always executes // at least once before any break, so x is guaranteed // initialized when the loop exits. { const char code[] = "char *getcwd(char *, int);\n" // 1 "void f(int n) {\n" // 2 " char *x;\n" // 3 ← declared without init " for (;;) {\n" // 4 " x = (char *)alloca(n);\n" // 5 ← top-level assignment " if (getcwd(x, n))\n" // 6 ← break condition " break;\n" // 7 " n++;\n" // 8 " }\n" // 9 " getcwd(x, n);\n" // 10 ← function call triggers U2 " (void)x;\n" // 11 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 11)); } // U14 (FP): variable assigned at top level of a regular for-loop body // must NOT be reported as UNINIT after the loop. // Loops are assumed to execute at least once (false negatives // are preferred over false positives per project policy). { const char code[] = "char *getcwd(char *, int);\n" // 1 "void f(int n) {\n" // 2 " char *x;\n" // 3 ← declared without init " for (int c = 0; c < n; ++c) {\n" // 4 " x = (char *)alloca(n);\n" // 5 ← top-level assignment " if (getcwd(x, n))\n" // 6 " break;\n" // 7 " }\n" // 8 " getcwd(x, n);\n" // 9 ← function call triggers U2 " (void)x;\n" // 10 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 10)); } // U15 (FP): integer variable assigned in the for-loop init clause must // NOT be reported as UNINIT after a function call following // the loop. The init clause always executes unconditionally, // so x is definitely initialized when the loop is reached. // A subsequent function call must not re-inject UNINIT for x // via Phase U2 (ctx.uninits must be cleared for init-clause vars). { const char code[] = "int func();\n" // 1 "void f(int n) {\n" // 2 " int x;\n" // 3 ← declared without init " for (x = 0; x < n; ++x) {\n" // 4 ← x assigned in init clause " }\n" // 5 " func();\n" // 6 ← triggers U2 re-injection " (void)x;\n" // 7 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // U16 (FP): flag-guarded use of a variable that is only assigned when // the flag is set. x is uninitialized on the path where // flag stays 0, but x is never read on that path because the // use is inside "if (flag)". Must NOT be reported as uninit. // // The analysis cannot track the correlation between x and flag, // so it suppresses Possible(UNINIT) for conditional reads // (branchDepth > 0) as a conservative false-positive guard // (Requirement 4: false negatives preferred). { const char code[] = "void f(int n) {\n" // 1 " int x, flag=0;\n" // 2 ← x declared without init " if (n>0) {\n" // 3 " x = 0;\n" // 4 ← x assigned when flag=1 " flag = 1;\n" // 5 " } else {\n" // 6 " if (n == -100) {\n" // 7 " x = 1;\n" // 8 ← x assigned when flag=1 " flag = 1;\n" // 9 " } else {\n" // 10 ← x NOT assigned, flag stays 0 " }\n" // 11 " }\n" // 12 " if (flag) {\n" // 13 ← guard: only reached when x was assigned " x++;\n" // 14 ← x must NOT be UNINIT here " }\n" // 15 "}\n"; // 16 ASSERT(!testValueOfXUninit(code, 14)); } // U17 (FP): variable assigned in a while-loop condition must NOT be // reported as UNINIT after the loop. The condition always // executes at least once, so the variable is definitely // initialized when the loop exits. // Reproduces: "while(0>(x=write(...))&&errno==EINTR)" reports // x as uninit after the loop. { const char code[] = "int getValue();\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 ← declared without init " while (0 > (x = getValue())) ;\n" // 4 ← x assigned in condition " (void)x;\n" // 5 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U18 (FP): variable initialized via &var argument to a function call // inside a while condition must NOT be reported as UNINIT. // The while condition executes at least once, so the callee // definitely writes the variable before it is used after the loop. // Reproduces: "double start; while(sscanf(...,&start,...)!=2){};" // falsely reports start as UNINIT after the loop. { const char code[] = "int scan(const char *s, double *out);\n" // 1 "void f() {\n" // 2 " double x;\n" // 3 ← declared without init " while (scan(\"s\", &x) != 1) {}\n" // 4 ← &x initializes x " (void)x;\n" // 5 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U19 (FP): variable initialized via &var argument to a function call // inside a do-while condition must NOT be reported as UNINIT. // The do-while condition executes at least once, so the callee // definitely writes the variable before it is used after the loop. // Reproduces: "double start; do {} while(sscanf(...,&start,...)!=2);" // falsely reports start as UNINIT at "current->start = start * 100". { const char code[] = "int scan(const char *s, double *out);\n" // 1 "void f() {\n" // 2 " double x;\n" // 3 ← declared without init " do {\n" // 4 " } while (scan(\"s\", &x) != 1);\n" // 5 ← &x initializes x " (void)x;\n" // 6 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // U20 (FP): "sentinel variable" while-loop pattern. // The while condition is "!done" where done has no top-level assignment // in the loop body — the loop can only exit via a conditional branch // (the else-block) that also assigns x. x is therefore always // initialized when the loop exits. // Phase NW3 must NOT re-inject UNINIT for x, and Phase U2 must NOT // re-inject it after the subsequent function call. // Reproduces: false positive "Uninitialized variable: x" reported on // the return statement in a typical GTK-style dialog loop pattern. { const char code[] = "void a(int);\n" // 1 "void f() {\n" // 2 " int done, x;\n" // 3 ← both declared without init " done = 0;\n" // 4 ← done initialized before loop " while (!done) {\n" // 5 " if (a(1) == 2) {\n" // 6 " a(1);\n" // 7 ← no assignment to done or x " } else {\n" // 8 " x = 0;\n" // 9 ← x assigned " done = 1;\n" // 10 ← done assigned (exit trigger) " }\n" // 11 " }\n" // 12 " a(1);\n" // 13 ← function call (triggers U2) " (void)x;\n" // 14 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 14)); } // U21 (FP): same "sentinel variable" pattern as U20 but using // for (; !done; ) — semantically equivalent to while (!done). { const char code[] = "void a(int);\n" // 1 "void f() {\n" // 2 " int done, x;\n" // 3 " done = 0;\n" // 4 " for (; !done; ) {\n" // 5 " if (a(1) == 2) {\n" // 6 " a(1);\n" // 7 " } else {\n" // 8 " x = 0;\n" // 9 " done = 1;\n" // 10 " }\n" // 11 " }\n" // 12 " a(1);\n" // 13 ← triggers U2 " (void)x;\n" // 14 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 14)); } // U22 (FP): same "sentinel variable" pattern as U20 but with do-while. // do { ... } while (!done) — the body always runs at least once, and // the only way the loop exits is via the else-branch that also assigns x. // Phase U2 must NOT re-inject UNINIT for x after the subsequent call. { const char code[] = "void a(int);\n" // 1 "void f() {\n" // 2 " int done, x;\n" // 3 " done = 0;\n" // 4 " do {\n" // 5 " if (a(1) == 2) {\n" // 6 " a(1);\n" // 7 " } else {\n" // 8 " x = 0;\n" // 9 " done = 1;\n" // 10 " }\n" // 11 " } while (!done);\n" // 12 " a(1);\n" // 13 ← triggers U2 " (void)x;\n" // 14 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 14)); } // U23 (FP): variable initialized either via &var to a function call // inside a for-loop body, or by a flag-guarded fallback // assignment after the loop. // dropWrittenVars must detect that &x is passed to the callee // (a potential out-parameter) and remove x from ctx.uninits, // so that Phase U2 does not re-inject UNINIT after the loop. { const char code[] = "int read_int(const char *, int *);\n" // 1 "void f(char **argv) {\n" // 2 " int flag = 0;\n" // 3 " int x;\n" // 4 ← declared without init " for (++argv; *argv; argv++) {\n" // 5 " if (!flag && !read_int(*argv, &x))\n" // 6 ← &x out-param " flag = 1;\n" // 7 " else\n" // 8 " return;\n" // 9 " }\n" // 10 " if (!flag)\n" // 11 " x = 1;\n" // 12 ← fallback init " (void)x;\n" // 13 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 13)); } // U24 (FP): variable assigned in a while-loop condition must NOT be // reported as UNINIT inside the loop body, even when a // different (unrelated) while loop appears before it. // // The bug: the do-while post-processing check fired on the first // while loop because its empty body "}" was followed by the second // "while" keyword. The code misidentified the second while's // condition as the do-while tail, causing its body to be visited // before Phase U-WC had cleared x from ctx.state/ctx.uninits. // // Reproduces: false positive "Uninitialized variable: wait_pid" // on "return (wait_pid != finger_pid)" inside a while body when a // separate "while (...) {}" loop precedes it. { const char code[] = "int getC(void);\n" // 1 "int getValue(int *);\n" // 2 "void f() {\n" // 3 " int x;\n" // 4 ← declared without init " while (getC() != 0) {}\n" // 5 ← unrelated while loop " while ((x = getValue(0)) != -1)\n" // 6 ← x assigned in condition " (void)x;\n" // 7 ← x is NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // U25 (FP): variable assigned in the for-loop increment clause must NOT // be reported as UNINIT after the loop. // // Pattern: for (cur = p; cur; x = cur, cur = cur->next) {} // (void)x; ← must NOT be uninit // // The bug: the increment clause "x = cur, cur = cur->next" was never // scanned by dropWrittenVars or the NW4 uninit-erase loop. So x // kept its UNINIT state from the declaration and was falsely flagged. { struct Node { struct Node* next; }; (void)sizeof(struct Node); // suppress unused-variable warning const char code[] = "struct Node { struct Node *next; };\n" // 1 "void f(struct Node *p) {\n" // 2 " struct Node *x;\n" // 3 ← declared without init " struct Node *cur;\n" // 4 " for (cur = p; cur; x = cur, cur = cur->next) {}\n" // 5 " (void)x;\n" // 6 ← x is NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // U27 (ticket #7614): uninitialized variable read in a for-loop condition. // Pattern: "for (; x != 0; x = x + 5) {}" // x is declared without an initializer and read in the condition // before any assignment. Phase UA-FC must annotate the condition // tokens with the UNINIT state so CheckUninitVar can detect the use. // // The bug: the for-loop increment clause "x = x + 5" caused x to be // erased from ctx.state and ctx.uninits (via dropWrittenVars / // hasTopLevelAssignment) BEFORE the condition tokens were annotated. // Phase UA-FC fixes this by annotating condition reads first. { const char code[] = "void f() {\n" // 1 " int x;\n" // 2 ← declared without init " for (; x != 0; x = x + 5) {\n" // 3 ← x is UNINIT in condition " }\n" // 4 "}\n"; ASSERT(testValueOfXUninit(code, 3)); } // U26 (FP): variable assigned inside a while(1) loop in a conditional // branch, with the only non-returning exit being a break // that follows the assignment. // // Pattern: while (1) { // if (cond) { // x = func(); ← x assigned here // if (x) break; ← break only after assignment // } // if (other) return; // } // (void)x; ← x is NOT uninit: break only reachable after x=func() // // The bug: Phase NW3 re-injects UNINIT(Possible) for any variable in // ctx.uninits whose only loop-body assignment is conditional (not // top-level). For while(1) the condition never becomes false, so the // loop only exits via break/return/throw. On the break path x was // always just assigned, so NW3 should not apply. { const char code[] = "int f(void);\n" // 1 "void g() {\n" // 2 " int x;\n" // 3 ← declared without init " while (1) {\n" // 4 " if (f()) {\n" // 5 " x = f();\n" // 6 ← x assigned here " if (x) break;\n" // 7 ← break only after assignment " }\n" // 8 " if (f()) return;\n" // 9 ← early exit " }\n" // 10 " (void)x;\n" // 11 ← x is NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 11)); } // U28 (FP): variable assigned inside a parenthesised group that is an // argument to a function call, then read later in the same // argument list (e.g. ternary false-branch). The annotation // loop skips '(...)' groups, so without a pre-pass the // assignment is missed and the ternary-branch read is falsely // annotated as UNINIT. // // Pattern (pointer): // use(buf, (x = get()) == 0 ? ":" : x) // — x is assigned in (x = get()), then read in the false-branch. // Pattern (int): // use((x = get()) > 0 ? x : 0) // — x is assigned in (x = get()), then read in the true-branch. // Pattern (float): // use((x = getf()) > 0.0f ? x : 0.0f) // — same as int but for float. // // None of these should produce a false UNINIT annotation. { // Pointer case (reproduces the original test1.c report) const char code[] = "char *get(void);\n" // 1 "void use(char *, const char *);\n" // 2 "void f() {\n" // 3 " const char *x;\n" // 4 ← declared without init " char buf[100];\n" // 5 " use(buf, (x = get()) == 0 ? \":\" : x);\n" // 6 ← x NOT uninit here "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // U29 (FP): uninitialized variable read in dead code after an if/else // where both branches return. The read is unreachable, so // the analysis must NOT annotate the variable as UNINIT. // // Pattern (reproduces test1.c false positive): // void *x; // if (cond) { return; } else { return; } // if (x == 0) {} ← dead code — x must NOT be UNINIT here // // Root cause: after an if/else where both branches terminate, the // ctx is left unchanged (still contains the pre-if UNINIT state). // The walker continues into the unreachable code and annotates x. // Fix: clear ctx.state and ctx.uninits when both branches terminate. { const char code[] = "int cond();\n" // 1 "void f() {\n" // 2 " int *x;\n" // 3 ← declared without init " if (cond()) {\n" // 4 " return;\n" // 5 " } else {\n" // 6 " return;\n" // 7 " }\n" // 8 " if (x == 0) {}\n" // 9 ← dead code, x must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 9)); } } // ----------------------------------------------------------------------- // Additional helpers for new phases // ----------------------------------------------------------------------- /// Requirement: x at linenr has a Known float value approximately equal /// to `value` (within 1e-9). Phase F float/double tracking. #define testValueOfXFloat(...) testValueOfXFloat_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfXFloat_(const char* file, int line, const char code[], unsigned int linenr, double value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isFloatValue() && v.isKnown() && std::abs(v.floatValue - value) < 1e-9; })) return true; } return false; } /// Requirement: x at linenr has a Known CONTAINER_SIZE value equal to /// `size`, using settings `s` which must have std.cfg loaded. /// Phase C: container variables are annotated with CONTAINER_SIZE values. #define testContainerSizeKnown(...) testContainerSizeKnown_(__FILE__, __LINE__, __VA_ARGS__) bool testContainerSizeKnown_(const char* file, int line, const char code[], unsigned int linenr, int size, const Settings& s) { SimpleTokenizer tokenizer(s, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [size](const ValueFlow::Value& v) { return v.isContainerSizeValue() && v.isKnown() && v.intvalue == size; })) return true; } return false; } // ----------------------------------------------------------------------- // Phase F — Float/double value tracking // ----------------------------------------------------------------------- // // Float/double/long double scalar variables are tracked in the same // DFState as integer variables, using FLOAT values with floatValue set. // evalConstFloat() folds constant float expressions; annotateTok() writes // FLOAT values onto tokens just like it does for INT values. void floatPropagation() { // F1: simple assignment from a float literal. { const char code[] = "void f() {\n" // 1 " double x = 3.14;\n" // 2 " (void)x;\n" // 3 ← x is 3.14 (Known FLOAT) "}\n"; ASSERT(testValueOfXFloat(code, 3, 3.14)); } // F2: float variable without initializer is UNINIT. { const char code[] = "void f() {\n" // 1 " double x;\n" // 2 ← no initializer " (void)x;\n" // 3 ← x is UNINIT "}\n"; ASSERT(testValueOfXUninit(code, 3)); } // F3: float assignment from another float literal (re-assignment). { const char code[] = "void f() {\n" // 1 " double x = 1.0;\n" // 2 " x = 2.5;\n" // 3 " (void)x;\n" // 4 ← x is 2.5 "}\n"; ASSERT(testValueOfXFloat(code, 4, 2.5)); ASSERT(!testValueOfXFloat(code, 4, 1.0)); } // F4: float arithmetic — constant folding. { const char code[] = "void f() {\n" // 1 " double x = 1.5 + 2.5;\n" // 2 " (void)x;\n" // 3 ← x is 4.0 "}\n"; ASSERT(testValueOfXFloat(code, 3, 4.0)); } // F5: float += compound assignment. { const char code[] = "void f() {\n" // 1 " double x = 3.0;\n" // 2 " x += 1.5;\n" // 3 " (void)x;\n" // 4 ← x is 4.5 "}\n"; ASSERT(testValueOfXFloat(code, 4, 4.5)); } // F6: float copy propagation. { const char code[] = "void f() {\n" // 1 " double x = 2.0;\n" // 2 " double y = x;\n" // 3 ← x used here: 2.0 "}\n"; ASSERT(testValueOfXFloat(code, 3, 2.0)); } // F7: float assigned from unknown — no known value. { const char code[] = "void f(double d) {\n" // 1 " double x = d;\n" // 2 " (void)x;\n" // 3 ← no known float value "}\n"; ASSERT(!testValueOfXFloat(code, 3, 0.0)); } } // ----------------------------------------------------------------------- // Phase S — String literal non-null tracking // ----------------------------------------------------------------------- // // Assigning a string literal to a pointer variable stores an Impossible(0) // value on the pointer, indicating the pointer is definitely NOT null. // This allows CheckNullPointer to suppress spurious warnings about string // literal pointers being dereferenced. void stringLiteralNonNull() { // S1: const char *x = "hello" → x is Impossible null (NOT null). { const char code[] = "void f() {\n" // 1 " const char *x = \"hello\";\n" // 2 " (void)*x;\n" // 3 ← x is Impossible 0 "}\n"; ASSERT(testValueOfXImpossible(code, 3, 0)); } // S2: nullptr assignment is still Known null (existing Phase N). { const char code[] = "void f() {\n" // 1 " const char *x = 0;\n" // 2 " (void)*x;\n" // 3 ← x IS null (Known 0) "}\n"; ASSERT(testValueOfXKnown(code, 3, 0)); } // S3: re-assignment from string literal clears the null value. { const char code[] = "void f() {\n" // 1 " const char *x = nullptr;\n" // 2 ← Known null " x = \"world\";\n" // 3 ← reassign to literal " (void)*x;\n" // 4 ← x is Impossible 0 "}\n"; ASSERT(testValueOfXImpossible(code, 4, 0)); ASSERT(!testValueOfXKnown(code, 4, 0)); } } // ----------------------------------------------------------------------- // Phase U2 — Enhanced UNINIT tracking (survives function calls) // ----------------------------------------------------------------------- // // Variables declared without an initializer are added to a persistent // DFUninitSet. After any function call clears the main state, UNINIT // (Possible) is re-injected for every variable still in the set. // This ensures that "still uninit" variables remain detectable even // after conservative state clearing. void uninitAfterCall() { // U2.1: variable remains UNINIT(Possible) at a use after a function call. { const char code[] = "void g(int);\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 " g(x);\n" // 4 " (void)x;\n" // 5 ← still UNINIT (Possible) "}\n"; ASSERT(testValueOfXUninit(code, 5)); } // U2.2: once assigned, the variable is removed from the uninit set // and must NOT be re-injected after a subsequent call. { const char code[] = "void g();\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 " x = 5;\n" // 4 ← assigned: removed from set " g();\n" // 5 " (void)x;\n" // 6 ← x is NOT UNINIT here "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // U2.3: variable is UNINIT before the call (existing Phase U) AND // still Possible-UNINIT after the call (Phase U2). { const char code[] = "void g(int);\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 " g(x);\n" // 4 ← UNINIT at arg read " (void)x;\n" // 5 ← UNINIT still possible "}\n"; ASSERT(testValueOfXUninit(code, 4)); ASSERT(testValueOfXUninit(code, 5)); } // U2.4: variable assigned via std::tie must NOT be UNINIT after the assignment. // std::tie takes its arguments by non-const lvalue reference — the callee // writes back through those references, so the variables are initialized. { const char code[] = "std::pair evalCond();\n" // 1 "void f() {\n" // 2 " bool x, y;\n" // 3 " std::tie(x, y) = evalCond();\n" // 4 " (void)x;\n" // 5 ← NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U2.5: variable passed to a function whose declaration is not visible // must NOT be flagged as UNINIT after the call. The callee may // initialize it through a non-const reference parameter. // Requirement 4: false negatives are preferable to false positives. { const char code[] = "struct S;\n" // 1 "void f(S& dp) {\n" // 2 " int x;\n" // 3 " dp.colour(x);\n" // 4 ← colour not declared " (void)x;\n" // 5 ← NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // U2.6: variable passed to a template function call must NOT be flagged // as UNINIT after the call. The template call "Tmpl(x, y)" // has '>' as the token before '('; isFunctionCallOpen must // recognise template calls so the state is cleared/re-injected // correctly (Phase U2). { const char code[] = "template void calc(double, double&);\n" // 1 "void f() {\n" // 2 " double x;\n" // 3 " double v = 0;\n" // 4 " calc(v, x);\n" // 5 ← template call initializes x " (void)x;\n" // 6 ← NOT uninit "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // U24: a function call must not change the UNINIT status of a pointer // variable — the behavior must be the same with or without the call. // clearCallClobberableState preserves UNINIT-only entries so the // variable remains uninitialized after the call, just as it was before. // Fixes: FP "Uninitialized variable" reported after a function call // (e.g. exit()) in dead code, where re-injection was the root cause. { // Without call: x is UNINIT at line 3. const char code_no_call[] = "void f() {\n" // 1 " int *x;\n" // 2 " (void)x;\n" // 3 ← UNINIT "}\n"; // With call: x should be UNINIT at line 4 — same as without call. const char code_with_call[] = "void a();\n" // 1 "void f() {\n" // 2 " int *x;\n" // 3 " a();\n" // 4 ← call: must not change uninit status " (void)x;\n" // 5 ← same UNINIT as without call "}\n"; ASSERT(testValueOfXUninit(code_no_call, 3)); ASSERT(testValueOfXUninit(code_with_call, 5)); } } // ----------------------------------------------------------------------- // Phase M — Struct/class member field tracking // ----------------------------------------------------------------------- // // "obj.field = value" assignments are stored in DFMemberState keyed by // (objectVarId, fieldVarId). "obj.field" reads are annotated from the // member state. The member state is forked at branches and merged at // join points using the same rules as the main state. void memberFieldPropagation() { // M1: simple member assignment and read. { const char code[] = "struct S { int x; };\n" // 1 "void f() {\n" // 2 " S obj;\n" // 3 " obj.x = 5;\n" // 4 " (void)obj.x;\n" // 5 ← x is 5 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 5, 5)); } // M2: member re-assignment — second value replaces first. { const char code[] = "struct S { int x; };\n" // 1 "void f() {\n" // 2 " S obj;\n" // 3 " obj.x = 5;\n" // 4 " obj.x = 10;\n" // 5 " (void)obj.x;\n" // 6 ← x is 10 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 6, 10)); ASSERT(!testValueOfXKnown(code, 6, 5)); } // M3: member state is cleared after a function call (conservative). { const char code[] = "struct S { int x; };\n" // 1 "void g();\n" // 2 "void f() {\n" // 3 " S obj;\n" // 4 " obj.x = 5;\n" // 5 " g();\n" // 6 ← call clears member state " (void)obj.x;\n" // 7 ← x has NO known value "}\n"; ASSERT(!testValueOfXKnown(code, 7, 5)); } // M4: member assignment from expression. { const char code[] = "struct S { int x; };\n" // 1 "void f() {\n" // 2 " S obj;\n" // 3 " obj.x = 3 + 4;\n" // 4 " (void)obj.x;\n" // 5 ← x is 7 (Known) "}\n"; ASSERT(testValueOfXKnown(code, 5, 7)); } } // ----------------------------------------------------------------------- // Phase M2 — Member-access expression ('.') carries the field value // ----------------------------------------------------------------------- // // Requirement: when "obj.field" is read as the divisor in "x / obj.field", // the '.' token (which is astOperand2 of the '/' operator) must carry the // known value of the field so that CheckOther::checkZeroDivision can find // it via tok->astOperand2()->getValue(0LL). // // annotateMemberTok now propagates the field value onto both the field // token ('x') AND the parent '.' token so that all existing checkers // that inspect the expression token work without modification. void memberFieldDivisionByZero() { // M2.1: '.' token carries Known(0) when s.x = 0 was assigned. // This is the value that checkZeroDivision inspects. // // In "s.x = 0; int a = 100 / s.x;", the token stream has two // '.' tokens. The read-site '.' in "100 / s.x" is followed by // "x ;" in the stream, while the write-site '.' in "s.x = 0" is // followed by "x =". Pattern ". x ;" selects the read-site '.'. { const char code[] = "struct S { int x; int y; };\n" // 1 "void f() {\n" // 2 " struct S s;\n" // 3 " s.x = 0;\n" // 4 " int a = 100 / s.x;\n" // 5 ← '.' must carry 0 "}\n"; // ". x ;" finds the '.' in the read expression on line 5. const auto vals = tokenValues(code, ". x ;"); ASSERT_EQUALS(1U, vals.size()); ASSERT_EQUALS(0LL, vals.front().intvalue); ASSERT(vals.front().isKnown()); } // M2.2: non-zero member value does NOT produce a false zerodiv warning. { const char code[] = "struct S { int x; };\n" // 1 "void f() {\n" // 2 " struct S s;\n" // 3 " s.x = 5;\n" // 4 " int a = 100 / s.x;\n" // 5 ← '.' must carry 5 "}\n"; const auto vals = tokenValues(code, ". x ;"); ASSERT_EQUALS(1U, vals.size()); ASSERT_EQUALS(5LL, vals.front().intvalue); ASSERT(vals.front().isKnown()); } } // ----------------------------------------------------------------------- // Phase MN — Struct member pointer null condition propagation // ----------------------------------------------------------------------- // // Requirement: when a condition "if (!s.x)" or "if (s.x == nullptr)" is // processed, the member pointer field s.x must be constrained in the // then/else branches. After an empty if (!s.x) {} body, the merge must // produce a Possible(0) value on s.x at the dereference site, enabling // CheckNullPointer to detect the potential null dereference. // // False negatives are preferred over false positives: only simple, // recognisable condition forms are handled. void memberPtrCondition() { // MN1: "if (!s.x) {}" — empty then-block, no else. // Then-path: s.x = Known(0). Else-path: s.x = Impossible(0). // After merge both paths reach dereference → Possible(0). { const char code[] = "struct S { int *x; int y; };\n" // 1 "void foo(struct S s) {\n" // 2 " if (!s.x) {}\n" // 3 " *s.x;\n" // 4 ← x is Possible 0 "}\n"; ASSERT(testValueOfXPossible(code, 4, 0)); } // MN2: "if (!s.x) { return; }" — then-block terminates. // Only the else-path (s.x != null → Impossible 0) reaches the // dereference, so x must carry Impossible(0) after the if. { const char code[] = "struct S { int *x; };\n" // 1 "void foo(struct S s) {\n" // 2 " if (!s.x) { return; }\n" // 3 " *s.x;\n" // 4 ← x is Impossible 0 "}\n"; ASSERT(testValueOfXImpossible(code, 4, 0)); } // MN3: "if (s.x == nullptr) {}" — equality form, same effect as MN1. { const char code[] = "struct S { int *x; };\n" // 1 "void foo(struct S s) {\n" // 2 " if (s.x == nullptr) {}\n" // 3 " *s.x;\n" // 4 ← x is Possible 0 "}\n"; ASSERT(testValueOfXPossible(code, 4, 0)); } // MN4: "if (s.x)" — direct truth-value, non-null on then-path. // Then-path: s.x = Impossible(0). Else-path: s.x = Known(0). // After merge: Possible(0) at the dereference. { const char code[] = "struct S { int *x; };\n" // 1 "void foo(struct S s) {\n" // 2 " if (s.x) {}\n" // 3 " *s.x;\n" // 4 ← x is Possible 0 "}\n"; ASSERT(testValueOfXPossible(code, 4, 0)); } // MN5: false-positive guard — no condition means no null value injected. { const char code[] = "struct S { int *x; };\n" // 1 "void foo(struct S s) {\n" // 2 " *s.x;\n" // 3 ← no null value "}\n"; ASSERT(!testValueOfXPossible(code, 3, 0)); ASSERT(!testValueOfXKnown(code, 3, 0)); } } // ----------------------------------------------------------------------- // Phase C — Container size tracking // ----------------------------------------------------------------------- // // Container variables (std::vector, etc.) are tracked in DFContState // using CONTAINER_SIZE values. push_back/emplace_back increment the // known size; pop_back decrements it; clear resets to 0. The container // variable token is annotated with the current size whenever it is read. // // Note: container type detection requires the standard library cfg to be // loaded. Tests use a local settings with "std.cfg". void containerSize() { const Settings stdSettings = settingsBuilder().checkLevel(Settings::CheckLevel::normal) .library("std.cfg") .build(); // C1: default-constructed vector has Known size 0. { const char code[] = "void f() {\n" // 1 " std::vector x;\n" // 2 " (void)x;\n" // 3 ← size 0 "}\n"; ASSERT(testContainerSizeKnown(code, 3, 0, stdSettings)); } // C2: one push_back increments size to 1. { const char code[] = "void f() {\n" // 1 " std::vector x;\n" // 2 " x.push_back(1);\n" // 3 " (void)x;\n" // 4 ← size 1 "}\n"; ASSERT(testContainerSizeKnown(code, 4, 1, stdSettings)); } // C3: push_back then pop_back returns to size 0. { const char code[] = "void f() {\n" // 1 " std::vector x;\n" // 2 " x.push_back(1);\n" // 3 " x.pop_back();\n" // 4 " (void)x;\n" // 5 ← size 0 "}\n"; ASSERT(testContainerSizeKnown(code, 5, 0, stdSettings)); } // C4: clear() resets size to 0 regardless of previous size. { const char code[] = "void f() {\n" // 1 " std::vector x;\n" // 2 " x.push_back(1);\n" // 3 " x.push_back(2);\n" // 4 " x.push_back(3);\n" // 5 " x.clear();\n" // 6 " (void)x;\n" // 7 ← size 0 "}\n"; ASSERT(testContainerSizeKnown(code, 7, 0, stdSettings)); } } // ----------------------------------------------------------------------- // Phase Cast — Cast expression value propagation // ----------------------------------------------------------------------- void castValuePropagation() { // Cast1: C-style integer cast of a literal — value propagates through. // Requirement: "(int)(5)" evaluates to 5; the assigned variable gets // the known value 5. { const char code[] = "void f() {\n" // 1 " int x = (int)(5);\n" // 2 " (void)x;\n" // 3 ← x must be Known 5 "}\n"; ASSERT(testValueOfXKnown(code, 3, 5)); } // Cast2: C-style pointer cast of a null constant — the null value // propagates through the cast so that ptr is Known null (0). { const char code[] = "void f() {\n" // 1 " void* x = (void*)0;\n" // 2 " (void)x;\n" // 3 ← x must be Known 0 "}\n"; ASSERT(testValueOfXKnown(code, 3, 0)); } // Cast3: C-style pointer cast of a non-zero constant — the pointer is // NOT null so the state must be cleared (no Known 0 value). { const char code[] = "void f() {\n" // 1 " void* x = (void*)(-1);\n" // 2 " (void)x;\n" // 3 ← x must NOT be null "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } // Cast4: cast of an unknown (non-constant) expression — no value. { const char code[] = "void f(int n) {\n" // 1 " int x = (int)(n);\n" // 2 " (void)x;\n" // 3 ← no known value "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); } // Cast5: the cast token itself must carry the evaluated value. // Requirement: DataFlow annotates the '(' cast token so that checkers // examining expression values see the same result as ValueFlow. { const char code[] = "void f() {\n" // 1 " int x = (int)(5);\n" // 2 ← cast token on line 2 " (void)x;\n" // 3 "}\n"; ASSERT(testCastKnown(code, 2, 5)); } } // ----------------------------------------------------------------------- // Literal constant annotation // ----------------------------------------------------------------------- /// Requirement: the token with string `tokstr` at line `linenr` has a /// Known integer value equal to `value`. DataFlow must annotate integer /// literals so that checkers (checkZeroDivision, arrayIndex) can find /// them via getValue() at normal check level. #define testValueOfTokenKnown(...) testValueOfTokenKnown_(__FILE__, __LINE__, __VA_ARGS__) bool testValueOfTokenKnown_(const char* file, int line, const char code[], unsigned int linenr, const char* tokstr, MathLib::bigint value) { SimpleTokenizer tokenizer(settings, *this); ASSERT_LOC(tokenizer.tokenize(code), file, line); for (const Token* tok = tokenizer.tokens(); tok; tok = tok->next()) { if (tok->str() != tokstr || tok->linenr() != linenr) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [value](const ValueFlow::Value& v) { return v.isIntValue() && v.isKnown() && v.intvalue == value; })) return true; } return false; } void literalAnnotation() { // Requirement: integer literal 0 in divisor position must be annotated // with Known value 0 so that checkZeroDivision detects "return x / 0". ASSERT(testValueOfTokenKnown("int f(int x) { return x / 0; }", 1, "0", 0)); // Requirement: integer literal 0 in modulo divisor must be annotated. ASSERT(testValueOfTokenKnown("int f(int x) { return x % 0; }", 1, "0", 0)); // Requirement: integer literal array subscript must be annotated so that // arrayIndex detects "int a[5]; a[5] = 0" (one-past-end). ASSERT(testValueOfTokenKnown("void f() { int a[5]; a[5] = 0; }", 1, "5", 5)); // Requirement: negative literal subscript must be annotated so that // arrayIndex detects "int a[5]; a[-1] = 0" (negative index). ASSERT(testValueOfTokenKnown("void f() { int a[5]; a[-1] = 0; }", 1, "-1", -1)); // Requirement: well-past-end literal subscript must also be annotated. ASSERT(testValueOfTokenKnown("void f() { int a[5]; a[10] = 0; }", 1, "10", 10)); } // ----------------------------------------------------------------------- // Robustness: no crash or hang on pathological inputs // ----------------------------------------------------------------------- // // These tests simply run the analysis on tricky or degenerate inputs and // verify that no crash, assertion failure, or infinite loop occurs. They // do not assert specific values — only that the analysis terminates. // // Requirement: the analysis must never crash regardless of input complexity. void nocrash() { // NC1: empty function body — nothing to analyze. { const char code[] = "void f() {}"; ASSERT(testValueOfXNone(code, 1)); } // NC2: function with no local variables — must not crash on empty state. { const char code[] = "void f(int a, int b) {\n" " return;\n" "}\n"; ASSERT(testValueOfXNone(code, 2)); } // NC3: deeply nested ternary expressions — must terminate. { const char code[] = "int f(int a,int b,int c,int d) {\n" " int x = a?b?c?d?1:2:3:4:5;\n" " return x;\n" "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); // only check no-crash; value irrelevant } // NC4: loop with complex condition — must terminate without hang. { const char code[] = "void f(int n) {\n" " int x = 0;\n" " while (n-- > 0 && x < 100) { x++; }\n" "}\n"; ASSERT(!testValueOfXKnown(code, 4, 0)); // no-crash check } // NC5: variable used only in sizeof — must not crash. // sizeof does not evaluate its argument, so x at line 3 may not // get a value annotation; we only verify the analysis terminates. { const char code[] = "void f() {\n" // 1 " int x = 5;\n" // 2 " (void)sizeof(x);\n" // 3 "}\n"; // No crash check: tokenize must succeed and analysis must not hang. ASSERT(!testValueOfXKnown(code, 3, 999)); // x is not 999; no crash } // NC6: goto forward — must not crash or loop. { const char code[] = "void f(int x) {\n" " if (x) goto end;\n" " x = 1;\n" " end: (void)x;\n" "}\n"; ASSERT(!testValueOfXKnown(code, 4, 0)); // no-crash check } // NC7: multiple return paths — must not crash on merge. { const char code[] = "int f(int a, int b) {\n" " int x;\n" " if (a) { x = 1; return x; }\n" " if (b) { x = 2; return x; }\n" " x = 3;\n" " return x;\n" "}\n"; ASSERT(testValueOfXKnown(code, 6, 3)); } } // ----------------------------------------------------------------------- // False-positive regression tests // ----------------------------------------------------------------------- // Each test here corresponds to a real false positive that was reported // against the DataFlow analysis. The analysis must NOT reproduce these. // Format: comment with ticket/issue reference, then the ASSERT. void falsePositiveRegression() { // FP1: re-assignment to an unknown value must clear the Known state. // Without this, "x = y" after "x = 5" would leave x as Known 5. { const char code[] = "void f(int y) {\n" " int x = 5;\n" " x = y;\n" " (void)x;\n" // x is NOT Known 5 after x=y "}\n"; ASSERT(!testValueOfXKnown(code, 4, 5)); } // FP2: variable modified in a loop body must not remain Known after // the loop — the loop may execute zero or more times. { const char code[] = "void f(int c) {\n" " int x = 5;\n" " while (c) { x = 10; }\n" " (void)x;\n" // x must NOT be Known 5 after the loop "}\n"; ASSERT(!testValueOfXKnown(code, 4, 5)); } // FP3: variable used before any assignment is UNINIT, not Known 0. // The analysis must not default-initialize local variables. { const char code[] = "void f() {\n" " int x;\n" " (void)x;\n" // x is UNINIT, NOT Known 0 "}\n"; ASSERT(!testValueOfXKnown(code, 3, 0)); ASSERT(testValueOfXUninit(code, 3)); } // FP4: pointer assigned null, then a function takes its address as an // out-parameter inside an if-condition. The callee may have written // a non-null value through the pointer, so x must NOT remain Known // null after the if-block. // // Regression test for lib/library.cpp:1326 false positive: // const Function* func = nullptr; // if (isNotLibraryFunction(ftok, &func)) return nullptr; // func->argumentChecks... ← must not be reported null { const char code[] = "struct S { int f; };\n" // 1 "bool getS(S** p);\n" // 2 "void test() {\n" // 3 " S* x = nullptr;\n" // 4 " if (getS(&x))\n" // 5 " return;\n" // 6 " (void)x->f;\n" // 7 ← x must NOT be Known null "}\n"; // After getS(&x), x may have been set to a non-null value. // The dataflow analysis must clear the Known-null state for x // when it detects a function call with &x in the if-condition. ASSERT(!testValueOfXKnown(code, 7, 0)); } // FP5: scalar passed by address as an out-parameter in an if-condition. // The call may initialize x, so x must NOT be reported UNINIT after // the condition. { const char code[] = "bool init(bool* out);\n" // 1 "void f() {\n" // 2 " bool x;\n" // 3 " if (init(&x))\n" // 4 " return;\n" // 5 " (void)x;\n" // 6 <- must not be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // FP6: call returns a pointer and also receives '&x' out-parameter; // x is only used when the return value indicates success. // This mirrors lib/library.cpp detectContainerOrIterator(). { const char code[] = "const int* init(bool* out);\n" // 1 "void f(bool* out) {\n" // 2 " bool x;\n" // 3 " const int* c = init(&x);\n" // 4 " if (c && out)\n" // 5 " *out = x;\n" // 6 <- must not be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // FP7: in "x && (x->...)" the RHS is evaluated only when x is true. // Even after "while (x && pred(x))", the dereference token in // the RHS must not keep a non-impossible null(0) value. // // This guards against false positives like: // return tok && (tok->... || ...); { const char code[] = "struct T { int v; };\n" // 1 "bool pred(const T*);\n" // 2 "const T* next(const T*);\n" // 3 "bool f(const T* x) {\n" // 4 " while (x && pred(x))\n" // 5 " x = next(x);\n" // 6 " return x && (x->v == 1);\n"// 7 "}\n"; const std::list values = tokenValues(code, "%var% ->"); const bool hasNullableZero = std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); }); ASSERT(!hasNullableZero); } // FP8: pointer returned from member function call (unknown result), // null-checked with early return, then dereferenced on the surviving // path. The dereference must not carry a nullable null value. // // Regression test for lib/astutils.cpp:334 false positive: // const auto* function = settings.library.getFunction(tok); // if (!function) // return Library::Container::Yield::NO_YIELD; // return function->containerYield; ← function must NOT be possibly null // // Root cause: when merging the if-then branch (terminates with return) // with the else branch (null check is false), DataFlow must use only // the else-branch state (which has Impossible(0) from applyCondition // Constraint), not merge it back with the unreachable then-branch. { const char code[] = "struct Lib { const void* getFunc(const char*) const; };\n" // 1 "struct Func { int y; };\n" // 2 "Lib lib;\n" // 3 "int test(const char* name) {\n" // 4 " const Func* function = (const Func*)lib.getFunc(name);\n" // 5 " if (!function)\n" // 6 " return 0;\n" // 7 " return function->y;\n" // 8 ← must NOT be possibly null "}\n"; // After the null check + early return at line 6-7, function is // guaranteed to be non-null on line 8. The analysis must NOT attach // a nullable (non-impossible) null value to function at the dereference. const std::list values = tokenValues(code, "function ->"); const bool hasNullableZero = std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); }); ASSERT(!hasNullableZero); } // FP9: pointer declared with brace-initialization ("T* p{}") is // value-initialized to nullptr — it must NOT be marked UNINIT. // Regression for lib/astutils.cpp:298: // const Library::Container* cont{}; // if (...) { ... cont = library.detectContainer(...) ... } // return { ..., cont ? cont : tok->valueType()->container }; // Root cause: isLhsOfAssignment() does not cover brace-init. { const char code[] = "struct S { int f; };\n" // 1 "void foo() {\n" // 2 " S* x{};\n" // 3 brace-init → nullptr " (void)x;\n" // 4 "}\n"; // Requirement: a pointer declared with {} is value-initialized to // nullptr, so it must NOT receive an UNINIT value. ASSERT(!testValueOfXUninit(code, 4)); } // FP10: integer declared with brace-initialization ("int x{}") is // zero-initialized — it must NOT be marked UNINIT. { const char code[] = "void foo() {\n" // 1 " int x{};\n" // 2 brace-init → 0 " (void)x;\n" // 3 "}\n"; // Requirement: an integer declared with {} is zero-initialized, // so it must NOT receive an UNINIT value. ASSERT(!testValueOfXUninit(code, 3)); } // FP11: scalar passed by non-const reference to a declared-only // function must not be UNINIT after the call. The callee likely // initializes x through the reference parameter. { const char code[] = "void init(int&);\n" // 1 "int f() {\n" // 2 " int x;\n" // 3 " init(x);\n" // 4 " return x;\n" // 5 "}\n"; // Requirement: x is passed as a non-const reference so the callee // can write to it. x must NOT be UNINIT at line 5. ASSERT(!testValueOfXUninit(code, 5)); } // FP11b: scalar passed by const reference — callee can only read x, // not write it. x must still be flagged as UNINIT at return. { const char code[] = "void observe(const int&);\n" // 1 "int f() {\n" // 2 " int x;\n" // 3 " observe(x);\n" // 4 " return x;\n" // 5 "}\n"; // Requirement: const-ref does not allow the callee to initialize x, // so x must still be UNINIT at line 5. ASSERT(testValueOfXUninit(code, 5)); } // FP12: scalar passed by non-const reference in a statement-level call, // followed by a second unrelated function call. The second call // must NOT re-inject UNINIT for x — the first call may have // initialized x through the reference parameter. // Mirrors lib/astutils.cpp: // int argnr; // const Token* ftok = getTokenArgumentFunction(tok, argnr); // const Function* func = ftok->function(); // if (func) { int a = argnr; } ← false positive without fix { const char code[] = "struct T { const void* g() const; };\n" // 1 "T* init(T*, int&);\n" // 2 "void f(T* tok) {\n" // 3 " int x;\n" // 4 " T* t = init(tok, x);\n" // 5 x written via ref " t->g();\n" // 6 second call " (void)x;\n" // 7 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // FP13: pointer used inside the body of "if (p && p->field)" must not // carry a nullable null value. The if-condition's LHS null-guard // must constrain p to non-null in the then-block. // Regression for lib/checkcondition.cpp:9 false positive: // while (scope && scope->isLocal()) scope = scope->nestedIn; // if (scope && scope->type == ScopeType::eFunction) { // const Function *func = scope->function; ← FP nullPointer // } { const char code[] = "struct S { int type; int val; };\n" // 1 "const S* next(const S*);\n" // 2 "void f(const S* p) {\n" // 3 " while (p && p->type == 1)\n" // 4 " p = next(p);\n" // 5 " if (p && p->type == 2) {\n" // 6 " int x = p->val;\n" // 7 must NOT be possibly null " (void)x;\n" // 8 " }\n" // 9 "}\n"; const std::list values = tokenValues(code, "p -> val"); const bool hasNullableZero = std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); }); ASSERT(!hasNullableZero); } // FP14: bool passed by non-const reference in an if-condition call. // The callee may initialize x through the reference when it returns // true; x is only read in the then-branch, so there is no UNINIT FP. // Regression for lib/checkcondition.cpp: // bool not_; // if (parseComparison(tok, not_)) // return not_; ← false positive without fix { const char code[] = "bool parse(const void*, bool&);\n" // 1 "bool f(const void* tok) {\n" // 2 " bool x;\n" // 3 " if (parse(tok, x))\n" // 4 " return x;\n" // 5 must NOT be UNINIT " return false;\n" // 6 "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // FP15: pointer in the true branch of "s ? s->field : 0" after a // "while (s && s->field > 0)" loop. The ternary condition already // null-checks s, so the dereference in the true branch must NOT // carry a nullable null value. // Regression for test1.cpp:11: // while (s && s->x > 0) s = s->next; // return s ? s->x : 0; <- false positive nullPointer { const char code[] = "struct S { int x; struct S* next; };\n" // 1 "int foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " return s ? s->x : 0;\n" // 5 "}\n"; // s in the true branch is guarded by the ternary condition — must NOT // carry a nullable (non-impossible) null value. const std::list values = tokenValues(code, "s . x"); const bool hasNullableZero = std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); }); ASSERT(!hasNullableZero); } // FP16: pointer in the false branch of null-test ternary after a loop. // Covers !s, s==0, 0==s, s==nullptr forms. // Regression for test1.cpp:11: // while (s && s->x > 0) s = s->next; // x = !s ? 0 : s->x; <- false positive nullPointer // x = (s==0 ? 0 : s->x); <- false positive nullPointer { // FP16a: !s const char code16a[] = "struct S { int x; struct S* next; };\n" // 1 "void foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " int x = !s ? 0 : s->x;\n" // 5 " (void)x;\n" // 6 "}\n"; const std::list vals16a = tokenValues(code16a, "s . x"); ASSERT(!std::any_of(vals16a.cbegin(), vals16a.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } { // FP16b: s==0 const char code16b[] = "struct S { int x; struct S* next; };\n" // 1 "void foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " int x = (s==0 ? 0 : s->x);\n" // 5 " (void)x;\n" // 6 "}\n"; const std::list vals16b = tokenValues(code16b, "s . x"); ASSERT(!std::any_of(vals16b.cbegin(), vals16b.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP15b: pointer in the true branch of "s!=0 ? s->field : 0". // s!=0 is a non-null-test so the true branch is guarded. { const char code[] = "struct S { int x; struct S* next; };\n" // 1 "int foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " return s!=0 ? s->x : 0;\n" // 5 "}\n"; const std::list values = tokenValues(code, "s . x"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP15c: pointer in the true branch of "!(s==0) ? s->field : 0". // !(null-test) is a non-null-test so the true branch is guarded. { const char code[] = "struct S { int x; struct S* next; };\n" // 1 "int foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " return !(s==0) ? s->x : 0;\n" // 5 "}\n"; const std::list values = tokenValues(code, "s . x"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP17: std::string assigned in both branches of an if/else must NOT // keep the Known-empty container size from its default construction. // Regression for test1.cpp:11: // std::string errmsg; // if (cond) errmsg = "non-empty A"; // else errmsg = "non-empty B"; // use(errmsg[0]); <- false positive containerOutOfBounds { const char code[] = "#include \n" // 1 "void f(bool cond) {\n" // 2 " std::string x;\n" // 3 " if (cond)\n" // 4 " x = \"non-empty A\";\n" // 5 " else\n" // 6 " x = \"non-empty B\";\n" // 7 " (void)x;\n" // 8 "}\n"; // After both branches assign x, the Known-empty state must be gone. // The container token must NOT carry a CONTAINER_SIZE value of 0. const std::list values = tokenValues(code, "x", ValueFlow::Value::ValueType::CONTAINER_SIZE); const bool hasEmptySize = std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isContainerSizeValue() && v.intvalue == 0 && !v.isImpossible(); }); ASSERT(!hasEmptySize); } // FP16c: pointer in the false branch of "!(s!=0) ? 0 : s->field". // !(non-null-test) is a null-test so the false branch is guarded. { const char code[] = "struct S { int x; struct S* next; };\n" // 1 "void foo(struct S* s) {\n" // 2 " while (s && s->x > 0)\n" // 3 " s = s->next;\n" // 4 " int x = (!(s!=0) ? 0 : s->x);\n" // 5 " (void)x;\n" // 6 "}\n"; const std::list values = tokenValues(code, "s . x"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP18: pointer dereference in a chained "!s || extra || s->field" // expression. The pointer s is null-tested by the first operand // of the || chain; s->b must NOT carry a nullable null value. // Regression: "while (s && s->a > 0) s = s->next; // return !s || g || s->c == 0;" { const char code[] = "struct S { int a; int c; struct S* next; };\n" // 1 "int g;\n" // 2 "bool foo(struct S* s) {\n" // 3 " while (s && s->a > 0)\n" // 4 " s = s->next;\n" // 5 " return !s || g || s->c == 0;\n" // 6 "}\n"; // s->c on line 6 is only reached when s is non-null (!s is false, // guarded by the first || operand). No nullable (non-impossible) null value. const std::list values = tokenValues(code, "s . c"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP17: pointer dereference in a chained "s && extra && s->field" // expression. The pointer s is guarded by the first operand of // the && chain; s->b must NOT carry a nullable null value. // Regression: "while (s && s->a > 0) s = s->next; // return s && g && s->b == 0;" { const char code[] = "struct S { int a; int b; struct S* next; };\n" // 1 "int g;\n" // 2 "bool foo(struct S* s) {\n" // 3 " while (s && s->a > 0)\n" // 4 " s = s->next;\n" // 5 " return s && g && s->b == 0;\n" // 6 "}\n"; // s->b on line 6 is only reached when s is non-null (guarded by // the first && operand). No nullable (non-impossible) null value. const std::list values = tokenValues(code, "s . b"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP20: variable passed by address to a function call inside an // if-condition with negated early return, followed by an // unrelated function call. // The unrelated call must NOT re-inject UNINIT for the variable // because the callee of the condition call may have initialized // it through the pointer. // Regression for test1.cpp (PythonQt importer pattern): // const char* x; // if (!PyArg_ParseTuple(args, "s", &x)) return -1; // if (importInterface()->exists(x)) { ... } ← false positive { const char code[] = "bool parse(const char** out);\n" // 1 "void other();\n" // 2 "void use(const char*);\n" // 3 "void f() {\n" // 4 " const char* x;\n" // 5 " if (!parse(&x))\n" // 6 " return;\n" // 7 " other();\n" // 8 triggers re-injection bug " use(x);\n" // 9 must not be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 9)); } // FP21: pointer passed via reinterpret_cast(&p) to a function // call. The cast wraps the '&p' address-of expression in a // nested '(...)' that the token iteration skips; as a result the // variable is not recognised as an out-parameter and UNINIT is // re-injected after the call. // Regression for test1.cpp (HSA/AMD GPU allocation pattern): // struct T* p; // fn(pool, reinterpret_cast(&p)); // memset(p->field, 0, sizeof(p->field)); ← false positive { // C++ named cast: reinterpret_cast(&x) const char code[] = "void alloc(void**);\n" // 1 "void f() {\n" // 2 " int* x;\n" // 3 " alloc(reinterpret_cast(&x));\n" // 4 " (void)*x;\n" // 5 must not be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } { // C-style cast: (void**)(&x) const char code[] = "void alloc(void**);\n" // 1 "void f() {\n" // 2 " int* x;\n" // 3 " alloc((void**)(&x));\n" // 4 " (void)*x;\n" // 5 must not be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // FP19: container.empty() early-return guard inside a lambda. // The lambda captures a container by reference; inside the lambda // body "if (x.empty()) return false; return x.top() > 0;" the // x.top() use is guarded. After the guard, x must NOT carry a // Known CONTAINER_SIZE=0 value. // Regression for test2.cpp: // std::stack<...> endInitList; // auto inInitList = [&] { // if (endInitList.empty()) return false; // line 9 // return endInitList.top().second == scope; // line 11 // }; { // No #include — line numbers stay stable; std.cfg provides container info. const char code[] = "void f() {\n" // 1 " std::stack x;\n" // 2 " auto lam = [&] {\n" // 3 " if (x.empty())\n" // 4 " return false;\n" // 5 " return x.top() > 0;\n" // 6 " };\n" // 7 " (void)lam;\n" // 8 "}\n"; const Settings containerSettings = settingsBuilder().checkLevel(Settings::CheckLevel::normal) .library("std.cfg") .build(); // After the guard, x at line 6 must NOT have a Known CONTAINER_SIZE=0. // Before the fix this was a false positive: the dataflow analysis // carried the Known-empty state through the early-return guard. ASSERT(!testContainerSizeKnown(code, 6, 0, containerSettings)); } // FP22: variable assigned in every branch of a deep if-else-if chain // (5 levels) that exceeds MAX_BRANCH_DEPTH must NOT be reported // as uninitialized after the chain. // Regression for test1.c (assembly-style number parser): // long radix; // if (A) radix=16; else if (B) radix=16; else if (C) radix=16; // else if (D) radix=8; else if (E) radix=2; else radix=10; // radix++; ← false positive uninitvar // Root cause: the analysis aborts at MAX_BRANCH_DEPTH=4 without // processing assignments in the innermost else-block, leaving // the variable in ctx.uninits. { const char code[] = "void f(int a, int b, int c, int d, int e) {\n" // 1 " int x;\n" // 2 " if (a) x = 1;\n" // 3 " else if (b) x = 2;\n" // 4 " else if (c) x = 3;\n" // 5 " else if (d) x = 4;\n" // 6 " else if (e) x = 5;\n" // 7 " else x = 6;\n" // 8 " (void)x;\n" // 9 "}\n"; // x is assigned on every path — must NOT be reported as UNINIT. ASSERT(!testValueOfXUninit(code, 9)); } // FP23: pointer assigned in for-loop condition clause must NOT be // reported as uninitialized after the loop. // Regression for test1.c (proj library search pattern): // const char* s; // for (i = 0; (s = array[i].id) && cmp(s, name); ++i) ; // if (s == nullptr) return; <- false positive uninitvar // Root cause: the for-loop condition clause was not scanned for // assignments; variables assigned there remained in ctx.uninits // with a stale UNINIT value in ctx.state. { const char code[] = "const char* get(int);\n" // 1 "int cmp(const char*, const char*);\n" // 2 "void f(const char* name) {\n" // 3 " const char* x;\n" // 4 " int i;\n" // 5 " for (i = 0; (x = get(i)) && cmp(x, name); ++i)\n" // 6 " ;\n" // 7 " (void)x;\n" // 8 "}\n"; // x is assigned in the for-loop condition clause — must NOT be UNINIT. ASSERT(!testValueOfXUninit(code, 8)); } // FP24: pointer initialized via address-of in a conditional block, // then used after a guard that ensures the condition was true. // Regression for test1.c (TclNRTailcallEval pattern): // Tcl_Namespace *nsPtr; // if (result == TCL_OK) { result = getNamespace(interp, obj, &nsPtr); } // if (result != TCL_OK) { return; } // other_call(); ← reinjects UNINIT (FP) without Phase U-CI // use(nsPtr); ← false positive uninitvar // Root cause: Phase U2 re-injects UNINIT for nsPtr after the // intermediate function call because the analysis does not know // that `if (result != TCL_OK) { return; }` implies the first // if-block was taken (and hence nsPtr was initialized). // Fix (Phase U-CI): record that nsPtr was conditionally initialized // when result == 1; resolve it on the surviving path of the guard. { const char code[] = "int init(int**);\n" // 1 "void other();\n" // 2 "void use(int*);\n" // 3 "void f(int result) {\n" // 4 " int* x;\n" // 5 " if (result == 1) {\n" // 6 " result = init(&x);\n" // 7 x initialized, result reassigned " }\n" // 8 " if (result != 1) {\n" // 9 " return;\n" // 10 " }\n" // 11 " other();\n" // 12 triggers UNINIT re-injection without fix " use(x);\n" // 13 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 13)); } // FP25: variables initialized via address-of in a function-pointer call // used as the if-condition must NOT be reported as uninitialized. // Regression for test1.c (fcitx input-method pattern): // int x; int y; // if ((*f)(0, &x, &y)) { use(x, y); } ← false positive uninitvar y // Root cause: isFunctionCallOpen returns false for "(*f)(" because // the token preceding the argument-list '(' is ')' (not a name). // Pass 2 of the condition scan therefore never recognises the call, // so &x/&y are never treated as out-parameters, and UNINIT is not // cleared from ctx.uninits before forking into the then-branch. { const char code[] = "int (*f)(int, int*, int*);\n" // 1 "void use(int, int);\n" // 2 "void foo() {\n" // 3 " int x;\n" // 4 " int y;\n" // 5 " if ((*f)(0, &x, &y)) {\n" // 6 " use(x, y);\n" // 7 must NOT be UNINIT " }\n" // 8 "}\n"; // y is passed as &y to the function — must NOT be reported as UNINIT. ASSERT(!testValueOfXUninit(code, 7)); } // FP26: variables initialized via address-of in a function-pointer call // used as a standalone statement must NOT be reported as uninitialized. // Same root pattern as FP25 but without the wrapping if-condition. // Regression for test1.c (fcitx input-method pattern, direct call form): // int x; int y; // (*f)(0, &x, &y); // use(x, y); ← false positive uninitvar y // Root cause: isFunctionCallOpen returned false for "(*f)(" because // the token before the argument-list '(' is ')' (not a name), so the // entire statement-level function-call handler was skipped. { const char code[] = "int (*f)(int, int*, int*);\n" // 1 "void use(int, int);\n" // 2 "void foo() {\n" // 3 " int x;\n" // 4 " int y;\n" // 5 " (*f)(0, &x, &y);\n" // 6 " use(x, y);\n" // 7 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // FP27: variables initialized via address-of in a function-pointer call // used as a for-loop condition must NOT be reported as uninitialized. // Regression for test1.c (fcitx input-method pattern, for-loop form): // int x; int y; // for (;(*f)(0, &x, &y);); // use(x, y); ← false positive uninitvar y // Root cause: the for-loop condition clause scan (Phase U2-FC) only // handled assignment operators, not function calls with &var arguments. // clearConditionOutVars was called for while/do-while but not for-loops. { const char code[] = "int (*f)(int, int*, int*);\n" // 1 "void use(int, int);\n" // 2 "void foo() {\n" // 3 " int x;\n" // 4 " int y;\n" // 5 " for (;(*f)(0, &x, &y););\n" // 6 " use(x, y);\n" // 7 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 7)); } // FP28: assignment through *& (dereference of address-of) must be // recognized as initializing the variable. // Regression for test1.cpp: // int mode; // *&mode = 1; ← this DOES initialize mode // ++mode; ← false positive uninitvar // Root cause: the assignment handler only handles lhs->varId() > 0 // (simple variable LHS); for *&var the LHS is '*' with varId==0, // so the assignment was skipped and mode remained UNINIT in state. { const char code[] = "void f() {\n" // 1 " int x;\n" // 2 " *&x = 1;\n" // 3 " (void)x;\n" // 4 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 4)); } // FP29: variable assigned in if-block before a goto must NOT be // reported as uninitialized at the goto label. // Regression for test1.c (Linux kernel error-handling pattern): // int err; // if (a()) { err = -1; goto out; } // return 0; // out: // return err; ← false positive uninitvar // Root cause 1: goto is not listed as a block terminator in // blockTerminates(), so the then-branch (err=assigned) and the // implicit else-branch (err=UNINIT) are merged as Possible(UNINIT). // Root cause 2: the label is a goto target where err is always // assigned, but the analysis propagates the merged Possible(UNINIT) // state past the label and re-injects UNINIT after b(). // Fix: (1) treat goto as a block terminator; (2) clear UNINIT // state at goto labels (conservative — false negatives preferred). { const char code[] = "int a();\n" // 1 "void b();\n" // 2 "int f() {\n" // 3 " int x;\n" // 4 " if (a()) { x = -1; goto out; }\n" // 5 " return 0;\n" // 6 " out:\n" // 7 " b();\n" // 8 " return x;\n" // 9 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 9)); } // FP30: variable initialized in the then-branch of an if/else where // the else-branch calls a noreturn library function (exit). // blockTerminates must recognise exit() as a terminator so only // the then-branch state survives to the join point. // Regression for test1.c pattern: // int deffile; // if ((s = cl_arg(1)) != NULL) { deffile = 0; } else { exit(1); } // write_ptrfile(ptrfile, deffile); ← false positive uninitvar { const Settings stdSettings = settingsBuilder().checkLevel(Settings::CheckLevel::normal) .library("std.cfg") .build(); const char code[] = "void f(int c) {\n" // 1 " int x;\n" // 2 " if (c) {\n" // 3 " x = 1;\n" // 4 " } else {\n" // 5 " exit(1);\n" // 6 " }\n" // 7 " (void)x;\n" // 8 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 8, stdSettings)); } // FP31: non-const pointer alias — address taken via non-const pointer. // Variable x is written through a non-const unsigned char* alias // (all bytes of a double overwritten via array indexing), then // read. Without the fix, Phase L only adds x to addressTaken but // does not remove x from ctx.uninits/ctx.state, so the UNINIT // value persists and produces a false positive at the read. // Requirement 7: abort tracking when &var is stored in a // non-const pointer. Requirement 8: partial initialization // through a non-const alias counts as full initialization. { const char code[] = "void f(unsigned char *b) {\n" // 1 " double x;\n" // 2 " unsigned char *p = (unsigned char *)(&x);\n" // 3 " p[0] = b[0];\n" // 4 " (void)x;\n" // 5 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 5)); } // FP32: variable assigned in a do-while condition must NOT be reported // as UNINIT after the loop. The do-while condition executes at // least once unconditionally, so "do {} while ((x = sel()) <= 0)" // guarantees x is initialized before any subsequent read. // Reproduces: false positive "Uninitialized variable: result" // for "do {} while (((result = select(...)) <= 0) && ...)". { const char code[] = "int sel();\n" // 1 "void f() {\n" // 2 " int x;\n" // 3 ← declared without init " do {\n" // 4 " } while ((x = sel()) <= 0);\n" // 5 ← assigns x " (void)x;\n" // 6 ← must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // FP33: variable initialized via inline asm output constraint must NOT // be reported as UNINIT. When asm() is encountered the dataflow // analysis is aborted because it cannot reason about assembler // effects (Requirement 9 / Requirement 4: no false positives). { const char code[] = "void f() {\n" // 1 " unsigned int x;\n" // 2 ← declared without init " asm(\"\" : \"=a\"(x));\n" // 3 ← initializes x via asm " (void)x;\n" // 4 ← must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 4)); } // FP34: pointer used in a for-loop increment clause must NOT be reported // as possibly null. The for-loop condition guarantees the pointer is // non-null when the increment executes. The backward analysis must // not propagate a post-loop null constraint backward through the // for-loop's increment clause. // Regression for test1.c (Linux kernel sym_check_print_recursive): // struct dep_stack *stack; // for (stack = check_top; stack != NULL; stack = stack->prev) ... // if (!stack) { ... } // Root cause: the backward pass walks the for-header tokens after // crossing the body '}', so "stack = Possible(null)" (from the // post-loop null check) reaches the RHS of "stack = stack->prev" // and annotates it with a nullable null value. { const char code[] = "struct dep_stack { struct dep_stack *prev; void *sym; };\n" // 1 "void f(struct dep_stack *check_top) {\n" // 2 " struct dep_stack *stack;\n" // 3 " for (stack = check_top; stack != NULL; stack = stack->prev)\n" // 4 " if (1) break;\n" // 5 " if (!stack) return;\n" // 6 "}\n"; // stack in "stack->prev" (increment clause, line 4) must NOT be possibly null. // Note: cppcheck's tokenizer replaces "->" with "." in the token list. const std::list values = tokenValues(code, "stack . prev"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP35: while (x && x->next) loop followed by "if (x == NULL)" — the // NULL macro must be recognized as a null pointer constant so that // the else branch knows x is non-null (Impossible 0). // Regression: test1.c false positive nullPointer at the else-branch // dereference "x->next = 0". // Root cause: evalConstInt did not handle the token "NULL", so // applyConditionConstraint ignored the "x == NULL" condition and // left x as Possible(0) in both branches. { const char code[] = "struct Node { struct Node *next; };\n" // 1 "void f(struct Node *x) {\n" // 2 " while (x && x->next)\n" // 3 " x = x->next;\n" // 4 " if (x == NULL)\n" // 5 " (void)0;\n" // 6 " else\n" // 7 " x->next = 0;\n" // 8 x must be Impossible(0) "}\n"; ASSERT(testValueOfXImpossible(code, 8, 0)); } // FP36: (obj->field == NULL) || (obj->field->member == ...) pattern. // The LHS null-test short-circuits: the RHS is only evaluated when // obj->field is NOT null. obj->field in the RHS must NOT be annotated // with Possible(0) (CheckNullPointer false positive). // Regression for test1.c (TkDisplay grabWinPtr pattern): // if (dispPtr->grabWinPtr != NULL) {} // if ((dispPtr->grabWinPtr == NULL) || (dispPtr->grabWinPtr->mainPtr == ...)) // Root cause: annotateMemberTok did not check whether the member // access is on the RHS of a guarded '||' null-test for the same member. { const char code[] = "struct T { int y; };\n" // 1 "struct S { struct T * x; };\n" // 2 "void foo(struct S *s) {\n" // 3 " if (s->x != 0) {}\n" // 4 x becomes Possible(0) after merge " if ((s->x == 0) || (s->x->y == 0)) {}\n" // 5 x in RHS must NOT be Possible(0) "}\n"; // After tokenizer "->" → ".": s->x->y becomes s . x . y // x in "x . y" context is guarded by (s->x == 0) on the LHS of || // and must not carry a non-Impossible null value. const std::list values = tokenValues(code, "x . y"); ASSERT(!std::any_of(values.cbegin(), values.cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.intvalue == 0 && !v.isImpossible(); })); } // FP37: goto-based error path — backward analysis must not propagate // the "if (x)" null-check backward past a goto label and a // return statement, annotating x at the dereference site // with Possible(0) (false positive nullPointerRedundantCheck). // Regression for test1.c (etna_cmd_stream_new pattern): // x = calloc(...); // if (!x) { goto fail; } // x->size = size; ← must NOT be Possible(0) // return x; // fail: // if (x) ← backward check must not propagate above // Root cause: backwardAnalyzeBlock walked backward past the "fail:" // label and past "return", propagating Possible(0) for x to // the dereference site where no null-path exists at runtime. // Fix: clear backward state at goto labels and return statements. { // The function call at line 7 clears x from the forward state so that // x at line 8 has no forward Impossible(0) to block the backward Possible(0). // Without the fix, the backward "if (x)" at line 11 propagates Possible(0) // backward past "fail:" and "return x", annotating x at line 8 with Possible(0) // (false positive nullPointerRedundantCheck). const char code[] = "struct S { void *buf; int size; };\n" // 1 "void del(struct S *p);\n" // 2 "void *alloc(int n);\n" // 3 "struct S *foo(int n) {\n" // 4 " struct S *x = 0;\n" // 5 " x = alloc(n);\n" // 6 clears x from forward state " if (!x) { goto fail; }\n" // 7 surviving: x=Impossible(0) " x->buf = alloc(n);\n" // 8 function call clears x from state " x->size = n;\n" // 9 x NOT in forward state here " return x;\n" // 10 " fail:\n" // 11 " if (x)\n" // 12 backward: injects Possible(0) " del(x);\n" // 13 " return 0;\n" // 14 "}\n"; // x at line 9 (the dereference site) must NOT carry Possible(0) ASSERT(!testValueOfXPossible(code, 9, 0)); } // FP38: null check inside an expanded macro must not inject Possible(null) // for the pointer after the macro call. // // Pattern (bzip2 BZ_SETERR macro): // #define SET_ERR(e) { if (x != 0) x->f = e; } // void foo(struct S* x) { // SET_ERR(1); // (void)x; ← x must NOT be Possible(null) // } // // Root cause: applyConditionConstraint was called for the "if" // inside the macro, setting x=Known(0) on the else-path. After // the no-else merge, x became Possible(0) on the surviving path, // causing a false nullPointerRedundantCheck. // Fix: skip applyConditionConstraint when the "if" token comes // from an expanded macro (tok->isExpandedMacro()). // // Uses SimpleTokenizer2 to get real macro expansion so that the // macro-expanded "if" token has isExpandedMacro() == true. { const char code[] = "#define SET_ERR(e) { if (x != 0) x->f = e; }\n" // 1 "struct S { int f; int g; };\n" // 2 "void foo(struct S* x) {\n" // 3 " SET_ERR(1);\n" // 4 " (void)x;\n" // 5 "}\n"; // Use SimpleTokenizer2 so that SET_ERR(1) is fully preprocessed // and the "if" token inside the expansion has isExpandedMacro() true. SimpleTokenizer2 tokenizer2(settings, *this, code, "test.c"); ASSERT(tokenizer2.simplifyTokens1("")); bool xPossibleNullAtLine5 = false; for (const Token* tok = tokenizer2.tokens(); tok; tok = tok->next()) { if (tok->str() != "x" || tok->linenr() != 5) continue; if (std::any_of(tok->values().cbegin(), tok->values().cend(), [](const ValueFlow::Value& v) { return v.isIntValue() && v.isPossible() && v.intvalue == 0; })) { xPossibleNullAtLine5 = true; break; } } // x at line 5 must NOT be Possible(null) — macro-internal null // check must not leak into the post-macro state. ASSERT(!xPossibleNullAtLine5); } // FP39: variable assigned in a top-level function-call argument // (e.g. "foo(bar, res = make())") must NOT be reported as UNINIT // when used after the outer call. // Pattern from jitterc (jitter VM code-generator): // struct jitterc_instruction * res; // jitterc_vm_append_instruction(vm, res = jitterc_make_instruction()); // res->name = ...; ← false positive uninitvar // Root cause: Phase U-ArgAssign only scanned for assignments inside // '(...)' groups. A top-level assignment "res = func()" at the // argument level (not wrapped in extra parens) was never processed, // so 'res' stayed in ctx.uninits. clearCallClobberableState then // preserved the UNINIT entry (UNINIT-only, not address-taken), and // the subsequent read was falsely annotated as UNINIT. // Fix: in the argument annotation loop, detect plain '=' at the // top level and erase the LHS variable from ctx.uninits/state. { const char code[] = "void append(void*, void*);\n" // 1 "void* make();\n" // 2 "void f(void* vm) {\n" // 3 " void* x;\n" // 4 " append(vm, x = make());\n" // 5 " (void)x;\n" // 6 must NOT be UNINIT "}\n"; ASSERT(!testValueOfXUninit(code, 6)); } // FP-flag: pointer guarded by a boolean flag — Possible(null) from a // prior if(x) merge must not propagate into a later if(found) block. // // RoutePoint* x = getPoint(); // state cleared after call // if (x) found = true; // merge: x gets Possible(null) // if (found) { // use(x->m_lat); // x must NOT be Possible(null) here // } // // Root cause: after merging the then-path (x=Impossible(0)) and // else-path (x=Known(0)) at the first if, x becomes Possible(0). // That Possible(0) was then annotated inside the second if-block, // triggering a false nullPointerRedundantCheck warning. // Fix: suppress Possible(0) null annotations inside conditional blocks // (branchDepth > 0), same as the existing Possible(UNINIT) suppression. { const char code[] = "struct RoutePoint { double m_lat; };\n" // 1 "RoutePoint* getPoint();\n" // 2 "void f() {\n" // 3 " bool found = false;\n" // 4 " RoutePoint* x = getPoint();\n" // 5 " if (x) found = true;\n" // 6 " if (found) {\n" // 7 " double s = x->m_lat;\n" // 8 ← x must NOT be Possible(null) " }\n" // 9 "}\n"; // 10 ASSERT(!testValueOfXPossible(code, 8, 0)); } } }; REGISTER_TEST(TestDataFlow)