ci(cppcheck): add library config to suppress false nullPointerRedundantCheck (#10032)
Arduino Lint / lint (push) Has been cancelled
Build Examples with C++ Compiler / build-examples (push) Has been cancelled
MicroPython CI / Build esp32 port (push) Has been cancelled
MicroPython CI / Build rp2 port (push) Has been cancelled
MicroPython CI / Build stm32 port (push) Has been cancelled
MicroPython CI / Build unix port (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build ESP IDF ESP32S3 (push) Has been cancelled
C/C++ CI / Run tests with 32bit build (push) Has been cancelled
C/C++ CI / Run tests with 64bit build (push) Has been cancelled
BOM Check / bom-check (push) Has been cancelled
Verify that lv_conf_internal.h matches repository state / verify-conf-internal (push) Has been cancelled
Verify GDB constants are up-to-date / verify-gdb-consts (push) Has been cancelled
Verify the widget property name / verify-property-name (push) Has been cancelled
Verify code formatting / verify-formatting (push) Has been cancelled
Compare file templates with file names / template-check (push) Has been cancelled
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (push) Has been cancelled
Check Makefile / Build using Makefile (push) Has been cancelled
Check Makefile for UEFI / Build using Makefile for UEFI (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Has been cancelled
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW Benchmark - Save PR Number (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Has been cancelled
Port repo release update / run-release-branch-updater (push) Has been cancelled
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Has been cancelled

This commit is contained in:
VIFEX
2026-04-29 03:42:14 +08:00
committed by GitHub
parent a7b95c5b08
commit f68c6bd429
2 changed files with 159 additions and 46 deletions
+148 -46
View File
@@ -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)):
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<def format="2">
<!-- Map LVGL assert macros to standard assert() so cppcheck
understands the post-condition (e.g. pointer is non-NULL). -->
<define name="LV_ASSERT(expr)" value="assert(expr)"/>
<define name="LV_ASSERT_MSG(expr,msg)" value="assert(expr)"/>
<define name="LV_ASSERT_FORMAT_MSG(expr,format,...)" value="assert(expr)"/>
<define name="LV_ASSERT_NULL(p)" value="assert((p) != NULL)"/>
<define name="LV_ASSERT_MALLOC(p)" value="assert((p) != NULL)"/>
<define name="LV_ASSERT_MEM_INTEGRITY()" value="assert(1)"/>
</def>