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
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,70 @@ jobs:
sarif_file: ${{ steps.own.outputs.sarif-file }}
category: own-net-samples

# P-014 Tier B: external-reference resolution. The SAME sample, run two ways, must give two
# verdicts — proving the extractor binds a THIRD-PARTY event only when its DLL is referenced:
# A (no refs) -> ObservableObject is an error type -> OWN050 (honest skip), no leak
# B (--ref-dir DLL) -> PropertyChanged binds to an IEventSymbol -> real OWN001 leak, no OWN050
# The package DLL is fetched from nuget (a .nupkg is a zip) and pinned to a version known to
# expose the event; Roslyn reads its metadata only (no build, no source, no .NET Framework needed).
tier-b-refs:
name: P-014 Tier B — external reference resolution (--ref-dir)
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: Materialize a third-party reference (CommunityToolkit.Mvvm 8.2.2, pinned)
run: |
mkdir -p "$RUNNER_TEMP/refdir"
curl -sSL --retry 3 --max-time 120 -o "$RUNNER_TEMP/ct.nupkg" \
"https://api.nuget.org/v3-flatcontainer/communitytoolkit.mvvm/8.2.2/communitytoolkit.mvvm.8.2.2.nupkg"
# a .nupkg is a zip; lift just the netstandard2.0 assembly into the ref dir
unzip -o -j "$RUNNER_TEMP/ct.nupkg" "lib/netstandard2.0/CommunityToolkit.Mvvm.dll" -d "$RUNNER_TEMP/refdir"
test -s "$RUNNER_TEMP/refdir/CommunityToolkit.Mvvm.dll" \
|| { echo "FAIL: could not materialize CommunityToolkit.Mvvm.dll"; exit 1; }
# pre-flight: confirm the fixture DLL actually exposes the event the A/B test binds to —
# read its .NET metadata in pure Python (no runtime). A clear "fixture rotted" failure
# beats a confusing "OWN001 not found" if the package ever drops/renames the member.
pip install --quiet dnfile
python - <<'PY'
import os, dnfile
pe = dnfile.dnPE(os.path.join(os.environ["RUNNER_TEMP"], "refdir", "CommunityToolkit.Mvvm.dll"))
ev = getattr(pe.net.mdtables, "Event", None)
events = {str(r.Name) for r in ev.rows} if ev else set()
types = {f"{r.TypeNamespace}.{r.TypeName}" for r in pe.net.mdtables.TypeDef.rows}
assert "PropertyChanged" in events, f"fixture DLL missing PropertyChanged event; has {sorted(events)}"
assert "CommunityToolkit.Mvvm.ComponentModel.ObservableObject" in types, "fixture DLL missing ObservableObject"
print("pre-flight OK: ObservableObject + PropertyChanged present in fixture metadata")
PY
- name: A — without the reference, the external event is OWN050 (honest skip), not a leak
run: |
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
frontend/roslyn/samples/TierBSample.cs -o "$RUNNER_TEMP/a.json"
out=$(python -m ownlang ownir "$RUNNER_TEMP/a.json" || true)
echo "$out"
echo "$out" | grep -q "\[OWN050\]" \
|| { echo "FAIL(A): expected OWN050 — ObservableObject unresolved without --ref-dir"; exit 1; }
if echo "$out" | grep -q "\[OWN001\]"; then
echo "FAIL(A): must NOT guess a leak when the declaring type is unresolved"; exit 1
fi
- name: B — with --ref-dir, the event resolves to a real subscription leak (OWN001)
run: |
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
frontend/roslyn/samples/TierBSample.cs --ref-dir "$RUNNER_TEMP/refdir" -o "$RUNNER_TEMP/b.json"
out=$(python -m ownlang ownir "$RUNNER_TEMP/b.json" || true)
echo "$out"
echo "$out" | grep -q "\[OWN001\]" \
|| { echo "FAIL(B): expected OWN001 — PropertyChanged resolved via --ref-dir"; exit 1; }
if echo "$out" | grep -q "\[OWN050\]"; then
echo "FAIL(B): the external event must RESOLVE, not stay OWN050"; exit 1
fi
echo "OK: Tier B A/B — external event OWN050 (no ref) -> OWN001 (with --ref-dir)"

# P-012 slice 1: score the checker against the labeled corpus on REAL C# — not
# just the .own reduction tests/test_corpus.py checks. Per case: the bug must be
# CAUGHT in before.cs (recall) and the fix must be SILENT in after.cs
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,6 @@ own scan. Label them as estimates wherever they appear.
| [P-011](proposals/P-011-editor-tooling.md) | Editor tooling & syntax highlighting | side-track | draft |
| [P-012](proposals/P-012-bug-corpus-mining.md) | Real-world bug corpus & mining | enabling | in progress (corpus benchmark + real-world cases, CI-gated) |
| [P-013](proposals/P-013-distribution-surface.md) | Distribution surface (CI Action + dotnet tool) | enabling | v0 built |
| [P-014](proposals/P-014-semantic-resolution.md) | Project-local semantic resolution (`+=` event vs number) | P0 | in progress (Tier A shipped & default-on; Tier B deferred) |
| [P-014](proposals/P-014-semantic-resolution.md) | Project-local semantic resolution (`+=` event vs number) | P0 | in progress (Tier A default-on + Tier B light path `--ref-dir`; full MSBuild closure deferred) |
| [P-015](proposals/P-015-configuration-surface.md) | Configuration surface (check selection & severity) | P2 | draft (stub) |
| [P-016](proposals/P-016-deep-fact-extraction.md) | Deep C# fact extraction (CFG + flow lowering) | P1 | in progress (B0a/B0b/B2/A1 via `--flow-locals`) |
14 changes: 11 additions & 3 deletions docs/proposals/P-014-semantic-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
advisory **OWN050** ("declaring type unresolved — leakage analysis skipped"), never a guessed
leak. One `CSharpCompilation` over the inputs with framework + `OWN_EXTRA_REF_DIRS` references;
the `--event-leaks`/`--no-event-leaks` gate defaults ON. CI-validated on the extractor sample.
**Tier B** (opt-in *full*-reference resolution via `MSBuildWorkspace` / a built `bin/**/*.dll`
reference set, to resolve third-party/DevExpress event types beyond the framework set) is
deferred (open question Q3), as are monorepo scoping (Q1) and a general check-selection surface (Q2).
**Tier B (light path) shipped:** `--ref-dir <dir>` (repeatable) widens the compilation's
reference set RECURSIVELY from a built `bin/` (or a restored package's `lib/`), so events on
third-party types (DevExpress, etc.) bind to real symbols instead of OWN050 — first simple-name
wins, so framework/TPA references are never double-added, and an unloadable DLL is skipped, not
fatal. CI-validated by an **A/B test** (the `tier-b-refs` job): the same sample subscribing to a
`CommunityToolkit.Mvvm.ComponentModel.ObservableObject.PropertyChanged` yields OWN050 *without* the reference and
a real OWN001 *with* `--ref-dir` — proving resolution flips the verdict. Roslyn reads metadata
only, so a .NET Framework `bin/` resolves exactly as a modern-.NET one (only the DLLs differ).
**Still deferred:** the heavier auto-discovery of a project's *full transitive closure* from a
`.csproj`/`.sln` without a prior build (`MSBuildWorkspace`, open question Q3), monorepo scoping
(Q1), and a general check-selection surface (Q2).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Depends on:**
- [P-001](P-001-csharp-extractor.md) — the seam this deepens. P-001 defines the
Roslyn-extractor → OwnIR → Python-core pipeline (P-001:71-77) *and* owns the
Expand Down
2 changes: 1 addition & 1 deletion docs/proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ proposal is marked `done` with a pointer.
| [P-011](P-011-editor-tooling.md) | Editor tooling & syntax highlighting | draft |
| [P-012](P-012-bug-corpus-mining.md) | Real-world bug corpus & mining | in progress (corpus benchmark + real-world cases, CI-gated) |
| [P-013](P-013-distribution-surface.md) | Distribution surface (how people run Own.NET) | v0 built (CI/Action + dotnet tool) |
| [P-014](P-014-semantic-resolution.md) | Project-local semantic resolution (kills `+=` false positives) | in progress (Tier A shipped & default-on; Tier B deferred) |
| [P-014](P-014-semantic-resolution.md) | Project-local semantic resolution (kills `+=` false positives) | in progress (Tier A default-on + Tier B light path `--ref-dir`; full MSBuild closure deferred) |
| [P-015](P-015-configuration-surface.md) | Configuration surface (check selection & per-category severity) | draft (stub) |
| [P-016](P-016-deep-fact-extraction.md) | Deep C# fact extraction (CFG + flow lowering; loops) | in progress (B0a/B0b/B2/A1 via `--flow-locals`) |

Expand Down
35 changes: 34 additions & 1 deletion frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@

var rawInputs = new List<string>();
string? outPath = null;
// --ref-dir <dir> (repeatable, P-014 Tier B): widen the compilation's reference set with the
// DLLs under <dir>, searched RECURSIVELY — point it at a project's built `bin/` output (or a
// restored package's `lib/`) and the SemanticModel can then bind events on third-party types
// (DevExpress, etc.) instead of surfacing them as OWN050. The first-class, scriptable twin of the
// OWN_EXTRA_REF_DIRS env var (which stays non-recursive, for framework ref packs). Roslyn reads
// metadata only, so it resolves a .NET Framework `bin/` exactly as a modern-.NET one — only the
// DLLs differ. First simple-name wins, so a TPA/framework reference is never double-added.
var refDirs = new List<string>();
// Event-subscription detection is on by default now that it is type-aware
// (P-014 Tier A graduates it from the interim off). `--no-event-leaks` opts out
// (e.g. to run only the disposable/pool detectors); it is the first instance of
Expand Down Expand Up @@ -59,6 +67,7 @@
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "-o" && i + 1 < args.Length) outPath = args[++i];
else if (args[i] == "--ref-dir" && i + 1 < args.Length) refDirs.Add(args[++i]);
else if (args[i] == "--no-event-leaks") emitEvents = false;
else if (args[i] == "--flow-locals") flowLocals = true;
else if (args[i] == "--body-throw-edges") BodyThrowEdges = true;
Expand All @@ -68,7 +77,7 @@

if (rawInputs.Count == 0)
{
Console.Error.WriteLine("usage: ownsharp-extract <file.cs | dir> [...] [-o facts.json]");
Console.Error.WriteLine("usage: ownsharp-extract <file.cs | dir> [...] [-o facts.json] [--ref-dir <bin-dir>]");
return 2;
}

Expand Down Expand Up @@ -2374,7 +2383,7 @@
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Where(p => p.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
.ToList();
var refNames = new HashSet<string>(tpa.Select(Path.GetFileName), StringComparer.OrdinalIgnoreCase);

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / P-014 Tier B — external reference resolution (--ref-dir)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / P-014 Tier B — external reference resolution (--ref-dir)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 2386 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.
var references = tpa.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToList();
// P-004 WPF profile: widen the reference set with assemblies named by the
// OWN_EXTRA_REF_DIRS env var (colon-separated dirs) — e.g. the WindowsDesktop ref
Expand All @@ -2392,6 +2401,30 @@
{ references.Add(MetadataReference.CreateFromFile(dll)); added++; }
Console.Error.WriteLine($"extractor: +{added} extra references from {dir}");
}
// P-014 Tier B: --ref-dir <dir> widens the reference set RECURSIVELY (a project's built `bin/`,
// a restored package's `lib/`), so events on third-party types resolve to real symbols. First
// simple-name wins (a TPA/framework or OWN_EXTRA_REF_DIRS reference already loaded is skipped),
// so a `bin/` carrying multiple target-framework copies of the same assembly references one. A
// reference that fails to load (a native DLL, a corrupt file) is skipped, not fatal — we only read
// metadata, and a missing reference degrades to OWN050, never a crash.
foreach (var dir in refDirs)
{
if (!Directory.Exists(dir)) { Console.Error.WriteLine($"extractor: --ref-dir not found: {dir}"); continue; }
var added = 0;
// Ordinal sort makes "first simple-name wins" deterministic across platforms/filesystems
// (EnumerateFiles order is unspecified), so a `bin/` with multiple TFM copies picks the same one.
foreach (var dll in Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
.OrderBy(p => p, StringComparer.Ordinal))
{
var name = Path.GetFileName(dll);
if (!refNames.Contains(name))
// Record the name only on a successful load, so a failed DLL here doesn't burn the
// name and silently skip a loadable same-named assembly elsewhere in the tree.
try { references.Add(MetadataReference.CreateFromFile(dll)); refNames.Add(name); added++; }
catch (Exception ex) { Console.Error.WriteLine($"extractor: skipped {name}: {ex.GetType().Name}"); }
}
Console.Error.WriteLine($"extractor: +{added} references from --ref-dir {dir} (recursive)");
}
var compilation = CSharpCompilation.Create(
"own", parsed.Select(p => p.tree), references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
Expand Down
34 changes: 34 additions & 0 deletions frontend/roslyn/samples/TierBSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// P-014 Tier B — proof that external-reference resolution flips an event from "unchecked"
// to a real, resolved subscription. `TierBSubscriber` holds a long-lived reference to a
// CommunityToolkit.Mvvm `ObservableObject` (a THIRD-PARTY type, not in the framework set)
// and subscribes to its `PropertyChanged` without ever `-=`'ing it.
//
// WITHOUT --ref-dir : CommunityToolkit.Mvvm.dll is not referenced, so `ObservableObject`
// is an error type and `_vm.PropertyChanged += ...` cannot bind to an
// event symbol -> the extractor emits the advisory OWN050 ("leakage
// analysis skipped"), NEVER a guessed leak.
// WITH --ref-dir : the package DLL is referenced, the SemanticModel binds PropertyChanged
// to an IEventSymbol, and the un-detached subscription is a real OWN001
// leak (warning-tier — the source is injected, of unknown lifetime).
//
// Same source, two reference sets, two verdicts: the A/B delta is the Tier B proof. Roslyn reads
// metadata only, so this resolves a .NET Framework `bin/` exactly as a modern-.NET one — only the
// referenced DLLs differ. Exercised by the `tier-b-refs` CI job (ci.yml).
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;

namespace TierBSample
{
public sealed class TierBSubscriber
{
private readonly ObservableObject _vm;

public TierBSubscriber(ObservableObject vm)
{
_vm = vm;
_vm.PropertyChanged += OnChanged; // subscribed, never -='d

Check notice

Code scanning / Own.NET

declaring type unresolved -- leakage analysis skipped Note

cannot verify '_vm.PropertyChanged' — its declaring type is an unresolved reference (build the project or pass references); leakage analysis skipped [resource: unresolved reference]
}

private void OnChanged(object? sender, PropertyChangedEventArgs e) { }
}
}
Loading