Home · Case studies · Tools

UI Linter, Widget Blueprint Static Analysis

A three-pillar UI-linting system for Unreal Widget Blueprints, a bug-DNA knowledge base mined from real Jira, an AI-driven pre-merge checklist generator, and an in-Editor deterministic linter. Rules come from bugs the team actually shipped, not opinions about what could go wrong.

Role

Designer, engineer, owner (solo build, AI-assisted)

Team

Halo Studios · UI

Technical Stack

Unreal Engine · C++ plugin · Python · pytest · Copilot CLI

Timeframe

2025 · in active use until departure

22

Deterministic rules, 14 widget-tree + 8 event-graph

100+ → 17

Real Jira bugs mined into 17 bug-DNA patterns across 6 categories

34

Layout bugs eliminated in the first ~2 months post-adoption

Team-wide

Adopted across the UI team, review process changed to require lint output

At a glance

  • Two surfaces: an in-Editor toolbar (Lint This Widget) with click-to-widget navigation, and a CLI for pre-merge / Copilot-driven review.
  • Rules are derived from a Jira-mined pattern library, every rule ties back to a bug someone actually shipped.
  • Adoption changed the review process itself: pull requests started including a linter screenshot as an evidence artifact.
Detail below

Prefer the visual tour? See the tool page for the editor UI, Message Log output, and gallery: UI Linter, project page →

The problem

The same shapes of UI bug kept showing up on different screens, and reviewers had no way to catch all of them consistently.

Widget Blueprints are easy to author and hard to audit. Across the project, the same defect patterns kept surfacing on different screens: text that clipped once the large-text accessibility setting was on; localization overlap in longer-worded languages; hidden widgets still accepting focus; delegate bindings never unbound; inconsistent style tokens between screens built by external partners. Individually small, together they added up to a steady drip of Jira tickets.

Code review couldn't scale to catch them. Reviewers didn't have time to open every widget, simulate every text scale, walk every event-graph binding. Rare edge cases (large text, long locales) were the ones falling through, exactly the ones a human reviewer is worst at.

Standards were understood. They weren't enforced. Nothing about that is a quality problem, it's a scaling problem.

Constraints

Two surfaces

In-Editor for authors while they build, and CLI for pre-merge automation and Copilot-driven review. Same ruleset, same output.

Zero authoring friction

If it slows down save, PIE, or hot reload, adoption dies. Findings must be one click away, and one click away from the offending widget.

Grounded in real bugs

Every rule must trace back to at least one bug the team actually shipped. Rules from opinion don't survive review pushback.

Extensible by anyone

Rules ship as data. Anyone should be able to add a rule when a new pattern shows up in Jira, without a plugin rebuild.

The key idea

Mine the bug database. Turn recurring patterns into deterministic checks. Surface them in the same place authors work.

The system is three cooperating pillars: a bug-DNA knowledge base mined from 100+ Jira tickets into 17 patterns across 6 categories; an AI-driven checklist generator that consumes widget-tree JSON and produces context-aware review checklists; and a deterministic linter (22 rules covering both the widget tree and the event graph) that runs inside the Editor and as a CLI. The three share the same knowledge base, so every deterministic rule and every checklist item is rooted in a real historical bug.

Widget Blueprint editor toolbar with Lint This Widget and Add New Rule buttons.
Editor entry points, Lint This Widget runs the full ruleset on the active WBP; Add New Rule launches Copilot with context to draft one.
Message Log window showing UI Linter category with dozens of tree and event-graph findings, severity icons, and clickable Fix links.
The Message Log after a single lint pass, findings surface with severity, plain-language fix suggestions, click-to-widget navigation for widget-tree hits, and click-to-node navigation for event-graph hits.

Production highlights

Architecture

C++ / Python split by strength

C++ owns Editor integration, traversal, and asset resolution (via a shared UIPythonCppBridge). Python owns rule authoring, tests, and CLI. Rules ship as JSON data, not builds.

Data

Six real bug categories

Layout/Scaling, Styling/Consistency, Text/Localization, Visibility/State, Input/Navigation, Data Binding, distilled from a Jira mining pass over the project's real defect history.

Adoption

Findings where authors work

Widget-tree hits jump to the offending widget in the Hierarchy panel; event-graph hits jump to the exact node. Fix suggestions include one-click Fix → Widgets actions for uncategorized variables and dead code.

AI-assist

A rule authoring loop, not a fixed set

Add New Rule launches Copilot CLI with the current widget as context, drafting a candidate rule from a real example. The 22-rule count grew over months as new patterns showed up in Jira.

The showcase rule, L001

L001, text-in-fixed-container, is the rule that made the case for the whole tool. It flags any text block placed inside a fixed-width container without an auto-size or wrap contract. The pattern comes straight from the Layout/Scaling category, and it's the exact class of bug that used to surface only after QA turned on large-text accessibility or switched to a longer locale.

In practice: a single lint pass on the pictured skulls-display widget above surfaces several L001 hits alongside binding-leak (L020) and dead-code (L026 / L027) findings. Each one is a bug that would previously have shipped to QA before anyone noticed.

What I'd carry forward