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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,22 @@ jobs:
<(jq -S . "$RUNNER_TEMP/proj-facts-verb.json") \
|| { echo "FAIL: 'extract --out' verb form disagrees with the bare form"; exit 1; }
echo "OK: .csproj input resolves to its source set and feeds the core identically (bare == --project == 'extract --out', fact-level parity)"
# --help renders the discoverable usage (commands/inputs/options) and exits 0.
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- --help > "$RUNNER_TEMP/help.txt"
grep -q "Usage:" "$RUNNER_TEMP/help.txt" && grep -q -- "--no-project-refs" "$RUNNER_TEMP/help.txt" \
|| { echo "FAIL: --help did not render the usage/options"; exit 1; }
# --no-project-refs is accepted and (with no bin/ on the sample) yields identical facts.
# Guard the precondition: the parity below only holds while the sample is unbuilt, so a
# future step that builds it fails here with a clear message, not a confusing facts diff.
[ ! -d frontend/roslyn/project-input-sample/bin ] \
|| { echo "FAIL: sample project must be unbuilt for the --no-project-refs parity check"; exit 1; }
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
--no-project-refs --project frontend/roslyn/project-input-sample/ProjectInputSample.csproj \
--out "$RUNNER_TEMP/proj-facts-norefs.json"
diff <(jq -S . "$RUNNER_TEMP/proj-facts.json") \
<(jq -S . "$RUNNER_TEMP/proj-facts-norefs.json") \
|| { echo "FAIL: --no-project-refs changed the facts for an unbuilt sample project"; exit 1; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "OK: --help renders the option list; --no-project-refs is accepted"
# explain: the diagnostic-catalogue CLI surface lives in the core (one checker). Smoke
# it end-to-end — explain a code, and harvest+explain every code in a real findings file.
- name: explain command (code + --json harvest)
Expand Down
21 changes: 18 additions & 3 deletions docs/notes/roslyn-tools-and-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,24 @@ output — not from `facts.ownir.json`, which carries extractor facts, no codes.
why it fires, and how to fix it; `--json` harvests every code from a findings/SARIF
file so you can explain exactly what a run produced.

A `System.CommandLine` migration of the C# tool (auto `--help`, validation) and
`--ref-dir`-from-project-`bin` auto-derivation remain the next polish — deferred over
a blind framework swap, since the extractor builds only in CI here.
### CLI polish (landed) — and why not `System.CommandLine`

Two pieces of the "discoverable CLI" value landed:

- **`-h` / `--help`** — a full hand-rendered usage block (commands, inputs, options), plus
a `--help` pointer on the no-input error.
- **`--ref-dir`-from-project-`bin` auto-derivation** — for a `.csproj`/`.sln` input, the
project's built `bin/` output is auto-added to the reference set (the `--ref-dir` you'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-refs` opts out; an unbuilt project
contributes nothing (no crash, just degrades to OWN050). See `ProjectBinDirs` in `Program.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.

## Earlier next-PR sketch (kept for the record)

Expand Down
83 changes: 83 additions & 0 deletions frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
// flow-analysed vs honestly skipped for an unmodelled construct) and stamp the
// same counts into the facts JSON. Turns "0 findings" into "clean vs didn't-reach".
bool reportStats = false;
// --no-project-refs: opt out of the automatic reference derivation below. By default, when an
// input is a `.csproj`/`.sln`, the project's built `bin/` output (if present) is auto-added to
// the reference set — the scriptable `--ref-dir` you'd otherwise pass by hand — so third-party
// events on a restored/built project bind to real symbols instead of surfacing as OWN050. This
// flag turns that off (e.g. to measure raw Tier-A coverage, or when bin/ is stale).
bool noProjectRefs = false;
// --body-throw-edges (opt-in, P-016 throw tier): also treat an ESCAPING body-level may-throw
// call/`new` (not only those inside a `try`) as a dispose-not-called-on-throw point — CodeQL
// cs/dispose-not-called-on-throw parity. OFF by default: it is the CA2000 firehose (flags even
Expand All @@ -82,6 +88,43 @@
// own-check orchestrator, `explain` is `python -m ownlang explain`; the C# tool is not a
// second checker.) A different first token (a path/flag) is left untouched as input.
var args0 = args.Length > 0 && args[0] == "extract" ? args[1..] : args;
// `-h` / `--help`: print the full usage (commands, inputs, options) and exit 0. A discoverable
// CLI is the value the roslyn-tools `System.CommandLine` shape buys; we render it by hand (no
// preview dependency) rather than adopt the framework — the surface is small and stable.
const string UsageText = """
ownsharp-extract — emit OwnIR leak facts from C# for the Own.NET core to check.

Usage:
ownsharp-extract [extract] <input>... [options]

`extract` is an optional leading verb (the tool's one job; the bare form is the default).
The sibling verbs live elsewhere by design — `check` is scripts/own-check.sh (extractor +
core), `explain` is `python -m ownlang explain OWN001`. One checker: this tool only emits facts.

Inputs (any mix; positional, or via --project/--solution):
file.cs a single C# file
dir a directory, walked recursively (skips bin/obj, generated, vendor)
App.csproj a project — resolved to its source set (no MSBuild evaluation)
App.sln a solution — fans out over its member projects

Options:
-o, --out FILE write the OwnIR facts JSON to FILE (default: stdout)
--project FILE add a .csproj input (flag twin of the positional form)
--solution FILE add a .sln input
--ref-dir DIR add DIR's DLLs (recursively) to the reference set, so third-party
events bind to real symbols instead of OWN050 (repeatable)
--no-project-refs don't auto-add a .csproj/.sln project's bin/ output to the references
--no-event-leaks skip event-subscription detection (run only disposable/pool detectors)
--flow-locals path-sensitive flow analysis of non-escaping local IDisposables
--stats print flow-locals coverage (requires --flow-locals)
--body-throw-edges treat escaping body-level may-throw as a dispose-on-throw point (needs --flow-locals)
-h, --help show this help and exit
""";
if (args0.Contains("-h") || args0.Contains("--help"))
{
Console.WriteLine(UsageText);
return 0;
}
for (int i = 0; i < args0.Length; i++)
{
// `--out FILE` is the long-form twin of `-o FILE` (the advertised `extract --out` UX).
Expand All @@ -92,6 +135,7 @@
// the positional form keeps the command unambiguous next to dotnet's own `run --project`.
else if ((args0[i] == "--project" || args0[i] == "--solution") && i + 1 < args0.Length) rawInputs.Add(args0[++i]);
else if (args0[i] == "--ref-dir" && i + 1 < args0.Length) refDirs.Add(args0[++i]);
else if (args0[i] == "--no-project-refs") noProjectRefs = true;
else if (args0[i] == "--no-event-leaks") emitEvents = false;
else if (args0[i] == "--flow-locals") flowLocals = true;
else if (args0[i] == "--body-throw-edges") BodyThrowEdges = true;
Expand All @@ -102,6 +146,7 @@
if (rawInputs.Count == 0)
{
Console.Error.WriteLine("usage: ownsharp-extract [extract] <file.cs | dir | *.csproj | *.sln> [...] [-o|--out facts.json] [--ref-dir <bin-dir>]");
Console.Error.WriteLine(" ownsharp-extract --help for the full option list");
return 2;
}

Expand Down Expand Up @@ -322,6 +367,33 @@
return projects;
}

// The built `bin/` output directory of each `.csproj` (or each member project of a `.sln`) among
// the inputs — the references to auto-derive (P-014 Tier B convenience). Returns only directories
// that exist, deduped: an unbuilt project contributes nothing (and its third-party events degrade
// to OWN050, never a crash). The caller adds these to the reference set exactly like a hand-passed
// `--ref-dir`, so a built/restored project's third-party events bind without one.
static List<string> ProjectBinDirs(IEnumerable<string> rawInputs)
{
var bins = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddFor(string csproj)
{
var full = Path.GetFullPath(csproj);
if (!File.Exists(full)) return; // missing project: nothing to derive
var dir = Path.GetDirectoryName(full);
if (dir is null) return;
var bin = Path.Combine(dir, "bin");
if (Directory.Exists(bin) && seen.Add(bin)) bins.Add(bin);
}
foreach (var p in rawInputs)
{
if (p.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) AddFor(p);
else if (p.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
foreach (var proj in SolutionProjects(p)) AddFor(proj);
}
return bins;
}

// Expand inputs into their .cs files. A directory is walked recursively; a `.csproj`/`.sln`
// is resolved to its source set (so `ownsharp-extract App.csproj` / `App.sln` works, the
// CLI-first project input borrowed from the roslyn-tools tooling shape); an explicit file
Expand Down Expand Up @@ -366,6 +438,17 @@
static string Rel(string path) =>
Path.GetRelativePath(Directory.GetCurrentDirectory(), path).Replace('\\', '/');

// Auto-derive project references (unless --no-project-refs): for a .csproj/.sln input, add its
// built bin/ output to the reference set so third-party events bind without a hand-passed --ref-dir.
// Appended to refDirs before the reference set is built; the recursive, first-name-wins --ref-dir
// loader handles it from there (a framework/TPA simple-name already loaded is never double-added).
if (!noProjectRefs)
foreach (var bin in ProjectBinDirs(rawInputs))
{
refDirs.Add(bin);
Console.Error.WriteLine($"extractor: auto-referencing project output {bin} (--no-project-refs to disable)");
}

var inputs = Expand(rawInputs).Distinct().ToList();

static bool IsHandler(ExpressionSyntax rhs) =>
Expand Down Expand Up @@ -2864,7 +2947,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 2950 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 2950 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 2950 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 2950 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 2950 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 2950 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 2950 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 2950 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 Down
7 changes: 7 additions & 0 deletions frontend/roslyn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ graph) is the `ProjectDependencies`-category work parked for DI/solution scans,
the v0 leak extractor — see
[`docs/notes/roslyn-tools-and-cli.md`](../../docs/notes/roslyn-tools-and-cli.md).

When the input is a `.csproj`/`.sln`, the project's built `bin/` output is
**auto-added to the reference set** (the `--ref-dir` you'd otherwise pass by hand),
so a built/restored project's third-party events (WPF/DevExpress) bind to real
symbols instead of surfacing as OWN050. `--no-project-refs` opts out; an unbuilt
project just contributes nothing. Run `dotnet run --project OwnSharp.Extractor -- --help`
for the full option list.

## Use it on a real repo / in CI (P-013)

The two stages are chained by one orchestrator script, so you don't run them by
Expand Down
Loading