feat(extractor): exception-edge recall slice — nested try, ctor throw-points, typed catches #209
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/LambdaHandlerViewModel.cs \ | |
| frontend/roslyn/samples/AliasedSourceViewModel.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/SelfOwnedControlParts.cs \ | |
| frontend/roslyn/samples/ExternalRefSubscription.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 "OWN001" \ | |
| || { echo "FAIL: expected OWN001"; exit 1; } | |
| # P-004 tiering: CustomerViewModel subscribes to an INJECTED bus (a ctor | |
| # param of unknown lifetime). We cannot prove it outlives the view model, | |
| # so the leak is reported at WARNING level (an honest "possible leak"), | |
| # not a hard error — until lifetime/ownership modelling lands. | |
| echo "$out" | grep -qE "CustomerViewModel\.cs:[0-9]+: warning: \[OWN001\]" \ | |
| || { echo "FAIL: expected CustomerViewModel as a WARNING (injected source)"; exit 1; } | |
| echo "$out" | grep -q "injected dependency whose lifetime is unknown" \ | |
| || { echo "FAIL: expected the injected-source wording"; exit 1; } | |
| if echo "$out" | grep -q "OrdersViewModel.cs"; then | |
| echo "FAIL: disposed subscription wrongly reported"; exit 1 | |
| fi | |
| # a lambda handler has no stored delegate, so it can NEVER be `-=`'d — the | |
| # finding says so. (Same injected source as Customer -> also a warning.) | |
| echo "$out" | grep -qE "LambdaHandlerViewModel\.cs:[0-9]+: warning: \[OWN001\]" \ | |
| || { echo "FAIL: expected the lambda-handler subscription leak (warning)"; exit 1; } | |
| echo "$out" | grep -q "inline lambda it has no '-=' handle" \ | |
| || { echo "FAIL: expected the lambda no-handle wording"; exit 1; } | |
| # P-004 provenance: a local that ALIASES an injected source (var src = | |
| # _bus) is NOT method-bounded — it must warn, not be silently dropped. A | |
| # local the scope CONSTRUCTS (var owned = new Calc()) IS bounded -> silent. | |
| echo "$out" | grep -qE "AliasedSourceViewModel\.cs:[0-9]+: warning: \[OWN001\]" \ | |
| || { echo "FAIL: aliased-injected local should warn, not be dropped"; exit 1; } | |
| if echo "$out" | grep -q "owned.Changed"; then | |
| echo "FAIL: a locally-constructed publisher must be dropped"; 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 self-owned (extended): a field built indirectly via a `ref`/`out` | |
| # helper, or fetched as one of the control's own template parts | |
| # (GetTemplateChild), is owned just like a `new`'d field — both | |
| # subscriptions in SelfOwnedControlParts are collectable cycles -> silent. | |
| if echo "$out" | grep -q "SelfOwnedControlParts.cs"; then | |
| echo "FAIL: a self-owned (ref-built / template-part) subscription was wrongly reported"; exit 1 | |
| fi | |
| # P-004 (ref/out narrowing, Codex P2): a field populated by an EXTERNAL | |
| # class's ref method (not this class's own helper) is NOT self-owned — the | |
| # subscription must still be reported, not silently suppressed. | |
| echo "$out" | grep -qE "ExternalRefSubscription\.cs:[0-9]+: warning: \[OWN001\]" \ | |
| || { echo "FAIL: expected OWN001 on the external-ref subscription (must not be exempted)"; exit 1; } | |
| # 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`/`for` 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; } | |
| echo "$out" | grep -qE "OWN001.*'forLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the undisposed local in a for loop"; exit 1; } | |
| # `try`/`finally` lowered with exception edges (try-methods no longer skipped): | |
| # a local never disposed inside a try is caught... | |
| echo "$out" | grep -qE "OWN001.*'tfLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the undisposed local in a try-method"; exit 1; } | |
| # ...and so is dispose-not-called-on-throw: `dot` is disposed inside the try | |
| # after a may-throw call, so it leaks on the exceptional path (matches CodeQL). | |
| echo "$out" | grep -qE "OWN001.*'dot'" \ | |
| || { echo "FAIL: expected OWN001 on the dispose-not-called-on-throw local"; exit 1; } | |
| # exception-edge RECALL slice — three sound recall wins, each matching CodeQL's | |
| # cs/dispose-not-called-on-throw: a may-throw in a nested `if` branch BEFORE the | |
| # dispose ('nestedLeak'); a constructor (`new`) as a throw point that skips a PRIOR | |
| # owned resource's dispose ('ctorPrior'); and a TYPED catch whose uncaught exception | |
| # types propagate past a post-try dispose ('typedLeak'). | |
| echo "$out" | grep -qE "OWN001.*'nestedLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the nested-throw leak"; exit 1; } | |
| echo "$out" | grep -qE "OWN001.*'ctorPrior'" \ | |
| || { echo "FAIL: expected OWN001 on the constructor-throw prior-resource leak"; exit 1; } | |
| echo "$out" | grep -qE "OWN001.*'typedLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the typed-catch uncaught-path leak"; exit 1; } | |
| # ...and a qualified DOMAIN catch (`catch (DomainErrors.Exception)` — rightmost name | |
| # `Exception` but NOT System.Exception) is typed too, so its uncaught types leak | |
| # ('qualLeak'); IsCatchAll matches only the canonical spellings (CodeRabbit review). | |
| echo "$out" | grep -qE "OWN001.*'qualLeak'" \ | |
| || { echo "FAIL: expected OWN001 on the qualified-typed-catch leak"; exit 1; } | |
| # dispose-optional (Task), disposed/escaping locals, a `for` loop whose | |
| # disposable is disposed after it (`looped`, balanced), a balanced | |
| # acquire+dispose in a loop (`whileClean`), a try/finally dispose (`tfClean`, | |
| # balanced) and a catch-disposes method (`tfCatch`, soundly skipped) must | |
| # stay silent: | |
| # released via `await x.DisposeAsync()` (asyncDisposed) and the chained | |
| # `.ConfigureAwait(false)` form (asyncDisposedCfg) -> both must stay silent. | |
| # PR #32 FP fixes: a swallowing catch with a Dispose AFTER the try/catch (cda), | |
| # an `await DisposeAsync().ConfigureAwait(false)` INSIDE a try (daci), and a Dispose | |
| # inside both branches of an `if` in a try alongside a may-throw call (cif) — all | |
| # disposed on every path, so all must stay silent (were false OWN001 before). | |
| # `ctorLater` is acquired AFTER the constructor-throw edge in CtorThrowLeaksPrior, so | |
| # it is never live at that edge and must stay silent (only `ctorPrior` leaks there). | |
| # `lamPrior`: a `new` inside a LAMBDA body is deferred (runs on invoke, not at the | |
| # declaration), so the lambda statement is not a throw point -> no phantom edge skips | |
| # its post-try dispose -> silent (Codex review: don't descend into lambda bodies). | |
| for ok in clean looped esc exemptTask whileClean asyncDisposed asyncDisposedCfg tfClean tfCatch tfRet tfNull cda daci cif ctorLater lamPrior; 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/for, try/finally sequential, never-vs-every-path wording, dispose-optional exempt, beyond flat)" | |
| - name: Coverage summary (--stats) | |
| run: | | |
| # --stats prints a flow-locals coverage line to stderr and stamps the same | |
| # counts into the facts JSON: of the methods with a disposable local, how | |
| # many were flow-analysed vs honestly skipped (an unmodelled construct). | |
| cov=$(dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \ | |
| frontend/roslyn/samples/FlowLocalsSample.cs --flow-locals --stats \ | |
| -o "$RUNNER_TEMP/stats.json" 2>&1 >/dev/null) | |
| echo "$cov" | |
| echo "$cov" | grep -qE '^coverage: [0-9]+/[0-9]+ methods .* flow-analysed' \ | |
| || { echo "FAIL: expected a --stats coverage line on stderr"; exit 1; } | |
| # Parse the JSON (not a substring grep): assert the stats object exists, | |
| # all three counters are numbers, and the invariant holds — every method | |
| # with a disposable local is either flow-analysed or honestly skipped. | |
| jq -e '.stats as $s | |
| | ($s.methods_with_local | type == "number") | |
| and ($s.methods_flow_analysed | type == "number") | |
| and ($s.methods_skipped_unmodelled | type == "number") | |
| and ($s.methods_flow_analysed + $s.methods_skipped_unmodelled | |
| == $s.methods_with_local)' \ | |
| "$RUNNER_TEMP/stats.json" >/dev/null \ | |
| || { echo "FAIL: stats object missing / non-numeric / invariant violated"; | |
| cat "$RUNNER_TEMP/stats.json"; exit 1; } | |
| echo "OK: --stats coverage on stderr + valid stats object (invariant holds)" | |
| - 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 (severity tiering) | |
| run: | | |
| out=$(scripts/own-check.sh --format msbuild -- frontend/roslyn/samples) | |
| echo "--- diagnostics ---"; echo "$out"; echo "-------------------" | |
| # P-004 tiering at the default severity, both sides: an injected-source | |
| # subscription (CustomerViewModel's `bus` is a ctor param of unknown | |
| # lifetime) renders as a WARNING, while a provable leak — the started, | |
| # never-stopped timer — stays an ERROR. | |
| echo "$out" | grep -qE "CustomerViewModel\.cs\([0-9]+\): warning OWN001:" \ | |
| || { echo "FAIL: expected CustomerViewModel as a warning (injected source)"; exit 1; } | |
| echo "$out" | grep -qE "TimerViewModel\.cs\([0-9]+\): error OWN001:" \ | |
| || { echo "FAIL: expected the timer leak to stay an error"; 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" | |