Skip to content

Commit 7efb2a4

Browse files
authored
Merge pull request #28 from sysprog21/static-analysis
CI: Add Clang Static Analyzer
2 parents eb4f066 + 9889ddf commit 7efb2a4

24 files changed

Lines changed: 170 additions & 106 deletions

.ci/install-deps.sh

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
#!/usr/bin/env bash
22

33
# Install build dependencies for CI
4-
# Usage: .ci/install-deps.sh [sdl2|headless|format]
4+
# Usage: .ci/install-deps.sh [sdl2|headless|format|analysis]
55

66
set -euo pipefail
77

88
source "$(dirname "$0")/common.sh"
99

1010
MODE="${1:-sdl2}"
1111

12+
# Setup LLVM 20 APT repository (shared by format and analysis modes)
13+
setup_llvm_repo() {
14+
LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg
15+
LLVM_KEY_FP="6084F3CF814B57C1CF12EFD515CF4D18AF4F7421"
16+
TMPKEY=$(mktemp)
17+
download_to_file https://apt.llvm.org/llvm-snapshot.gpg.key "$TMPKEY"
18+
ACTUAL_FP=$(gpg --with-fingerprint --with-colons "$TMPKEY" 2>/dev/null | grep fpr | head -1 | cut -d: -f10)
19+
if [ "$ACTUAL_FP" != "$LLVM_KEY_FP" ]; then
20+
print_error "LLVM key fingerprint mismatch!"
21+
print_error "Expected: $LLVM_KEY_FP"
22+
print_error "Got: $ACTUAL_FP"
23+
rm -f "$TMPKEY"
24+
exit 1
25+
fi
26+
sudo gpg --dearmor -o "$LLVM_KEYRING" <"$TMPKEY"
27+
rm -f "$TMPKEY"
28+
29+
CODENAME=$(lsb_release -cs)
30+
echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
31+
sudo apt-get update -q=2
32+
}
33+
1234
case "$MODE" in
1335
sdl2)
1436
if [ "$OS_TYPE" = "Linux" ]; then
@@ -33,35 +55,25 @@ format)
3355
fi
3456
sudo apt-get update -q=2
3557
sudo apt-get install -y -q=2 --no-install-recommends shfmt python3-pip gnupg ca-certificates lsb-release
36-
37-
# Install clang-format-20 from LLVM repository
38-
# LLVM signing key fingerprint: 6084F3CF814B57C1CF12EFD515CF4D18AF4F7421
39-
LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg
40-
LLVM_KEY_FP="6084F3CF814B57C1CF12EFD515CF4D18AF4F7421"
41-
TMPKEY=$(mktemp)
42-
download_to_file https://apt.llvm.org/llvm-snapshot.gpg.key "$TMPKEY"
43-
ACTUAL_FP=$(gpg --with-fingerprint --with-colons "$TMPKEY" 2>/dev/null | grep fpr | head -1 | cut -d: -f10)
44-
if [ "$ACTUAL_FP" != "$LLVM_KEY_FP" ]; then
45-
print_error "LLVM key fingerprint mismatch!"
46-
print_error "Expected: $LLVM_KEY_FP"
47-
print_error "Got: $ACTUAL_FP"
48-
rm -f "$TMPKEY"
49-
exit 1
50-
fi
51-
sudo gpg --dearmor -o "$LLVM_KEYRING" <"$TMPKEY"
52-
rm -f "$TMPKEY"
53-
54-
CODENAME=$(lsb_release -cs)
55-
echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
56-
sudo apt-get update -q=2
58+
setup_llvm_repo
5759
sudo apt-get install -y -q=2 --no-install-recommends clang-format-20
5860

5961
# Install Python formatter (version-pinned for reproducibility)
6062
pip3 install --break-system-packages --only-binary=:all: black==25.1.0
6163
;;
64+
analysis)
65+
if [ "$OS_TYPE" != "Linux" ]; then
66+
print_error "Static analysis only supported on Linux"
67+
exit 1
68+
fi
69+
sudo apt-get update -q=2
70+
sudo apt-get install -y -q=2 --no-install-recommends gnupg ca-certificates lsb-release libsdl2-dev
71+
setup_llvm_repo
72+
sudo apt-get install -y -q=2 --no-install-recommends clang-20 clang-tools-20
73+
;;
6274
*)
6375
print_error "Unknown mode: $MODE"
64-
echo "Usage: $0 [sdl2|headless|format]"
76+
echo "Usage: $0 [sdl2|headless|format|analysis]"
6577
exit 1
6678
;;
6779
esac

.github/workflows/main.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,43 @@ jobs:
6060
- name: Check code formatting
6161
run: .ci/check-format.sh
6262

63+
static-analysis:
64+
needs: [detect-code-related-file-changes]
65+
if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true'
66+
timeout-minutes: 15
67+
runs-on: ubuntu-24.04
68+
steps:
69+
- uses: actions/checkout@v6
70+
- name: Install dependencies
71+
run: .ci/install-deps.sh analysis
72+
- name: scan-build
73+
run: |
74+
make defconfig
75+
make clean
76+
scan-build-20 --status-bugs \
77+
-o /tmp/scan-build-report \
78+
make -j$(nproc)
79+
- name: Upload scan-build report
80+
if: failure()
81+
uses: actions/upload-artifact@v7
82+
with:
83+
name: scan-build-report
84+
path: /tmp/scan-build-report
85+
retention-days: 7
86+
if-no-files-found: ignore
87+
- name: scan-build (alpha checkers, informational)
88+
if: always()
89+
continue-on-error: true
90+
run: |
91+
make clean
92+
scan-build-20 \
93+
-enable-checker alpha.deadcode.UnreachableCode \
94+
-enable-checker alpha.unix.cstring.OutOfBounds \
95+
-o /tmp/scan-build-alpha-report \
96+
make -j$(nproc) 2>&1 | tee /tmp/alpha-output.txt
97+
WARNINGS=$(grep -c "warning:" /tmp/alpha-output.txt || true)
98+
echo "::notice::Alpha checkers found $WARNINGS warnings (informational only)"
99+
63100
unit-tests:
64101
needs: [detect-code-related-file-changes]
65102
if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true'

include/iui.h

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -913,8 +913,8 @@ void iui_segmented(iui_context *ctx,
913913
*/
914914
void iui_slider(iui_context *ctx,
915915
const char *label,
916-
float min,
917-
float max,
916+
float min_value,
917+
float max_value,
918918
float step,
919919
float *value,
920920
const char *fmt);
@@ -2031,7 +2031,7 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state);
20312031
* @ctx: current UI context
20322032
* @state: table state from iui_table_begin
20332033
*/
2034-
void iui_table_end(iui_context *ctx, iui_table_state *state);
2034+
void iui_table_end(iui_context *ctx, const iui_table_state *state);
20352035

20362036
/* Scrollable Container
20372037
* Creates a scrollable viewport using the existing clip stack.
@@ -2189,7 +2189,7 @@ bool iui_nav_rail_item(iui_context *ctx,
21892189
int index);
21902190

21912191
/* End navigation rail rendering */
2192-
void iui_nav_rail_end(iui_context *ctx, iui_nav_rail_state *state);
2192+
void iui_nav_rail_end(iui_context *ctx, const iui_nav_rail_state *state);
21932193

21942194
/* Toggle rail expanded/collapsed state */
21952195
void iui_nav_rail_toggle(iui_nav_rail_state *state);
@@ -2227,7 +2227,7 @@ bool iui_nav_bar_item(iui_context *ctx,
22272227
int index);
22282228

22292229
/* End navigation bar rendering */
2230-
void iui_nav_bar_end(iui_context *ctx, iui_nav_bar_state *state);
2230+
void iui_nav_bar_end(iui_context *ctx, const iui_nav_bar_state *state);
22312231

22322232
/* Navigation Drawer Component
22332233
* Side panel navigation for larger screens
@@ -2267,7 +2267,7 @@ bool iui_nav_drawer_item(iui_context *ctx,
22672267
void iui_nav_drawer_divider(iui_context *ctx);
22682268

22692269
/* End navigation drawer rendering */
2270-
void iui_nav_drawer_end(iui_context *ctx, iui_nav_drawer_state *state);
2270+
void iui_nav_drawer_end(iui_context *ctx, const iui_nav_drawer_state *state);
22712271

22722272
/* Open/close drawer */
22732273
void iui_nav_drawer_open(iui_nav_drawer_state *state);
@@ -2339,7 +2339,8 @@ bool iui_bottom_sheet_begin(iui_context *ctx,
23392339
float screen_height);
23402340

23412341
/* End bottom sheet rendering */
2342-
void iui_bottom_sheet_end(iui_context *ctx, iui_bottom_sheet_state *state);
2342+
void iui_bottom_sheet_end(iui_context *ctx,
2343+
const iui_bottom_sheet_state *state);
23432344

23442345
/* Open/close bottom sheet */
23452346
void iui_bottom_sheet_open(iui_bottom_sheet_state *state);
@@ -2387,12 +2388,13 @@ bool iui_bottom_app_bar_action(iui_context *ctx,
23872388

23882389
/* Add FAB (right side). Returns true if clicked */
23892390
bool iui_bottom_app_bar_fab(iui_context *ctx,
2390-
iui_bottom_app_bar_state *state,
2391+
const iui_bottom_app_bar_state *state,
23912392
const char *icon,
23922393
iui_fab_size_t size);
23932394

23942395
/* End bottom app bar rendering */
2395-
void iui_bottom_app_bar_end(iui_context *ctx, iui_bottom_app_bar_state *state);
2396+
void iui_bottom_app_bar_end(iui_context *ctx,
2397+
const iui_bottom_app_bar_state *state);
23962398

23972399
/* Menu Component
23982400
* Vertical dropdown menu with support for icons, shortcuts, dividers, and

ports/headless.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ const uint32_t *iui_headless_get_framebuffer(iui_port_ctx *ctx)
710710
/*
711711
* Get framebuffer dimensions.
712712
*/
713-
void iui_headless_get_framebuffer_size(iui_port_ctx *ctx,
713+
void iui_headless_get_framebuffer_size(const iui_port_ctx *ctx,
714714
int *width,
715715
int *height)
716716
{
@@ -1165,7 +1165,7 @@ void iui_headless_process_shm_events(iui_port_ctx *ctx)
11651165
/* Process all pending events */
11661166
while (hdr->event_read_idx != hdr->event_write_idx) {
11671167
uint32_t idx = hdr->event_read_idx % IUI_SHM_EVENT_RING_SIZE;
1168-
iui_shm_event_t *ev = &events[idx];
1168+
const iui_shm_event_t *ev = &events[idx];
11691169

11701170
switch (ev->type) {
11711171
case IUI_SHM_EVENT_MOUSE_MOVE:

ports/headless.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void iui_headless_inject_text(iui_port_ctx *ctx, uint32_t codepoint);
8383
const uint32_t *iui_headless_get_framebuffer(iui_port_ctx *ctx);
8484

8585
/* Get framebuffer dimensions */
86-
void iui_headless_get_framebuffer_size(iui_port_ctx *ctx,
86+
void iui_headless_get_framebuffer_size(const iui_port_ctx *ctx,
8787
int *width,
8888
int *height);
8989

src/basic.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,10 @@ void iui_segmented(iui_context *ctx,
113113
uint32_t text_color = is_selected ? ctx->colors.on_secondary_container
114114
: ctx->colors.on_surface;
115115

116-
/* Draw checkmark icon on selected segment */
117-
float icon_size = IUI_SEGMENTED_ICON_SIZE,
118-
text_w = iui_get_text_width(ctx, entries[i]);
119-
120116
if (is_selected) {
121117
/* Calculate total content width: checkmark + gap + text */
118+
float icon_size = IUI_SEGMENTED_ICON_SIZE,
119+
text_w = iui_get_text_width(ctx, entries[i]);
122120
float gap = 8.f, content_width = icon_size + gap + text_w,
123121
content_x = seg_x + (seg_width - content_width) / 2.f,
124122
icon_cx = content_x + icon_size / 2.f,

src/container.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ bool iui_bottom_sheet_begin(iui_context *ctx,
655655
return true;
656656
}
657657

658-
void iui_bottom_sheet_end(iui_context *ctx, iui_bottom_sheet_state *state)
658+
void iui_bottom_sheet_end(iui_context *ctx, const iui_bottom_sheet_state *state)
659659
{
660660
if (!ctx || !state)
661661
return;
@@ -1186,7 +1186,7 @@ void iui_table_row_end(iui_context *ctx, iui_table_state *state)
11861186
0.f, ctx->colors.outline_variant, ctx->renderer.user);
11871187
}
11881188

1189-
void iui_table_end(iui_context *ctx, iui_table_state *state)
1189+
void iui_table_end(iui_context *ctx, const iui_table_state *state)
11901190
{
11911191
if (!ctx || !ctx->current_window || !state)
11921192
return;

src/core.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,7 +1515,7 @@ void iui_pop_layer(iui_context *ctx)
15151515
ctx->input_layer.current_z_order = 0;
15161516
} else {
15171517
/* Restore to previous stack entry */
1518-
iui_layer_entry_t *prev =
1518+
const iui_layer_entry_t *prev =
15191519
&ctx->input_layer.layer_stack[ctx->input_layer.layer_depth - 1];
15201520
ctx->input_layer.current_layer_id = prev->layer_id;
15211521
ctx->input_layer.current_z_order = prev->z_order;
@@ -1665,7 +1665,7 @@ void iui_register_textfield(iui_context *ctx, void *buffer)
16651665

16661666
/* Probe entire table until finding empty slot or duplicate */
16671667
for (int probe = 0; probe < IUI_MAX_TRACKED_TEXTFIELDS; probe++) {
1668-
void *cached = ctx->field_tracking.textfield_ids[idx];
1668+
const void *cached = ctx->field_tracking.textfield_ids[idx];
16691669
if (!cached) {
16701670
/* Found empty slot, insert here */
16711671
ctx->field_tracking.textfield_ids[idx] = buffer;
@@ -1715,7 +1715,7 @@ bool iui_textfield_is_registered(const iui_context *ctx, const void *buffer)
17151715

17161716
/* Probe entire table looking for match or empty slot */
17171717
for (int probe = 0; probe < IUI_MAX_TRACKED_TEXTFIELDS; probe++) {
1718-
void *cached = ctx->field_tracking.textfield_ids[idx];
1718+
const void *cached = ctx->field_tracking.textfield_ids[idx];
17191719
if (cached == buffer)
17201720
return true;
17211721
if (!cached)
@@ -1725,7 +1725,7 @@ bool iui_textfield_is_registered(const iui_context *ctx, const void *buffer)
17251725
return false;
17261726
}
17271727

1728-
bool iui_slider_is_registered(const iui_context *ctx, uint32_t slider_id)
1728+
static bool iui_slider_is_registered(const iui_context *ctx, uint32_t slider_id)
17291729
{
17301730
if (slider_id == 0)
17311731
return false;

src/fab.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ static bool iui_fab_internal(iui_context *ctx,
7777
iui_internal_draw_text(ctx, content_x, label_y, label, content_color);
7878
} else {
7979
/* Standard FAB: centered icon only */
80-
float icon_cx = x + fab_w * 0.5f;
81-
float icon_cy = center_y;
8280
if (icon) {
81+
float icon_cx = x + fab_w * 0.5f;
82+
float icon_cy = center_y;
8383
iui_draw_fab_icon(ctx, icon_cx, icon_cy, icon_size, icon,
8484
content_color);
8585
}

src/input.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,11 @@ static void textfield_move_left(const char *buffer,
459459
state->cursor = prev;
460460
}
461461
while (state->cursor > 0) {
462-
size_t prev = iui_utf8_prev(buffer, state->cursor);
463-
uint32_t cp = iui_utf8_decode(buffer, prev, len);
462+
size_t prev2 = iui_utf8_prev(buffer, state->cursor);
463+
uint32_t cp = iui_utf8_decode(buffer, prev2, len);
464464
if (!iui_utf8_is_word_char(cp))
465465
break;
466-
state->cursor = prev;
466+
state->cursor = prev2;
467467
}
468468
} else {
469469
state->cursor = iui_utf8_prev(buffer, state->cursor);

0 commit comments

Comments
 (0)