Skip to content

Commit 78c041a

Browse files
PhysShellclaude
andcommitted
fix(roslyn): a region opened inside the protocol's own types is refused, not skipped
The types that declare a protocol are trusted: their bodies implement it and are not lowered. The lowering skipped every method of such a type — including one that USES the protocol. A handler written in the same class as the region entry it calls was therefore never analysed, and read as clean: public static class ParcelHandlers { [ProtocolRegion] public static void WithPacked(...) => ...; public static void Dispatch(Parcel parcel) { WithPacked(parcel, packed => { packed.Send(); packed.Send(); }); } } Before: exit 0, no function emitted, verdict clean — a token spent twice. After: exit 2. Trusted is not the same as checked, and an unanalysed region must not read as a clean one. The remedy is in the message: use the protocol from outside the types that declare it. Found while probing what an unrelated attribute that merely shares the name [ProtocolRegion] does to a scan. refused/R15 pins it; no committed fact moves (18 cases, 15 refused, 0 failures), and the extractor's output on every C# sample CI scans is still equal to main's, 122 of 122. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent ab678eb commit 78c041a

4 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎frontend/roslyn/OwnSharp.Extractor/ProtocolLowering.cs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,17 @@ public static Result Lower(Compilation compilation,
117117
if (model.GetDeclaredSymbol(method) is not IMethodSymbol msym)
118118
continue;
119119
if (IsApiType(msym.ContainingType))
120-
continue; // the protocol API itself: its tokens' own bodies are not user code
120+
{
121+
// The protocol's own types are TRUSTED: their bodies implement the
122+
// protocol and are not lowered. So a region OPENED in one of them — a
123+
// handler written next to the region entry it calls — would never be
124+
// analysed and would read as clean. Trusted is not the same as checked.
125+
foreach (var opened in method.DescendantNodes().OfType<InvocationExpressionSyntax>()
126+
.Where(i => IsRegionEntry(MethodOf(model, i))))
127+
refusals.Add(At(file, opened,
128+
$"a protocol region is opened inside '{msym.ContainingType.Name}', one of the protocol's own types: code there implements the protocol and is not analysed, so this region would read as clean. Use the protocol from outside the types that declare it"));
129+
continue;
130+
}
121131
try
122132
{
123133
foreach (var p in msym.Parameters)

‎frontend/roslyn/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ not skipped: the extractor exits `2` and writes no facts, for the whole scan.
135135
`nameof`, `typeof` and reads are mentions, not operations.
136136
- *Source.* The protocol and its entity must be in the scan as source; one that
137137
arrives only as a compiled reference cannot be admitted.
138+
- *Trusted types.* The types that declare a protocol (the tokens, the class
139+
holding a region entry) implement it and are not analysed as user code. A
140+
region opened inside one of them is refused rather than skipped: write the
141+
code that uses a protocol outside the types that declare it.
138142
- *Binding.* The scan does not read `obj/`, so usings a project only gets
139143
**implicitly** are not there. A region whose entity does not bind is refused;
140144
write the usings out in the files that open regions (or qualify the names).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using Own.Protocols.Sample;
3+
4+
namespace Own.Protocols.Cases;
5+
6+
// The types that DECLARE a protocol are trusted: their bodies are the protocol, and the
7+
// lowering does not read them as user code. A handler written in the same class as the
8+
// region entry it calls would therefore never be analysed — and this one spends a token
9+
// twice. It is refused, not skipped: an unanalysed region must not read as a clean one.
10+
public sealed class Parcel
11+
{
12+
public bool Sent { get; private set; }
13+
14+
internal void MarkSent() => Sent = true;
15+
}
16+
17+
[ProtocolToken]
18+
public readonly ref struct PackedParcel
19+
{
20+
private readonly Parcel _parcel;
21+
internal PackedParcel(Parcel parcel) => _parcel = parcel;
22+
23+
public void Send() => _parcel.MarkSent();
24+
}
25+
26+
public delegate void PackedParcelRegion(PackedParcel packed);
27+
28+
public static class ParcelHandlers
29+
{
30+
[ProtocolRegion]
31+
public static void WithPacked(Parcel parcel, PackedParcelRegion body) => body(new PackedParcel(parcel));
32+
33+
public static void Dispatch(Parcel parcel)
34+
{
35+
WithPacked(parcel, packed =>
36+
{
37+
packed.Send();
38+
packed.Send(); // a stale token: OWN002 anywhere else
39+
});
40+
}
41+
}

‎frontend/roslyn/protocol-samples/refused/expected.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"R12_default_token_temporary": "a protocol token 'ApprovedOrder' is created outside the protocol's own types",
55
"R13_public_mutator_reaches_protocol_state": "'Doc.Publish' is public and writes 'Published', state the protocol's transitions own",
66
"R14_public_setter_of_protocol_state": "'Ticket.State' is public and writes 'State', state the protocol's transitions own",
7+
"R15_region_opened_inside_protocol_type": "a protocol region is opened inside 'ParcelHandlers', one of the protocol's own types",
78
"R1_default_token": "has no lowerable origin",
89
"R2_token_outside_region": "is declared outside a protocol region",
910
"R3_return_inside_region": "`return` inside a protocol region",

0 commit comments

Comments
 (0)