Skip to content

Spell by-ref parameters from compiler attributes, not the interop In flag - #10033

Merged
richlander merged 3 commits into
mainfrom
claude/in-ref-parameter-spelling
Oct 13, 2026
Merged

richlander merged 3 commits into
mainfrom
claude/in-ref-parameter-spelling

Conversation

@richlander

@richlander richlander commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Change

Should we accept this declaration-spelling fix?

Conclusion: REVIEW.

Both Metadata declaration paths spelled any by-ref parameter that has the raw In flag (and not Out) as C# in. That flag is the interop marshalling direction, not the C# modifier.

  • C# in is identified by IsReadOnlyAttribute.
  • C# ref readonly is identified by RequiresLocationAttribute.
  • [In] ref is an ordinary mutable ref.

So two kinds of parameter came out wrong:

  • Interop [In] ref parameters rendered as in. That is a readonly reference callers cannot assign through, so it changes the declaration's meaning.
  • ref readonly parameters rendered as in.

Demo (production host: the CLI, real asset)

System.Runtime.InteropServices.ComTypes.IConnectionPointContainer.FindConnectionPoint in System.Private.CoreLib. The runtime source declares FindConnectionPoint([In] ref Guid riid, out IConnectionPoint ppCP).

dotnet-inspect member System.Runtime.InteropServices.ComTypes.IConnectionPointContainer FindConnectionPoint --all --library <System.Private.CoreLib.dll> -v:d
before: public abstract void FindConnectionPoint(in System.Guid riid, out System.Runtime.InteropServices.ComTypes.IConnectionPoint? ppCP)
after:  public abstract void FindConnectionPoint(ref System.Guid riid, out System.Runtime.InteropServices.ComTypes.IConnectionPoint? ppCP)

The same spelling feeds the RTS shells.

  • In the round 1 review of Reconstruct same-module delegate types as RTS closure roots #9980, a same-module delegate InRefOnly([In] ref int x) and an abstract method V([In] ref int x) were rebuilt with in. Roslyn then emitted modreq(InAttribute), which produced a false OperandDiff for correct body text.
  • Both shells now carry ref.

Design basis

  • Normative owner: Metadata parameter facts. The new ParameterByRefKinds.Classify in src/ILInspector.MetadataPrimitives/ParameterByRefKind.cs is the one classification of a by-ref parameter's C# passing mode. Its remarks state the rule and its precedence.

  • Claim: for a parameter whose signature type is by-reference, the declaration modifier follows the first rule that matches. This is Roslyn's metadata-reader precedence.

    Metadata Modifier
    Out flag without In out
    IsReadOnlyAttribute present in
    RequiresLocationAttribute present ref readonly
    otherwise (including [In] ref and [In, Out] ref) ref
    • Non-by-ref parameters are unchanged.
    • params is unchanged.
  • Baseline: C# compiler encoding. Roslyn emits IsReadOnlyAttribute for in, RequiresLocationAttribute for ref readonly, and sets the In flag on both. The In and Out flags alone are ECMA-335 marshalling directions (II.23.1.13). Roslyn's PEParameterSymbol checks Out-only before either attribute.

  • Analogous implementations: the Decompiler's call-site classifiers, IrImporter.ClassifyByRefParameter and MethodDefinitionFacts.ClassifyByRefParameter. They apply the attribute rule but check the attributes before Out; the follow-up adoption takes the corrected order.

What changed

  • ParameterByRefKinds (ILInspector.MetadataPrimitives, SRM-only) adds Classify(reader, parameter) and CSharpModifier().
  • MetadataDeclarationQuery.GetParameterInfo and ApiSurfaceExtractor.GetParameterInfo replace their duplicate raw-flag rules with the shared classifier. It runs for every parameter row; its result is used only when the signature type is by-ref, exactly as before.
  • JsonUnionWireRules.CaseConstructors (JsExportSurface) excludes ref readonly alongside ref, in and out. Case constructors are by-value. Before this PR, ref readonly constructors arrived as in and were already excluded.

Evidence

Gates (Release)

  • New tests:

    Test Kind What it asserts
    MetadataDeclarationQueryTests.MethodDeclaration_SpellsByRefParameterPassingFromCompilerAttributes theory, 6 cases ref, [In] ref, [In, Out] ref, out, in and ref readonly spell as ref, ref, ref, out, in and ref readonly
    MetadataDeclarationQueryTests.MethodDeclaration_SpellsOutOnlyFlagAsOutBeforeCompilerAttributes theory, 2 cases Param rows carrying IsReadOnlyAttribute or RequiresLocationAttribute, patched to Out-only, spell out (Roslyn precedence)
    MetadataDeclarationQueryTests.MethodDeclaration_SpellsCoreLibInteropInRefParameterAsRef real asset CoreLib's FindConnectionPoint gives ref and out
    ApiSignatureModelTests.MethodSignatureModel_SpellsByRefParameterPassingFromCompilerAttributes API surface path the same 6 shapes, through both the model and the signature text
    JsonUnionWireTests.CaseConstructors_ExcludeEveryByRefPassingMode theory, 4 cases ref, in, out and ref readonly constructors stay out of the union case set
    CSharpDeclarationWriterTests.AbbreviatedMemberDeclaration_PreservesParameterModifiers extended ref readonly passes through raw signatures
  • Mutations, each caught:

    Mutation Failing tests
    Old rule restored in the declaration query 4
    Old rule restored in the API surface 1
    RequiresLocationAttribute classified as in 2
    Attributes checked before Out-only (round 1 order) 2
    ref readonly dropped from the union filter 1
  • Suites at the round 2 head:

    • ILInspector.Metadata.Tests 4061/4061, ILInspector.JsExportSurface.Tests 722/722, ILInspector.CSharp.Tests 794/794, DotnetInspector.MetadataRendering.Tests 174/174.
    • DotnetInspector.Queries.Tests 4101/4101 (round 1 head).
    • DotnetInspect.Cli.Tests (29 methods, 35 cases) and DotnetInspector.Services.Tests (3 tests) fail at the round 1 head. I re-ran every one with main's two Metadata files swapped in. All the CLI failures and two of the Services failures fail the same way on main (local feed acquisition, routing, section pipelines, positional records, root-owned unreadable directories). The third Services failure, VersionPopulationOperationTimeoutRetainsEnumeratedVersions, is timing-dependent: it failed 2 of 3 runs on the branch and 1 of 3 on main.
  • Build: dotnet build dotnet-inspect.slnx -c Release gives 0 warnings and 0 errors.

Corpus exposure and baselines

An SRM probe counted the affected parameters in each corpus:

Corpus [In] ref → ref ref readonly Consequence
real-world (14 assemblies) 1 (Roslyn IMetadataImport.ResolveTypeRef) 19 (Roslyn) Regenerated (see below)
classic-state-machines, opt-in-net11 fixtures 0 0 Unaffected by construction
pr-quick (adds System.Private.CoreLib) 7 88 Rows carry metadata signatures (ref only) and compile-cap 0 compiles no declarations, so nothing can observe the spelling

Real-world baseline (cap 4000 / fidelity 50, rts-cutover), regenerated three times on the round 1 code:

The round 2 change affects only Out-only rows that also carry a compiler attribute. Roslyn never emits that shape, and no corpus assembly contains it. No baseline or pin changes.

User-visible changes (release tracker #8151)

  • Declarations of interop [In] ref parameters now render as ref, not in. ref readonly parameters now render as ref readonly, not in.
  • Member digests (Digest, a fingerprint of the canonical C# signature) change for those members. For example, FindConnectionPoint goes from 6c6babd372 to 856a522237. ECMA member anchors come from metadata and are unchanged.

Non-claims and follow-ups

  • Decompiler call-site classifiers (IrImporter, MethodDefinitionFacts): they still keep their own copies of the rule, in attribute-first order. Adopting the shared classifier is the next step, kept separate so that one owner changes at a time. That adoption takes Roslyn's precedence and makes MethodDefinitionFacts handle ref readonly too.
  • [In]/[Out] pseudo-attributes: they are still not rendered, so the result is ref System.Guid riid, not [In] ref System.Guid riid. The meaning is now correct and the call-site IL is identical; rendering the pseudo-attributes is a separate feature.
  • [Out] ref: it is metadata-identical to out, and still spells out, as before.
  • Performance: no claim. Each parameter row now gets one more attribute-name scan.

Review rounds

  • Round 1 (983fed8fa): 2 accepted findings, both low severity, fixed in round 2.
    • The classifier checked the attributes before the Out-only flag, so it disagreed with Roslyn and dropped out on hand-built metadata.
    • JsonUnionWireRules didn't exclude "ref readonly" constructors.

🤖 Generated with Claude Code

https://claude.ai/code/session_016JAYbAeLyaZdJSXjXNjGCP

…flag

The raw In/Out parameter flags are interop marshalling directions, not C#
modifiers. Both Metadata declaration paths spelled any In-flagged by-ref
parameter as `in`, so the runtime's FindConnectionPoint([In] ref Guid riid)
rendered as `in System.Guid riid`, a readonly reference callers cannot assign
through, and RTS delegate and virtual-method shells gained a false
modreq(InAttribute).

ParameterByRefKinds (MetadataPrimitives) is now the one classification:
IsReadOnlyAttribute -> in, RequiresLocationAttribute -> ref readonly, Out
without In -> out, otherwise ref. MetadataDeclarationQuery and
ApiSurfaceExtractor both use it. Fixes #3537.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JAYbAeLyaZdJSXjXNjGCP
…readonly union case constructors

Round 1 review of #10033:

- ParameterByRefKinds.Classify now reads Out without In as out before
  consulting IsReadOnlyAttribute or RequiresLocationAttribute, matching
  Roslyn's metadata reader. A patched-flag theory covers both attributes.
- JsonUnionWireRules.CaseConstructors excludes "ref readonly" alongside
  ref, in and out, so a [Union] convention type with a ref readonly
  constructor keeps its by-value case set.
- Caller comments now say the classification runs for every parameter
  row and is consumed only for by-ref signature types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JAYbAeLyaZdJSXjXNjGCP
@richlander richlander added the review-clean PR has met the adversarial review standard per AGENTS.md. label Oct 13, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

Review clean at head 801e3de7e120a8080316a3ff74946a0d742df3a7 (round 2, base 13890712e)

  • Round 1 (983fed8fa): 2 low-severity findings. Both are resolved at this head:
    • Out-only precedence: Out without In is now out before either compiler attribute is checked, matching Roslyn.
    • Union case constructors: ref readonly constructors are now excluded.
  • Round 2: clean. The reviewer checked 20 C# and VB shapes, covering both the declaration and API-surface paths, against Roslyn's IParameterSymbol.RefKind, with 0 differences. It also checked every consumer of Modifier and of the signature text.
  • CI: ci-required is green on this head, and the branch merges cleanly with main.
  • Follow-up: a separate later slice can adopt ParameterByRefKinds in two places. One is the Decompiler call-site classifiers. The other is a third raw-flag direction rule the reviewer found in ILInspector.Analysis MemberResolver (pre-existing, untouched here).

Merge needs explicit authorization.


Generated by Claude Code

@richlander
richlander merged commit 3bc74c2 into main Oct 13, 2026
15 checks passed
@richlander
richlander deleted the claude/in-ref-parameter-spelling branch October 13, 2026 13:37
richlander added a commit that referenced this pull request Oct 16, 2026
MethodDefinitionFacts.ReadParameterRefKinds now classifies each by-ref
parameter through the shared Metadata classifier (#10033), so call-site
spelling follows Roslyn's precedence: Out without In is out before either
compiler attribute is consulted. in and ref readonly still map to
ArgumentRefKind.In (spelled bare), and HasRefReadOnlyParameters is kept.

The unused IrImporter copy (ReadParameterRefKinds, ClassifyByRefParameter,
HasReadOnlyRefAttribute) is deleted.

Tests cover compiled passing modes, Out-only rows patched onto in and
ref readonly parameters, and the CoreLib FindConnectionPoint real asset.

Part of #10164.


Claude-Session: https://claude.ai/code/session_016JAYbAeLyaZdJSXjXNjGCP

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-clean PR has met the adversarial review standard per AGENTS.md.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interop [In] ref parameters are rendered as C# 'in'

2 participants