From f68c6bd42910364aff0a5a3b5ece129190e425ca Mon Sep 17 00:00:00 2001 From: VIFEX Date: Wed, 29 Apr 2026 03:42:14 +0800 Subject: [PATCH] ci(cppcheck): add library config to suppress false nullPointerRedundantCheck (#10032) --- scripts/check_cppcheck.py | 194 +++++++++++++++++++++++++++++--------- scripts/lvgl_cppcheck.cfg | 11 +++ 2 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 scripts/lvgl_cppcheck.cfg diff --git a/scripts/check_cppcheck.py b/scripts/check_cppcheck.py index 0de0a9ffb2..82cbb76bd7 100644 --- a/scripts/check_cppcheck.py +++ b/scripts/check_cppcheck.py @@ -33,10 +33,13 @@ BLOCKING_SEVERITIES = {"error"} # Severity levels that produce annotations but don't block ANNOTATION_SEVERITIES = {"warning"} +CPPCHECK_LIBRARY = str(Path(__file__).resolve().parent / "lvgl_cppcheck.cfg") + CPPCHECK_COMMON_ARGS = [ "--enable=all", "--quiet", "--inline-suppr", + f"--library={CPPCHECK_LIBRARY}", "--suppress=unusedFunction", "--suppress=preprocessorErrorDirective", "--suppress=missingIncludeSystem", @@ -80,17 +83,19 @@ def run_cppcheck(files: List[str], jobs: int = 1) -> List[Dict]: if not files: return [] - cmd = ["cppcheck"] + CPPCHECK_COMMON_ARGS + [ - f"--template={TEMPLATE}", - ] + cmd = ( + ["cppcheck"] + + CPPCHECK_COMMON_ARGS + + [ + f"--template={TEMPLATE}", + ] + ) if jobs > 1: cmd.append(f"-j{jobs}") cmd.extend(files) try: - result = subprocess.run( - cmd, capture_output=True, text=True, timeout=600 - ) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) except subprocess.TimeoutExpired as exc: print("cppcheck timed out after 600s", file=sys.stderr) raise RuntimeError("cppcheck timed out after 600s") from exc @@ -99,8 +104,14 @@ def run_cppcheck(files: List[str], jobs: int = 1) -> List[Dict]: # Non-zero typically means internal error (bad args, crash). # Parse stderr regardless, but warn on unexpected exit codes. if result.returncode != 0: - print(f"warning: cppcheck exited with code {result.returncode}", - file=sys.stderr) + print( + f"warning: cppcheck exited with code {result.returncode}", file=sys.stderr + ) + + if "Failed to load library" in result.stderr: + raise RuntimeError( + f"cppcheck failed to load library configuration:\n{result.stderr.strip()}" + ) issues = [] for line in result.stderr.splitlines(): @@ -108,17 +119,21 @@ def run_cppcheck(files: List[str], jobs: int = 1) -> List[Dict]: if len(parts) == 5: severity, filepath, lineno, checker_id, message = parts if severity in ("error", "warning", "style", "performance", "portability"): - issues.append({ - "severity": severity, - "file": filepath, - "line": int(lineno) if lineno.isdigit() else 0, - "id": checker_id, - "message": message, - }) + issues.append( + { + "severity": severity, + "file": filepath, + "line": int(lineno) if lineno.isdigit() else 0, + "id": checker_id, + "message": message, + } + ) # If cppcheck failed AND produced no parseable output, that's a real problem if result.returncode != 0 and not issues and not result.stderr.strip(): - raise RuntimeError(f"cppcheck exited with code {result.returncode} and no output") + raise RuntimeError( + f"cppcheck exited with code {result.returncode} and no output" + ) return issues @@ -136,7 +151,12 @@ def get_changed_files(diff_range: str, root: Path) -> List[str]: files = [] for f in result.stdout.strip().splitlines(): f = f.strip() - if f and f.startswith("src/") and (f.endswith(".c") or f.endswith(".h")) and not is_excluded(f): + if ( + f + and f.startswith("src/") + and (f.endswith(".c") or f.endswith(".h")) + and not is_excluded(f) + ): full = root / f if full.exists(): files.append(str(full)) @@ -187,7 +207,11 @@ def print_summary(issues: List[Dict], root: Path) -> int: for sev in ("error", "warning", "style", "performance", "portability"): group = by_severity.get(sev, []) if group: - icon = "🚫" if sev in BLOCKING_SEVERITIES else "⚠️" if sev in ANNOTATION_SEVERITIES else "📝" + icon = ( + "🚫" + if sev in BLOCKING_SEVERITIES + else "⚠️" if sev in ANNOTATION_SEVERITIES else "📝" + ) print(f" {icon} {sev}: {len(group)}") print() @@ -199,7 +223,9 @@ def print_summary(issues: List[Dict], root: Path) -> int: rel = os.path.relpath(issue["file"], str(root)) except ValueError: rel = issue["file"] - print(f" {rel}:{issue['line']}: {issue['severity']}: {issue['message']} [{issue['id']}]") + print( + f" {rel}:{issue['line']}: {issue['severity']}: {issue['message']} [{issue['id']}]" + ) ci = os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS") if ci: @@ -238,8 +264,12 @@ def run_self_test() -> int: check(not is_excluded("src/core/lv_obj.c"), "include core") check(not is_excluded("src/widgets/chart/lv_chart.c"), "include widgets") check(not is_excluded("src/draw/nanovg/lv_nanovg.c"), "include draw drivers") - check(not is_excluded("src/drivers/wayland/lv_wayland.c"), "include platform drivers") - check(not is_excluded("src/core/lv_libs_helper.c"), "include file with 'libs' in name") + check( + not is_excluded("src/drivers/wayland/lv_wayland.c"), "include platform drivers" + ) + check( + not is_excluded("src/core/lv_libs_helper.c"), "include file with 'libs' in name" + ) print(f" exclusion logic: {passed} passed") # --- 2. cppcheck availability --- @@ -266,27 +296,27 @@ def run_self_test() -> int: test_cases = [ { "name": "null pointer dereference", - "code": 'void f(void) { int *p = 0; *p = 1; }\n', + "code": "void f(void) { int *p = 0; *p = 1; }\n", "expect_severity": "error", }, { "name": "uninitialized variable", - "code": 'void f(void) { int x; int y = x + 1; (void)y; }\n', + "code": "void f(void) { int x; int y = x + 1; (void)y; }\n", "expect_severity": "error", }, { "name": "array out of bounds", - "code": 'void f(void) { int a[10]; a[10] = 0; }\n', + "code": "void f(void) { int a[10]; a[10] = 0; }\n", "expect_severity": "error", }, { "name": "memory leak", - "code": 'void* malloc(unsigned long);\nvoid f(void) { void *p = malloc(10); if(!p) return; }\n', + "code": "void* malloc(unsigned long);\nvoid f(void) { void *p = malloc(10); if(!p) return; }\n", "expect_severity": "error", }, { "name": "variable scope (style)", - "code": 'void f(int n) { int i; if(n > 0) { for(i = 0; i < n; i++) {} } }\n', + "code": "void f(int n) { int i; if(n > 0) { for(i = 0; i < n; i++) {} } }\n", "expect_severity": "style", }, ] @@ -297,26 +327,85 @@ def run_self_test() -> int: fh.write(tc["code"]) issues = run_cppcheck([test_file], jobs=1) - # Check that at least one issue of expected severity is found - # (don't pin checker IDs — they vary across cppcheck versions) found = any(i["severity"] == tc["expect_severity"] for i in issues) check(found, f"detect {tc['name']}") status = "✅" if found else "❌" print(f" {status} {tc['name']}") + + # --- 4b. Assert-aware suppression tests --- + # Use LV_ASSERT_NULL (mapped by lvgl_cppcheck.cfg) to verify + # the library config is actually loaded and effective. + print(" assert suppression tests:") + assert_test_cases = [ + { + "name": "LV_ASSERT_NULL suppresses nullPointerRedundantCheck", + "code": ( + "void f(int *p) {\n" + " LV_ASSERT_NULL(p);\n" + " *p = 42;\n" + "}\n" + ), + "expect_no_ids": ["nullPointerRedundantCheck"], + }, + { + "name": "LV_ASSERT_NULL on p does not suppress null deref on q", + "code": ( + "void f(int *p, int *q) {\n" + " LV_ASSERT_NULL(p);\n" + " if(q) {}\n" + " *q = 42;\n" + "}\n" + ), + "expect_ids": ["nullPointerRedundantCheck"], + }, + { + "name": "without assert null deref is still detected", + "code": ( + "void f(int *p) {\n" " if(p) {}\n" " *p = 42;\n" "}\n" + ), + "expect_ids": ["nullPointerRedundantCheck"], + }, + ] + + for tc in assert_test_cases: + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, "w") as fh: + fh.write(tc["code"]) + + issues = run_cppcheck([test_file], jobs=1) + issue_ids = {i["id"] for i in issues} + if "expect_no_ids" in tc: + bad = issue_ids & set(tc["expect_no_ids"]) + ok = len(bad) == 0 + else: + ok = all(eid in issue_ids for eid in tc["expect_ids"]) + check(ok, tc["name"]) + status = "✅" if ok else "❌" + print(f" {status} {tc['name']}") else: print(" detection tests: SKIPPED (cppcheck not found)") # --- 5. Annotation formatting --- root = Path("/fake/root") - test_issue = {"severity": "error", "file": "/fake/root/src/core/lv_obj.c", - "line": 42, "id": "nullPointer", "message": "test"} + test_issue = { + "severity": "error", + "file": "/fake/root/src/core/lv_obj.c", + "line": 42, + "id": "nullPointer", + "message": "test", + } ann = format_github_annotation(test_issue, root) check("::error" in ann, "annotation level") check("line=42" in ann, "annotation line") check("cppcheck:nullPointer" in ann, "annotation checker id") - test_issue_noline = {"severity": "warning", "file": "/fake/root/src/core/lv_obj.c", - "line": 0, "id": "test", "message": "test"} + test_issue_noline = { + "severity": "warning", + "file": "/fake/root/src/core/lv_obj.c", + "line": 0, + "id": "test", + "message": "test", + } ann2 = format_github_annotation(test_issue_noline, root) check("line=" not in ann2, "annotation omits line=0") print(f" annotation formatting: OK") @@ -331,18 +420,28 @@ def main() -> int: description="LVGL cppcheck static analysis checker", formatter_class=argparse.RawDescriptionHelpFormatter, ) - parser.add_argument("--diff", metavar="RANGE", - help="Check files changed in git diff range (e.g. HEAD~3...HEAD)") - parser.add_argument("--file", metavar="PATH", - help="Check a specific file or directory") - parser.add_argument("--all", action="store_true", - help="Full scan of all src/**/*.c and src/**/*.h") - parser.add_argument("--self-test", action="store_true", - help="Run built-in sanity checks") - parser.add_argument("--jobs", "-j", type=int, default=os.cpu_count() or 4, - help="Number of parallel jobs (default: CPU count)") - parser.add_argument("--verbose", "-v", action="store_true", - help="Verbose output") + parser.add_argument( + "--diff", + metavar="RANGE", + help="Check files changed in git diff range (e.g. HEAD~3...HEAD)", + ) + parser.add_argument( + "--file", metavar="PATH", help="Check a specific file or directory" + ) + parser.add_argument( + "--all", action="store_true", help="Full scan of all src/**/*.c and src/**/*.h" + ) + parser.add_argument( + "--self-test", action="store_true", help="Run built-in sanity checks" + ) + parser.add_argument( + "--jobs", + "-j", + type=int, + default=os.cpu_count() or 4, + help="Number of parallel jobs (default: CPU count)", + ) + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") args = parser.parse_args() root = find_repo_root() @@ -360,8 +459,11 @@ def main() -> int: elif args.file: target = Path(args.file) if target.is_dir(): - files = [str(f) for f in sorted(target.rglob("*.c")) + sorted(target.rglob("*.h")) - if not is_excluded(str(f))] + files = [ + str(f) + for f in sorted(target.rglob("*.c")) + sorted(target.rglob("*.h")) + if not is_excluded(str(f)) + ] else: # Apply exclusion even for single file (Copilot fix) if is_excluded(str(target)): diff --git a/scripts/lvgl_cppcheck.cfg b/scripts/lvgl_cppcheck.cfg new file mode 100644 index 0000000000..99dabec2b3 --- /dev/null +++ b/scripts/lvgl_cppcheck.cfg @@ -0,0 +1,11 @@ + + + + + + + + + +