Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
python audit/aggregate/normalize.py --selftest
python audit/aggregate/score.py --selftest
python audit/aggregate/report.py --selftest
python audit/static/tools/xaml_check.py --selftest
python audit/static/run_static.py --selftest
python audit/runtime/ingest.py --selftest

Expand Down
14 changes: 13 additions & 1 deletion audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ audit/
tools/
owncheck.py # build-free runner: own-check.sh --format sarif (needs dotnet)
codeql.sh # build-free runner: CodeQL build-mode=none, security-and-quality
xaml_check.py # build-free runner: markup-only XAML perf/lifetime pass (stdlib XML, no SDK)
roslyn_pack.ps1 # build-required runner (local Windows): NetAnalyzers/Roslynator/...
infersharp.sh # build-required runner: Infer# over built binaries
inject/ # OwnAudit.Directory.Build.props/.targets (analyzer injection, gated)
Expand All @@ -65,7 +66,7 @@ audit/

| Tier | Tools | Needs a successful build of the target? |
|---|---|---|
| **build-free** | own-check, CodeQL (`build-mode: none`) | no — works on a solution that does not compile |
| **build-free** | own-check, CodeQL (`build-mode: none`), XAML markup pass | no — works on a solution that does not compile |
| **build-required** | Roslyn analyzer packs, Infer# | yes |

The entire audit of the target runs on a **local Windows machine** (VS Build Tools
Expand Down Expand Up @@ -108,6 +109,7 @@ Linux CI:
python audit/aggregate/normalize.py --selftest
python audit/aggregate/score.py --selftest
python audit/aggregate/report.py --selftest
python audit/static/tools/xaml_check.py --selftest # XAML rules + line preservation + SARIF round-trip
python audit/static/run_static.py --selftest # full pipeline end-to-end on fixtures
```

Expand All @@ -122,6 +124,16 @@ python audit/static/run_static.py --selftest # full pipeline end-to-end on fix
to the coverage ledger), DevExpress baseline-suppress, cross-tool agreement
scoring, the pain heatmap, **all four renderers (markdown / json / merged SARIF /
HTML)**, the analyzer-injection props/targets, and selftests.
- **XAML analyzer (Phase 1, markup-only) — done:** a build-free, stdlib-XML pass
(`static/tools/xaml_check.py`) feeding the same pipeline as a second fact source —
line-preserving parse, the canonical SARIF record, and rules XAML101/102/103/104/
106/107/108/109/110/111/112/113 (virtualization-off, per-keystroke binding, template
complexity, Freezable/x:Shared/DynamicResource/merged-dictionary perf, image
decode-at-full-size, LayoutTransform cost, TemplateBinding opportunities, and inline
Freezable duplication). This makes category 8 (broken virtualization) statically
covered, not NO-TOOL. Design + the full rule catalogue, phasing, and the Phase-2
binding-path join: [`../docs/notes/xaml-analyzer-design.md`](../docs/notes/xaml-analyzer-design.md).
Phase 2 (Roslyn-linked XAML2xx) and Phase 3 (runtime correlation) are deferred.
- **Runtime (Phase 2) — started:** the runtime→pipeline bridge (`runtime/ingest.py`,
CI-gated), the leak-harness scenario schema + one scenario, runtime rule mappings
in the taxonomy (categories 2/3/4/11), and the C# leak-harness skeleton. See
Expand Down
8 changes: 6 additions & 2 deletions audit/config/profiles/desktop-wpf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ tiers:
build_free:
- own-check # error-tolerant SemanticModel; works on a broken solution
- codeql # build-mode: none, security-and-quality suite
- xaml # markup-only XAML pass (stdlib XML, no SDK needed);
# XAML perf/lifetime rules — docs/notes/xaml-analyzer-design.md
build_required:
- roslyn-pack # NetAnalyzers, Meziantou, Roslynator, AsyncFixer,
# SonarAnalyzer, IDisposableAnalyzers, WpfAnalyzers,
Expand All @@ -43,9 +45,11 @@ roslyn_packs:
no_tool_static:
- 4 # DependencyPropertyDescriptor.AddValueChanged leak -> runtime leak-harness
- 6 # PropertyChanged storms / expensive getters -> runtime
# (XAML108 gives a static per-keystroke-binding suspicion; storms stay runtime)
- 7 # WPF binding errors -> runtime
- 8 # broken/disabled virtualization -> runtime
- 10 # allocations in converters/getters -> runtime
# NOTE: category 8 (broken/disabled virtualization) is no longer NO-TOOL:
# the build-free XAML pass covers it statically (XAML107/XAML109).
- 10 # allocations in converters/getters -> runtime (XAML phase 2)
- 11 # duplicated immutable data (the project's "gold") -> runtime
- 12 # heavy reference data / LOH / Gen2 bloat -> runtime
- 13 # cross-thread ObjectDisposedException / INPC -> runtime
30 changes: 30 additions & 0 deletions audit/static/run_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from owncheck import run_own_check # noqa: E402
from report import render_html, render_json, render_markdown, render_sarif # noqa: E402
from score import score # noqa: E402
from xaml_check import run_xaml_check # noqa: E402

try:
from oracle_compare import parse_sarif
Expand Down Expand Up @@ -123,6 +124,14 @@ def run(target: str, profile: dict[str, Any], out_dir: Path, target_name: str =
tiers.append(st)
if st["available"] and st["sarif"]:
sarif_inputs.append(("codeql", st["sarif"]))
if "xaml" in build_free:
# The markup-only XAML pass (docs/notes/xaml-analyzer-design.md, phase 1):
# pure stdlib XML, so it has no toolchain prerequisite and always runs here,
# emitting the same SARIF record into the same aggregate pipeline.
st = run_xaml_check(target, out_dir)
tiers.append(st)
if st["available"] and st["sarif"]:
sarif_inputs.append(("xaml", st["sarif"]))

# Pick up any build-required SARIFs already dropped here by the Windows runners.
# Roslyn writes ONE SARIF PER PROJECT under roslyn/ (see the injected props's
Expand Down Expand Up @@ -305,6 +314,27 @@ def check(ok: bool, msg: str) -> None: # total derives from the call count
check(res2["totals"]["high_confidence"] >= 1,
"runtime leak + static finding in one file must form a high-confidence cluster")

# The build-free XAML tier must wire in like own-check/codeql: with "xaml" in
# build_free and a .xaml under the target, run() reports a xaml tier and the
# markup finding rides the pipeline through to a scored cluster — all on Linux,
# no SDK (the whole point of the markup-only phase).
with tempfile.TemporaryDirectory() as td3:
out3 = Path(td3) / "out"
src3 = Path(td3) / "src" / "Views"
src3.mkdir(parents=True)
(src3 / "Main.xaml").write_text(
'<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n'
' xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">\n'
' <ListBox VirtualizingStackPanel.IsVirtualizing="False" />\n'
'</UserControl>\n', encoding="utf-8")
profile3 = {"name": "t", "severity_floor": "warning",
"tiers": {"build_free": ["xaml"]}}
res3 = run(str(Path(td3) / "src"), profile3, out3, target_name="t/p")
check(any(t["tool"] == "xaml" and t["available"] for t in res3["tiers"]),
"xaml build-free tier must run and be reported by run()")
check(res3["totals"]["candidates"] >= 1,
"a XAML107 markup finding must flow through to a scored cluster")

fails = [c for c in checks if c]
for f in fails:
print(f"RUN_STATIC SELFTEST FAIL: {f}")
Expand Down
21 changes: 21 additions & 0 deletions audit/static/taxonomy/categories.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ rules:
# ── Category 9: WPF Freezable / per-instance brush-geometry (partial) ────────
"WPF0*": {category: 9, name: wpf-freezable} # WpfAnalyzers (subset)

# ── XAML analyzer (build-free markup pass) — docs/notes/xaml-analyzer-design.md.
# XAML is a SECOND fact source, not a parallel linter: each rule maps to one of
# the same Plan.md §2 categories so a markup finding rides the same fingerprint
# -> baseline -> ratchet path as a .cs finding. Exact ids win over the XAML1*
# glob, which is a safety net so a not-yet-mapped XAML rule never lands in
# `uncategorized` (the design note's "nothing quietly falls through").
"XAML107": {category: 8, name: broken-virtualization} # virtualization disabled
"XAML109": {category: 8, name: template-complexity} # visual-tree inflation
"XAML108": {category: 6, name: binding-update-frequency} # per-keystroke source flood
"XAML106": {category: 9, name: wpf-freezable} # Freezable not frozen
"XAML101": {category: 9, name: per-instance-resource} # duplicate converter
"XAML102": {category: 9, name: dynamic-resource-misuse} # DynamicResource for a static key
"XAML103": {category: 9, name: per-instance-resource} # x:Shared=False
"XAML104": {category: 9, name: merged-dictionary-waste} # duplicate merged dict
"XAML110": {category: 9, name: image-decode} # full-size thumbnail decode
"XAML111": {category: 8, name: layout-cost} # LayoutTransform layout pass
"XAML112": {category: 9, name: template-binding-opportunity} # cheaper compiled binding
"XAML113": {category: 9, name: per-instance-resource} # duplicated inline freezable
"XAML1*": {category: 9, name: xaml-markup-perf} # safety net for future rules

# ── Category 14: general bugs / perf / best-practice / async ──────────────────
"CA1*": {category: 14, name: general-quality} # NetAnalyzers design/perf
"CA2*": {category: 14, name: general-quality} # (CA2000/CA2213 above win by exactness)
Expand Down Expand Up @@ -78,6 +98,7 @@ category_severity:
4: P1 # DependencyPropertyDescriptor.AddValueChanged leak (runtime-confirmed)
5: P2
6: P2 # PropertyChanged storms/cascades — runtime raise-frequency, perf-tier
8: P2 # broken/disabled virtualization — now statically covered by the XAML pass
9: P2
11: P2 # duplicated immutable data — memory bloat (the project's "gold"), perf-tier
14: P2
Expand Down
Loading
Loading