Skip to content

docs: add # P-016 — Deep C# fact extraction: CFG + flow lowering #86

docs: add # P-016 — Deep C# fact extraction: CFG + flow lowering

docs: add # P-016 — Deep C# fact extraction: CFG + flow lowering #86

Workflow file for this run

name: CI
# Least privilege: every job only reads the repo (no job pushes or needs write).
# Action SHA-pinning / persist-credentials hardening is deliberately deferred to
# a Dependabot/hardening pass — see README "где оно жульничает" item #7.
permissions:
contents: read
on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:
jobs:
# Quality gate: ruff (style/bugs) on the whole tree, and mypy --strict on the
# ownlang package (tests are dynamic/fuzzer code, covered by ruff only). These
# are the "tighten the screws on Python" guard rails — see README.
lint:
name: lint (ruff + mypy --strict)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install linters
run: pip install "ruff==0.15.8" "mypy==1.19.1"
- name: ruff
run: ruff check .
- name: mypy --strict (ownlang)
run: mypy
tests:
name: tests (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The PoC needs 3.11+ (see README). Run the floor and current releases.
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# Zero-dependency project: nothing to install. The suite runs the
# analyzer cases, the golden ArrayPool lowering, the codegen content
# assertions, and the property fuzzer (fixed seed) in one entrypoint.
- name: Run test suite
run: python tests/run_tests.py
# A heavier, non-blocking fuzz pass so a flake-free regression that only
# shows up on other random draws still gets surfaced on every push.
fuzz-extended:
name: extended codegen fuzz
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Property fuzz (50k draws, rotating seed)
run: python tests/test_codegen_props.py 50000 ${{ github.run_number }}
# Prove the lowering is real: take the generated C# and put it through the
# actual .NET compiler (the PoC sandbox has no SDK, so this is the only place
# the golden example is genuinely compiled and run, not "verified by
# construction").
dotnet-golden:
name: golden C# compiles & runs (.NET)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Check the emitted method is still in sync with the golden host
run: python examples/golden_arraypool/verify_emit.py
- name: Compile & run the generated C# with the real compiler
run: |
dotnet new console -o "$RUNNER_TEMP/golden_app"
cp examples/golden_arraypool/Program.cs "$RUNNER_TEMP/golden_app/Program.cs"
dotnet run --project "$RUNNER_TEMP/golden_app"
# P-001: prove the C# leak pipeline end-to-end on real C# — the Roslyn
# extractor turns sample .cs into OwnIR facts, and the core surfaces the
# subscription leak at its C# location (and stays silent on the disposed one).
wpf-extractor:
name: C# leak extractor (Roslyn) -> OwnIR -> core
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Extract OwnIR facts from sample C#
run: |
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
frontend/roslyn/samples/CustomerViewModel.cs \
frontend/roslyn/samples/OrdersViewModel.cs \
frontend/roslyn/samples/TimerViewModel.cs \
frontend/roslyn/samples/DisposableFieldViewModel.cs \
frontend/roslyn/samples/MessengerViewModel.cs \
frontend/roslyn/samples/PooledBufferSample.cs \
frontend/roslyn/samples/LocalDisposableSample.cs \
frontend/roslyn/samples/SelfOwnedViewModel.cs \
frontend/roslyn/samples/StaticHandlerViewModel.cs \
frontend/roslyn/samples/SampleTypes.cs \
-o "$RUNNER_TEMP/facts.json"
cat "$RUNNER_TEMP/facts.json"
- name: Check facts through the core
run: |
out=$(python -m ownlang ownir "$RUNNER_TEMP/facts.json" || true)
echo "$out"
echo "$out" | grep -q "CustomerViewModel.cs" \
|| { echo "FAIL: expected the CustomerViewModel leak"; exit 1; }
echo "$out" | grep -q "OWN001" \
|| { echo "FAIL: expected OWN001"; exit 1; }
if echo "$out" | grep -q "OrdersViewModel.cs"; then
echo "FAIL: disposed subscription wrongly reported"; exit 1
fi
# WPF002: the started, never-stopped timer leaks with a [resource: timer]
# tag; the timer stopped in Dispose stays silent.
echo "$out" | grep -q "TimerViewModel.cs" \
|| { echo "FAIL: expected the TimerViewModel timer leak"; exit 1; }
echo "$out" | grep -q "resource: timer" \
|| { echo "FAIL: expected a [resource: timer] tag"; exit 1; }
if echo "$out" | grep -q "CleanTimerViewModel"; then
echo "FAIL: stopped timer wrongly reported"; exit 1
fi
# WPF003: the IDisposable field the class new's but never disposes leaks
# with a [resource: disposable field] tag; the one disposed in Dispose
# stays silent.
echo "$out" | grep -q "DisposableFieldViewModel.cs" \
|| { echo "FAIL: expected the ReportViewModel field leak"; exit 1; }
echo "$out" | grep -q "resource: disposable field" \
|| { echo "FAIL: expected a [resource: disposable field] tag"; exit 1; }
if echo "$out" | grep -q "CleanReportViewModel"; then
echo "FAIL: disposed field wrongly reported"; exit 1
fi
# WPF004: an ignored `X.Subscribe(...)` result leaks; the captured+
# disposed one stays silent. "ignored" is unique to the WPF004 message.
echo "$out" | grep -q "MessengerViewModel.cs" \
|| { echo "FAIL: expected the InboxViewModel ignored-Subscribe leak"; exit 1; }
echo "$out" | grep -q "is ignored" \
|| { echo "FAIL: expected the ignored-Subscribe message"; exit 1; }
if echo "$out" | grep -q "CleanInboxViewModel"; then
echo "FAIL: captured+disposed subscription wrongly reported"; exit 1
fi
# POOL001: a Rent'd-but-never-Return'd buffer leaks; the rent+return
# (finally) one stays silent.
echo "$out" | grep -q "pooled buffer 'leaky'" \
|| { echo "FAIL: expected the rented-not-returned buffer leak"; exit 1; }
if echo "$out" | grep -q "pooled buffer 'ok'"; then
echo "FAIL: returned buffer wrongly reported"; exit 1
fi
# P-005 D1: a `new`'d local IDisposable never disposed leaks; a `using`
# one and a returned (transferred) one stay silent.
echo "$out" | grep -q "local IDisposable 'leaky'" \
|| { echo "FAIL: expected the undisposed-local leak"; exit 1; }
echo "$out" | grep -q "LocalDisposableSample.cs" \
|| { echo "FAIL: expected LocalDisposableSample.cs in the local-disposable finding"; exit 1; }
echo "$out" | grep -q "resource: disposable]" \
|| { echo "FAIL: expected a [resource: disposable] tag"; exit 1; }
if echo "$out" | grep -qE "'guarded'|'moved'"; then
echo "FAIL: using/returned local wrongly reported"; exit 1
fi
# P-004 self-owned exemption: a subscription whose source is a field the
# class constructs (owns) is a GC-collectable cycle, not a leak — silent.
if echo "$out" | grep -q "SelfOwnedViewModel.cs"; then
echo "FAIL: a self-owned subscription was wrongly reported"; exit 1
fi
# P-004 static-handler exemption: a static-method handler has a null
# delegate target — no instance retained, so not a leak — silent.
if echo "$out" | grep -q "StaticHandlerViewModel.cs"; then
echo "FAIL: a static-handler subscription was wrongly reported"; exit 1
fi
echo "OK: real C# -> facts -> OWN001 (subscription + timer + field + Subscribe + pool + local) at the C# location"
# The distribution surface (Уровень 1): the own-check.sh orchestrator walks a
# directory of real C# and prints findings in the host-parseable formats the
# GitHub Action (PR annotations) and a VS Error List (MSBuild) consume — and
# the composite action itself runs end-to-end. One checker: the script just
# chains the extractor and the Python core.
own-check-surface:
name: own-check repo scan (github + msbuild) + composite action
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: GitHub-annotation format over the sample tree (directory walk)
run: |
# stdout (captured) carries only the annotations; the extractor's build
# chatter and any error flow to stderr -> the job log (never muted).
out=$(scripts/own-check.sh --format github -- frontend/roslyn/samples)
echo "--- annotations ---"; echo "$out"; echo "-------------------"
echo "$out" | grep -q "^::error " \
|| { echo "FAIL: expected a ::error annotation"; exit 1; }
echo "$out" | grep -q "frontend/roslyn/samples/CustomerViewModel.cs" \
|| { echo "FAIL: expected the relative path to the Customer leak"; exit 1; }
echo "$out" | grep -q "title=OWN001" \
|| { echo "FAIL: expected the OWN001 title in the annotation"; exit 1; }
- name: MSBuild diagnostic format over the sample tree
run: |
out=$(scripts/own-check.sh --format msbuild -- frontend/roslyn/samples)
echo "--- diagnostics ---"; echo "$out"; echo "-------------------"
echo "$out" | grep -qE "CustomerViewModel\.cs\([0-9]+\): error OWN001:" \
|| { echo "FAIL: expected an MSBuild-format error line"; exit 1; }
- name: --severity warning renders advisory diagnostics
run: |
out=$(scripts/own-check.sh --format msbuild --severity warning -- frontend/roslyn/samples)
echo "$out"
echo "$out" | grep -qE "CustomerViewModel\.cs\([0-9]+\): warning OWN001:" \
|| { echo "FAIL: expected an MSBuild-format warning line"; exit 1; }
if echo "$out" | grep -qE ": error OWN001:"; then
echo "FAIL: --severity warning should not emit error-level lines"; exit 1
fi
- name: --fail-on-finding propagates the core's exit code
run: |
if scripts/own-check.sh --fail-on-finding -- frontend/roslyn/samples >/dev/null 2>&1; then
echo "FAIL: a tree with leaks should exit non-zero under --fail-on-finding"; exit 1
fi
echo "OK: --fail-on-finding surfaced the leaks as a non-zero exit"
- name: The composite action runs end-to-end (non-failing)
uses: ./
with:
path: frontend/roslyn/samples
format: github
fail-on-finding: "false"