A bound generic argument list without its closing brace fell off the
end of the string and silently produced an arity, making malformed ID
strings appear to parse and resolve to nothing instead of failing with
the documented ReflectionNameParseException. Also replaces the non-ASCII
punctuation in comments added by this branch with ASCII equivalents,
per the repository convention. Both raised by review on #3926.
Assisted-by: Claude:claude-fable-5:Claude Code
The C#/Roslyn-form ID of a member whose C++/CLI form differs can equal
the only key of a same-named sibling overload (char* vs signed char*).
Assemblies containing such overloads cannot come from the C# compiler,
so their xml files use the C++/CLI dialect, where that key documents
the sibling: falling back to the Roslyn form would show the sibling's
documentation for an undocumented member. GetIdStringCandidates now
omits the Roslyn form when a same-named sibling's C++/CLI form owns it,
so a lookup miss stays a miss.
Assisted-by: Claude:claude-fable-5:Claude Code
The recursive-descent parser for the full ID string grammar existed only
to feed the type-system-based FindEntity, while signatures were already
matched by regenerating candidate IDs; with two dialects a parser would
have to implement both grammars and stay in sync with the generator.
FindEntity now only decodes the structural skeleton - the declaring type
name and the member name - resolves the type, and narrows the members by
metadata name before the generate-and-compare step, falling back to an
unfiltered scan for keys whose name does not equal the metadata name
(a C++/CLI 'default' indexer). ParseTypeName, ParseMemberIdString and
their reference types (IdStringMemberReference,
GetPotentiallyNestedClassTypeReference) are removed.
Assisted-by: Claude:claude-fable-5:Claude Code
Member lookup compares regenerated ID strings, and with two dialects a
single mixed pass can resolve to the wrong member: the stripped
C#/Roslyn form of one member can equal the C++/CLI-dialect key of a
different member, e.g. C++ overloads differing only in a custom
modifier such as char* vs signed char*. FindMemberInType therefore
runs two passes over the whole member list, the more specific C++/CLI
form first, so an MSVC-written cref resolves to the member it names
instead of the first member whose stripped form happens to collide.
Assisted-by: Claude:claude-fable-5:Claude Code
The member keys in an xml doc file depend on the compiler that wrote it
(C# vs the MSVC C++/CLI dialect), so XmlDocumentationProvider's
entity-based lookup now tries each candidate form in order. The tooltip
path goes through the entity overload instead of building the key
itself, so the fallback lives in one place. The C++/CLI form is tried
first: wherever it differs it contains character sequences Roslyn never
writes, so it can only match MSVC-generated keys, while the stripped
Roslyn form of one member could match the key of a different member in
an MSVC-generated file.
Assisted-by: Claude:claude-fable-5:Claude Code
MSVC documents C++/CLI members with ECMA-372-style ID strings that
differ from Roslyn's in signatures: custom modifiers are rendered after
the modified type (a 'const int' parameter becomes
System.Int32!System.Runtime.CompilerServices.IsConst), arity markers
stay on generic instantiations (List`1{System.Int32}.Enumerator), and
the default indexed property is called 'default'. Roslyn ignores
modifiers entirely, so one generated string cannot match both
compilers' xml files: GetIdString keeps producing the C#/Roslyn form,
and the new GetIdStringCandidates additionally yields the C++/CLI form,
most specific first, for lookup code to try in order.
The dialect is pinned by IdStringProbe.il/.xml, the trimmed disassembly
of an MSVC-compiled probe assembly together with the unmodified xml MSVC
generated for it. Notable observed deviations from MSVC's documented
format: modreq is generated, but only modreq(IsVolatile) uses the
documented '|'; modreq(IsByValue) on conversion operator operands is
rendered with '!'.
Assisted-by: Claude:claude-fable-5:Claude Code
Resolve an ID string to a (module, handle) pair by scanning metadata
directly: namespace/type-name splits are tried at every dot (the format
does not mark the boundary), nested types and type forwarders are
walked, and members are matched by regenerating each candidate's ID
string instead of parsing the signature portion, which keeps resolution
in sync with generation by construction. This gives navigation a
resolution path that needs no type system and works on assemblies
exactly as their metadata spells them. Inherent format limitations
(metadata names containing ID string special characters; function
pointer types rendering empty, so such overloads share an ID) are
documented on the method.
Assisted-by: Claude:claude-fable-5:Claude Code
Generate ID strings directly from metadata instead of the type system,
so callers holding only a MetadataFile and an EntityHandle do not need
to materialize a compilation first, and raw metadata names survive
verbatim. GetIdString(IEntity) stays as a thin wrapper over the new
implementation, keeping the published entity-based entry point intact.
The implementation is validated by a differential suite comparing every
symbol of a test corpus against Roslyn's GetDocumentationCommentId.
Corners pinned by those tests: generic arguments distribute to their
nesting level (Outer{A}.Inner{B}), op_CheckedExplicit carries the
~ReturnType suffix like the other conversion operators, and custom
modifiers are ignored like Roslyn ignores them (a virtual method's 'in'
parameter is modreq(InAttribute) but documented as T@). Array shapes
use the spec's lowerbound:size notation, covered by a hand-built
module, since C# cannot express non-default array bounds in signatures.
Assisted-by: Claude:claude-fable-5:Claude Code
The WPF app showed the disassembled IL header when hovering a member
reference in IL view, and the opcode's XML documentation when hovering
an instruction. The Avalonia port lost both: ILLanguage fell back to
the base ambience one-liner and the opcode tooltip carried only the
name and encoding. Opcode docs additionally need a modern-.NET source:
the WPF code read the .NET Framework reference-assembly docs, which do
not exist on modern .NET, so MscorlibDocumentation now falls back to
the ref pack parallel to the hosting runtime.
Assisted-by: Claude:claude-fable-5:Claude Code
XmlDocLoader's modern-.NET fallback built the ref-pack path from the EXACT runtime
patch version (shared/Microsoft.NETCore.App/10.0.8 -> packs/Microsoft.NETCore.App.Ref/
10.0.8/ref), but the targeting (ref) pack version usually differs from the installed
runtime patch -- e.g. pack 10.0.0 -- and a relocated CI install (DOTNET_INSTALL_DIR)
may carry only a different feature band. The exact-version lookup then missed and the
provider came back null, leaving CoreLib hovers undocumented. Broaden the search:
prefer an exact match, then the newest pack sharing the runtime's major.minor, then
the newest pack present.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Promotes the modern .NET XML-doc lookup from the Avalonia port into the
shared ICSharpCode.Decompiler library so every host (WPF, Avalonia, any
third-party consumer of XmlDocLoader) gets hover/tooltip documentation
for system entities without per-host fallback wiring.
While support for multi-module assemblies isn't fully working yet; it is clear at this point that we want
to treat each module in a multi-module assembly separately for the purposes of the type system.
This was the last place where the public API of the resolved TS was referencing the unresolved TS.
We could now implement the resolved TS directly using SRM, and remove the whole unresolved TS layer.