A scoping note, prompted by "could dotnet/roslyn-tools give us a ready-made C#
frontend for ownership?" Short answer: no — it is engineering harness, not an
analysis source. Recording the verdict so we don't re-open the question.
dotnet/roslyn-tools is a grab-bag of infrastructure tooling around Roslyn —
its own README is honestly terse ("A set of tools used by Roslyn"), and its
builds live in an Azure package feed. It is not a library that "understands
C# for us."
not a source of ownership analysis
a donor of engineering scaffolding (CLI shape, packaging, repo tooling, CI)
For actually reading C# the real sources are dotnet/roslyn, the Roslyn SDK,
and the Roslyn APIs — which is exactly what OwnSharp.Extractor already uses.
What's actually in roslyn-tools (so nobody has to spelunk again): a zoo of
internal tools — ModifyVsixManifest, SignTool, VSIXExpInstaller,
RoslynInsertionTool, CompilerPerfTests, BuildTasks, NuGetRepack,
RepoToolset, ProjectDependencies, and the Microsoft.RoslynTools CLI
(auth, PR finder/tagger, NuGet dependencies/prepare/publish, release tags, VS
branch info, PR validation, insertion/update-insertion). All release/insertion/
signing plumbing — none of it ownership.
The architecture in the README and P-001 already nails the boundary:
C# (Roslyn) extractor -> OwnIR facts JSON -> Python Own.Core -> diagnostics mapped back to C#
The C# side is a fact extractor, not a second checker. P-001 v0
(event += without -=) already does this: type-aware, project-local
SemanticModel, emits OwnIR facts, and the Python bridge runs them through the
existing core and maps OWN001 back onto the C# location. A full C# ownership
frontend (generics / async / interprocedural dataflow) is explicitly rejected as
"human-years." Keep it rejected.
-
CLI-first, shipped as a
dotnet tool— not a VSIX, not an analyzer package first. The mature tooling repos all start from a CLI:ownsharp extract --project MyApp.csproj --out facts.ownir.json ownsharp check --solution MyApp.sln ownsharp explain OWN001 --json diagnostic.json
Why: easier CI, easier debug, easier golden tests, and no living inside a Visual Studio extension before the analysis is even solid. (Use
System.CommandLine, as they do.) IDE squiggles come later. -
Repo-tooling separation. Their repo tools live apart from compiler logic; we want the same layering so one concern can't break another:
Own.Core Python checker / OwnIR schema + logic OwnSharp.Extractor C# Roslyn fact extractor Own.Cli orchestration (extract / check / explain) Own.Tests golden facts + expected diagnostics Own.Corpus real-world before/after casesDon't blend Roslyn-walking, OwnIR schema, ownership logic, diagnostic rendering, and CI packaging into one pile — that's how "the event-handler change broke ArrayPool codegen" becomes a recurring genre.
-
ProjectDependenciesas a category, not a dependency. For a future DI-lifetime checker and solution-wide scans we'll need the solution/project graph (project references, package references, target frameworks, compilation references). The v0 WPF event extractor doesn't need it; DI/effects will.
-
Don't take
roslyn-toolsas a dependency. It is an internal tooling repo; importing its insertion/release/signing baggage "in case it's useful" is the classic way to adopt someone else's enterprise suitcase without a handle. -
Don't reimplement the checker in C#. P-001 already says "Do not reimplement the checker in C#" — worth carving in stone. The C# frontend's whole job is:
find syntax/semantic facts -> resolve symbols -> get the location -> emit OwnIRIt must not decide ownership, join states at merges, reason about borrows, or otherwise produce an alternative truth. One checker; the C# side feeds it.
The first borrowed plyushka is real: the extractor now accepts a .csproj or
.sln as input (positional or via --project / --solution), not just a file
list — the CLI-first project resolution the mature tooling repos start from.
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
frontend/roslyn/project-input-sample/ProjectInputSample.csproj \
-o artifacts/facts.ownir.json
python -m ownlang ownir artifacts/facts.ownir.json
# -> CustomerSubscription.cs:11: warning: [OWN001] event 'bus.CustomerChanged'
# is subscribed (handler 'OnCustomerChanged') but never unsubscribedResolution is dependency-free (text/XML, no MSBuild evaluation): a .csproj
maps to its directory's *.cs the SDK default-compile-items way plus concrete
linked <Compile> files; a .sln fans out over its member projects (see
ProjectCsFiles / SolutionProjects in Program.cs). It deliberately stops
short of the full project/package/reference graph — that is the
ProjectDependencies-category work, parked for DI/solution scans. A
wpf-extractor CI step pins the .csproj path to a golden (positional ==
--project).
The advertised three-verb CLI is realised — but split to where the architecture puts each, not bolted onto one binary (there is one checker; the C# tool only emits facts):
ownsharp-extract extract --project App.csproj --out facts.ownir.json # C# extractor (facts)
scripts/own-check.sh --format sarif -- App.sln > findings.sarif # `check`: the orchestrator
python -m ownlang explain OWN001 # `explain`: the core's catalogue
python -m ownlang explain --json findings.sarif # (explain every code a run produced)(explain --json harvests diagnostic codes from the checker's findings/SARIF
output — not from facts.ownir.json, which carries extractor facts, no codes.)
extractis an optional leading verb on the C# tool (the bare form stays the default), plus--outas the long twin of-o. The tool's one job.checkis not a C# verb:own-check.shalready chains the extractor and the core, and since the project/solution PR it takes a.csproj/.slndirectly. Re-implementing a checker in C# is the rejected path.explainlives in the core next to the diagnostic catalogue (ownlang/diagnostics.py:TITLES+EXPLANATIONS): it prints what a code means, why it fires, and how to fix it;--jsonharvests every code from a findings/SARIF file so you can explain exactly what a run produced.
Two pieces of the "discoverable CLI" value landed:
-h/--help— a full hand-rendered usage block (commands, inputs, options), plus a--helppointer on the no-input error.--ref-dir-from-project-binauto-derivation — for a.csproj/.slninput, the project's builtbin/output is auto-added to the reference set (the--ref-diryou'd otherwise pass by hand), so a built/restored project's third-party events bind to real symbols instead of surfacing as OWN050.--no-project-refsopts out; an unbuilt project contributes nothing (no crash, just degrades to OWN050). SeeProjectBinDirsinProgram.cs.
We did not adopt the System.CommandLine package itself. It buys auto---help,
validation, and completions — but it is a churn-prone preview dependency, and the migration
is a large restructure of a 3,400-line entry point that builds only in CI here (no local
dotnet). The hand-rolled help/validation delivers the same user-facing surface at a fraction
of the risk; the framework swap stays available if tab-completion / generated help ever earns
its keep.
ownsharp extract --project App.csproj --out facts.ownir.json
ownsharp check --solution App.sln
ownsharp explain OWN001 --json diagnostic.jsonA facts record carries enough to place and explain the finding — kind, resource, owner, subject, location, and domain-neutral metadata:
{
"ownir_version": 0,
"source": "CustomerViewModel.cs",
"facts": [
{
"kind": "acquire",
"resource": "subscription",
"owner": "this",
"subject": "bus.CustomerChanged",
"location": { "file": "CustomerViewModel.cs", "line": 9, "column": 13 },
"metadata": { "handler": "OnCustomerChanged", "resource_kind": "subscription token" }
}
]
}This already works conceptually (tests/test_ownir.py exercises the bridge on
hand-written facts); the work is hardening the CLI/contract/golden.
1. event += without -= v0; drive to an iron golden
2. DispatcherTimer Tick += / Start => Stop + Tick -=
3. IDisposable fields an owned field must be disposed by the owner's Dispose
4. Subscribe() -> IDisposable an ignored token => leak
5. region/lifetime facts App > Window > ViewModel => OWN014
The ArrayPool/Span storage track lives alongside this, not blocking it. One
pattern, one facts schema, one golden, one CI step at a time — then the next
pattern.
dotnet tool packaging, build/release automation, VSIX packaging (if an IDE
extension ever happens), repo-wide validation, perf-testing style, and CI
conventions. Useful as a worked example of the engineering layer — not as a
source of the analysis.