mirror of
https://github.com/lvgl/lvgl.git
synced 2026-05-10 04:37:55 +08:00
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
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:
+148
-46
@@ -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)):
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user