docs: add # P-016 — Deep C# fact extraction: CFG + flow lowering #86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | |