Skip to content

feat(utilities): TypeDeclarationInfo snapshot + tree-bound location round-trip - #150

Merged
github-actions[bot] merged 1 commit into
mainfrom
claude/type-declaration-info
Jul 6, 2026
Merged

feat(utilities): TypeDeclarationInfo snapshot + tree-bound location round-trip#150
github-actions[bot] merged 1 commit into
mainfrom
claude/type-declaration-info

Conversation

@ANcpLua

@ANcpLua ANcpLua commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Two additions inspired by the Microsoft.Agents.AI.Workflows.Generators model layer — plus one pre-existing bug the new tests surfaced.

1. TypeDeclarationInfo + ContainingTypeInfo + TypeDeclarationScope (Models/)

Cacheable, value-equatable snapshot of a type declaration for incremental generators: namespace, declaration keyword (class/struct/record/record struct/interface), generic parameter clause, partial-ness, and the containing-type chain.

The chain is stored as per-level declarations (keyword + name + generic clause + partial-ness), not a flat name string. This deliberately fixes the flaw in the Microsoft version, whose ContainingTypeChain string loses container type kind and generics — its emitted partial class Outer { wrapper fails to compile when the container is a struct or generic.

  • From(INamedTypeSymbol) — snapshot in the transform stage (cache-safe, no symbol retention)
  • BeginDeclaration(IndentedStringBuilder) — emits namespace + full nested partial wrapper as a disposable scope, house-style
  • IsFullyPartial — the report-a-diagnostic-instead-of-emitting gate

2. Tree-bound location round-trip

LocationInfo.ToLocation(SyntaxTree?) and DiagnosticInfo.ToDiagnostic(SyntaxTree?) bind the recreated Location to a live syntax tree when available (IDE navigation + live squiggles; IsInSource == true), falling back to the existing path-based location. A span-bounds guard makes a changed/wrong tree degrade to the fallback instead of throwing.

3. Bug fix: GetGenericParameterClause on nested types

The helper keyed on INamedTypeSymbol.IsGenericType, which Roslyn defines as “this type or some containing type has type parameters” — so a non-generic type nested in Outer<T> produced a bogus "<>" clause. Now keyed on TypeParameters.Length. Found by the new per-level chain tests.

Verification

  • CI=true dotnet build ANcpLua.Roslyn.Utilities.slnx -c Release — 0 errors
  • dotnet test208 passed, 0 failed, including:
    • per-level chain capture for Deep.Outer<T>.Middle.Inner<U> (class→struct→record struct)
    • emitted wrapper compiles together with the original source (the exact case the MS flat chain gets wrong)
    • keyword capture across class/interface/record/record struct/struct/enum
    • value equality across identical compilations (cache-hit correctness)
    • tree-bound vs path-based location behavior incl. short-tree fallback

Status: complete-and-verified. Touches src/**, tests/**, and README.md → publish gate opens; merge auto-bumps to 2.2.30 via trusted publishing.

🤖 Generated with Claude Code

…on round-trip

TypeDeclarationInfo / ContainingTypeInfo: cacheable, value-equatable snapshot
of a type declaration for incremental generators — namespace, declaration
keyword, generic clause, partial-ness, and the containing-type chain captured
as per-level declarations (keyword + name + generic clause + partial-ness),
so emission into targets nested in generic or struct containers compiles.
BeginDeclaration(IndentedStringBuilder) emits the nested partial wrapper as a
disposable scope; IsFullyPartial gates emission for diagnostics.

LocationInfo.ToLocation(SyntaxTree?) / DiagnosticInfo.ToDiagnostic(SyntaxTree?):
bind recreated Locations to a live syntax tree for IDE navigation/squiggles,
with a span-bounds guard falling back to the path-based location.

Also fixes GetGenericParameterClause: it keyed on IsGenericType, which Roslyn
defines as true when any containing type has type parameters, producing a
bogus "<>" clause for non-generic nested types. Now keyed on
TypeParameters.Length. Surfaced by the new nested-chain tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot merged commit 5a5af5d into main Jul 6, 2026
6 of 7 checks passed
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e09b0b64-19b1-4bca-a5c5-ecdc34f27912

📥 Commits

Reviewing files that changed from the base of the PR and between d9e0b20 and 259b241.

⛔ Files ignored due to path filters (2)
  • .claude/TASK.md is excluded by none and included by none
  • README.md is excluded by none and included by none
📒 Files selected for processing (6)
  • src/ANcpLua.Roslyn.Utilities/Models/DiagnosticInfo.cs
  • src/ANcpLua.Roslyn.Utilities/Models/LocationInfo.cs
  • src/ANcpLua.Roslyn.Utilities/Models/TypeDeclarationInfo.cs
  • src/ANcpLua.Roslyn.Utilities/TypeSymbolExtensions.CodeGen.cs
  • tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TreeBoundLocationTests.cs
  • tests/ANcpLua.Roslyn.Utilities.Testing.Tests/TypeDeclarationInfoTests.cs

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting


Summary by CodeRabbit

  • New Features

    • Improved support for generating code tied to the correct source location, including better handling when a syntax tree is available.
    • Added support for capturing and emitting nested type declarations more accurately, including generic and partial types.
  • Bug Fixes

    • Fixed location conversion so diagnostics now point to the expected source when possible, with a safe fallback when they can’t be tree-bound.
    • Corrected generic type clause handling for nested types to avoid emitting incorrect type parameters.

Walkthrough

This PR adds tree-bound location resolution for LocationInfo/DiagnosticInfo with path-based fallback, introduces a new TypeDeclarationInfo model (plus ContainingTypeInfo and TypeDeclarationScope) for emitting partial type declaration wrappers, fixes generic parameter clause detection, and adds tests for all of it.

Changes

Tree-bound locations and type declaration snapshot

Layer / File(s) Summary
Tree-bound Location and Diagnostic conversion
src/ANcpLua.Roslyn.Utilities/Models/LocationInfo.cs, src/ANcpLua.Roslyn.Utilities/Models/DiagnosticInfo.cs, tests/.../TreeBoundLocationTests.cs
LocationInfo.ToLocation(SyntaxTree?) binds a Location to the tree when the span fits, else falls back to path-based; DiagnosticInfo.ToDiagnostic now delegates through this overload. Tests cover in-source, null-tree, and short-tree fallback cases.
TypeDeclarationInfo model and BeginDeclaration emission
src/ANcpLua.Roslyn.Utilities/Models/TypeDeclarationInfo.cs, tests/.../TypeDeclarationInfoTests.cs
New TypeDeclarationInfo, ContainingTypeInfo, and TypeDeclarationScope capture namespace/keyword/name/generic-clause/partial state from INamedTypeSymbol, compute IsFullyPartial/DisplayName, and emit disposable namespace/type wrapper blocks. Tests validate extraction, partial detection, code emission, compilation of generated output, and value equality.
Generic parameter clause detection fix
src/ANcpLua.Roslyn.Utilities/TypeSymbolExtensions.CodeGen.cs
GetGenericParameterClause now checks TypeParameters.Length > 0 instead of IsGenericType to avoid incorrect <> clauses for non-generic nested types.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DiagnosticInfo
  participant LocationInfo
  Caller->>DiagnosticInfo: ToDiagnostic(syntaxTree)
  DiagnosticInfo->>LocationInfo: ToLocation(syntaxTree)
  alt tree non-null and span fits
    LocationInfo-->>DiagnosticInfo: tree-bound Location
  else fallback
    LocationInfo-->>DiagnosticInfo: path-based Location
  end
  DiagnosticInfo-->>Caller: Diagnostic
Loading

Possibly related PRs

  • ANcpLua/ANcpLua.Roslyn.Utilities#144: Directly touches the same LocationInfo.ToLocation(SyntaxTree?)/DiagnosticInfo.ToDiagnostic(SyntaxTree?) mechanism, adding a regression test for diagnostic location validity across shrinking syntax trees.

Comment @coderabbitai help to get the list of available commands.

github-actions Bot pushed a commit that referenced this pull request Jul 6, 2026
…0, qyl bumped) (#151)

Task fully complete: merged via #150, published as 2.2.30 (tagged, indexed
on nuget.org for both Utilities and Utilities.Testing), qyl pins bumped in
qyl#487.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 259b24179c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

blockCount++;
}

builder.AppendLine($"partial {Keyword} {DisplayName}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve accessibility in emitted partial declarations

When a generator uses this helper for a public partial target (or any target/container with explicit accessibility), this emits a partial declaration with no accessibility modifier. C# partial type parts must use the same accessibility, so the generated file conflicts with the user's declaration (CS0262) before any generated members can compile; capture and replay the accessibility for the target and containing wrappers.

Useful? React with 👍 / 👎.

type.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null,
GetKeyword(type),
type.Name,
type.GetGenericParameterClause(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve generic variance in partial wrappers

For a partial interface such as partial interface I<out T> (or a target nested in one), this captures the generic clause via GetGenericParameterClause(), which records only T; BeginDeclaration later emits partial interface I<T>. Partial declarations must use the same variance modifiers, so consumers hit CS1067 for variant interfaces; include the in/out modifier in the stored clause for interface type parameters.

Useful? React with 👍 / 👎.

Comment on lines +130 to +131
return syntaxTree is not null && Span.End <= syntaxTree.Length
? Location.Create(syntaxTree, Span)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Location.None from binding to arbitrary trees

When LocationInfo was created from Location.None (for example, DiagnosticInfo.Create(descriptor, symbol) falls back for symbols without source locations), its span is the default [0,0], so this condition succeeds for any supplied tree and turns a no-location diagnostic into a squiggle at the start of that file. This affects callers that pass a tree to rebind a mixed set of diagnostics; fall back unless the stored location actually identifies the supplied source tree.

Useful? React with 👍 / 👎.

return new TypeDeclarationInfo(
type.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null,
GetKeyword(type),
type.Name,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape type names before emitting wrappers

For a legal declaration such as partial class @class { } (or an escaped containing type), INamedTypeSymbol.Name is class, so the snapshot is later emitted as partial class class. That generated source does not parse, which breaks generators using this helper on escaped keyword identifiers; capture the syntax token text or format identifiers with C# keyword escaping before emission.

Useful? React with 👍 / 👎.

{
{ IsRecord: true, TypeKind: TypeKind.Struct } => "record struct",
{ IsRecord: true } => "record",
{ TypeKind: TypeKind.Struct } => "struct",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve ref struct kind in partial wrappers

When the target or a containing type is declared as partial ref struct, this maps the symbol to plain struct, so BeginDeclaration emits partial struct S. Ref structs are a distinct partial type kind, and the generated part will not merge with the user's ref struct declaration (CS0261); include IsRefLikeType when capturing the keyword.

Useful? React with 👍 / 👎.

GetKeyword(type),
type.Name,
type.GetGenericParameterClause(),
IsDeclaredPartial(type),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject file-local targets before emission

For a file partial class C, this stores only that the declaration is partial, so IsFullyPartial can pass and BeginDeclaration emits a normal partial class C into the generated file. File-local types are scoped to their declaring source file, so the generated declaration is a different non-file type rather than an augmentation of the target; treat file-local targets as unsupported instead of reporting them as fully partial.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant