fix(oracle): count Infer# PULSE_RESOURCE_LEAK + product-only scoping (--exclude-tests) #144
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 | |
| # The evaluation scripts (corpus miner, cross-tool oracle diff) carry | |
| # embedded fixtures; run their selftests here so the parsers/aggregators | |
| # stay honest on every push, not only on workflow_dispatch. | |
| - name: script selftests (miner + oracle) | |
| run: | | |
| python scripts/mine_report.py --selftest | |
| python scripts/oracle_compare.py --selftest | |
| 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 | |
| # a static IDisposable field is a process-lifetime singleton (Dapper's | |
| # DisposedReader.Instance) — never an owned leak, so it stays silent. | |
| if echo "$out" | grep -q "SharedTokenHolder"; then | |
| echo "FAIL: a static singleton IDisposable field was 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" | |
| - name: Flow-sensitive local IDisposables (--flow-locals, P-016 B0b/B2) | |
| run: | | |
| # Path-sensitive flow analysis of local IDisposables — bugs the flat D1 | |
| # detector cannot catch (use-after-dispose, double-dispose, leak-on-path). | |
| dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \ | |
| frontend/roslyn/samples/FlowLocalsSample.cs --flow-locals -o "$RUNNER_TEMP/flow.json" | |
| out=$(python -m ownlang ownir "$RUNNER_TEMP/flow.json" || true) | |
| echo "$out" | |
| echo "$out" | grep -q "OWN002" || { echo "FAIL: expected OWN002 (use-after-dispose)"; exit 1; } | |
| echo "$out" | grep -q "OWN001" || { echo "FAIL: expected OWN001 (leak on a path)"; exit 1; } | |
| echo "$out" | grep -q "OWN003" || { echo "FAIL: expected OWN003 (double-dispose)"; exit 1; } | |
| # a real Timer leak the flat curated allowlist misses but the semantic path catches: | |
| echo "$out" | grep -q "OWN001.*'realTimer'" || { echo "FAIL: expected OWN001 on the leaked Timer"; exit 1; } | |
| # the OWN001 wording splits on whether the local was released anywhere: the | |
| # Timer is released on no path -> "is never disposed"; LeakOnElse's `leak` is | |
| # released on the then-branch only -> "may not be disposed on every path". | |
| echo "$out" | grep -qE "'realTimer' is never disposed" \ | |
| || { echo "FAIL: expected the never-disposed wording for the 0-release Timer"; exit 1; } | |
| echo "$out" | grep -qE "'leak' may not be disposed on every path" \ | |
| || { echo "FAIL: expected the partial-path wording for LeakOnElse"; exit 1; } | |
| # P-016 A1 reached the frontend: `while`/`foreach` bodies are now lowered | |
| # (not skipped), so a per-iteration leak in one is caught. | |
| echo "$out" | grep -qE "OWN001.*'whileLeak' is never disposed" \ | |
| || { echo "FAIL: expected OWN001 on the undisposed local in a while loop"; exit 1; } | |
| echo "$out" | grep -qE "OWN001.*'foreachLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the undisposed local in a foreach loop"; exit 1; } | |
| # dispose-optional (Task), disposed/escaping locals, a `for` loop (still | |
| # skipped: `looped`), and a balanced acquire+dispose in a loop (`whileClean`) | |
| # must stay silent: | |
| # released via `await x.DisposeAsync()` (asyncDisposed) and the chained | |
| # `.ConfigureAwait(false)` form (asyncDisposedCfg) -> both must stay silent. | |
| for ok in clean looped esc exemptTask whileClean asyncDisposed asyncDisposedCfg; do | |
| if echo "$out" | grep -q "'$ok'"; then echo "FAIL: silent/exempt case '$ok' was reported"; exit 1; fi | |
| done | |
| echo "OK: flow-sensitive OWN001/002/003 on real C# (path-sensitive, loops via while/foreach, never-vs-every-path wording, dispose-optional exempt, beyond flat)" | |
| - name: Escape-via-projection leak — GTM UnitOfWork (--flow-locals, P-016 B0b/B2) | |
| run: | | |
| # A real GTM leak the flat detector misses: a UnitOfWork (IDisposable) used | |
| # ONLY through member access to build a returned DEFERRED IQueryable. The | |
| # bare `uow` never escapes, so it stays tracked and is disposed on no path | |
| # -> OWN001. Crucially NOT fixable by a naive `using` (the deferred query | |
| # would run after dispose) — the `using var`+materialize fix (uowFixed) is | |
| # the one that must stay silent. | |
| dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \ | |
| frontend/roslyn/samples/UnitOfWorkFlowSample.cs --flow-locals -o "$RUNNER_TEMP/uow.json" | |
| out=$(python -m ownlang ownir "$RUNNER_TEMP/uow.json" || true) | |
| echo "$out" | |
| # `uow` is released on no path, so the OWN001 reads "is never disposed". | |
| echo "$out" | grep -qE "OWN001.*'uow' is never disposed" \ | |
| || { echo "FAIL: expected OWN001 'is never disposed' on UnitOfWork 'uow'"; exit 1; } | |
| echo "$out" | grep -q "UnitOfWorkFlowSample.cs" \ | |
| || { echo "FAIL: expected the finding at the C# sample location"; exit 1; } | |
| # the three correct fixes must stay silent: materialize inside `using` | |
| # (uowFixed), and ownership TRANSFERRED to the caller — returned (uowOwned) | |
| # or moved out as an argument (uowMoved). | |
| for ok in uowFixed uowOwned uowMoved; do | |
| if echo "$out" | grep -q "'$ok'"; then | |
| echo "FAIL: a correct fix ('$ok') was wrongly reported as a leak"; exit 1 | |
| fi | |
| done | |
| echo "OK: escape-via-projection UnitOfWork leak -> OWN001 'never disposed'; materialize + ownership-transfer fixes stay silent" | |
| # 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" | |