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
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
<None Include="TestCases\VBPretty\Issue1906.vb" />
<None Include="TestCases\VBPretty\Issue2192.vb" />
<None Include="TestCases\VBPretty\Select.vb" />
<None Include="TestCases\VBPretty\VBAnonymousTypes.vb" />
<None Include="TestCases\VBPretty\ParameterizedProperties.vb" />
<None Include="TestCases\ILPretty\Unsafe.il" />
<None Include="TestCases\ILPretty\Issue1389.il" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ private static void UseLocalFunctionReference()

private static void SimpleCapture()
{
_003C_003Ec__DisplayClass1_0 _003C_003Ec__DisplayClass1_1 = default(_003C_003Ec__DisplayClass1_0);
_003C_003Ec__DisplayClass1_1.x = 1;
_003CSimpleCapture_003Eg__F_007C1_0(ref _003C_003Ec__DisplayClass1_1);
_003C_003Ec__DisplayClass1_0 obj = default(_003C_003Ec__DisplayClass1_0);
obj.x = 1;
_003CSimpleCapture_003Eg__F_007C1_0(ref obj);
}

private static void SimpleCaptureWithRef()
Expand All @@ -56,9 +56,9 @@ private static void SimpleCaptureWithRef()
obj.x = 1;
new Handle(new Func<int>(obj._003CSimpleCaptureWithRef_003Eg__F_007C0));
#else
_003C_003Ec__DisplayClass2_0 _003C_003Ec__DisplayClass2_1 = new _003C_003Ec__DisplayClass2_0();
_003C_003Ec__DisplayClass2_1.x = 1;
Handle handle = new Handle(new Func<int>(_003C_003Ec__DisplayClass2_1._003CSimpleCaptureWithRef_003Eg__F_007C0));
_003C_003Ec__DisplayClass2_0 obj = new _003C_003Ec__DisplayClass2_0();
obj.x = 1;
Handle handle = new Handle(new Func<int>(obj._003CSimpleCaptureWithRef_003Eg__F_007C0));
#endif
}

Expand Down
156 changes: 156 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
#if LEGACY_VBC
using System.Text;
#endif

using Microsoft.VisualBasic.CompilerServices;

// A VB anonymous type. Its properties are settable and only those declared 'Key'
// take part in Equals and GetHashCode, so it cannot be written as a C# anonymous
// type and is declared here instead.
#if LEGACY_VBC && OPT
[DebuggerDisplay("Value={Value}, Name={Name}")]
[CompilerGenerated]
#else
[CompilerGenerated]
[DebuggerDisplay("Value={Value}, Name={Name}")]
#endif
internal sealed class VB_AnonymousType_0<T0, T1>
{
#if !OPT && !LEGACY_VBC
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
#endif
private T0 _Value;

#if !OPT && !LEGACY_VBC
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
#endif
private T1 _Name;

public T0 Value {
get {
return _Value;
}
set {
_Value = value;
}
}

public T1 Name {
get {
return _Name;
}
set {
_Name = value;
}
}

#if !OPT && !LEGACY_VBC
[DebuggerHidden]
#endif
public VB_AnonymousType_0(T0 Value, T1 Name)
{
_Value = Value;
_Name = Name;
}

#if !OPT && !LEGACY_VBC
[DebuggerHidden]
#endif
public override string ToString()
{
#if LEGACY_VBC
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("{ ");
stringBuilder.AppendFormat("{0} = {1}, ", "Value", _Value);
stringBuilder.AppendFormat("{0} = {1} ", "Name", _Name);
stringBuilder.Append("}");
return stringBuilder.ToString();
#else
return string.Format(null, "{{ Value = {0}, Name = {1} }}", new object[2] { _Value, _Name });
#endif
}
}
[StandardModule]
public sealed class VBAnonymousTypes
{
public static void MutableAnonymousType()
{
VB_AnonymousType_0<int, string> obj = new VB_AnonymousType_0<int, string>(1, "test");
Console.WriteLine(obj.Value);
Console.WriteLine(obj.Name);
}

public static void KeyAnonymousType()
{
var anon = new {
Value = 1,
Name = "test"
};
Console.WriteLine(anon.Value);
Console.WriteLine(anon.Name);
}

public static void AnonymousTypeAsArgument()
{
Console.WriteLine(new {
Value = 1,
Name = "test"
}.ToString());
}

public static void SelectAnonymousType(IEnumerable<int> items)
{
var enumerable = items.Select([SpecialName] (int i) => new {
Value = i,
Square = checked(i * i)
});
foreach (var item in enumerable)
{
Console.WriteLine(item.Value);
Console.WriteLine(item.Square);
}
}

public static void LetWhereSelect(IEnumerable<int> items)
{
var enumerable = from i in items
let square = checked(i * i)
where square > 4
select new { i, square };
foreach (var item in enumerable)
{
Console.WriteLine(item.i);
Console.WriteLine(item.square);
}
}

public static void JoinSelect(IEnumerable<int> left, IEnumerable<int> right)
{
var enumerable = from x in left
join y in right on x equals y
select new { x, y };
foreach (var item in enumerable)
{
Console.WriteLine(item.x);
Console.WriteLine(item.y);
}
}

public static void OrderBySelect(IEnumerable<int> items)
{
var enumerable = from i in items
let doubled = checked(i * 2)
orderby doubled
select new { i, doubled };
foreach (var item in enumerable)
{
Console.WriteLine(item.i);
Console.WriteLine(item.doubled);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Imports System
Imports System.Collections.Generic
Imports System.Linq

Public Module VBAnonymousTypes
Public Sub MutableAnonymousType()
Dim value = New With {.Value = 1, .Name = "test"}
Console.WriteLine(value.Value)
Console.WriteLine(value.Name)
End Sub

Public Sub KeyAnonymousType()
Dim value = New With {Key .Value = 1, Key .Name = "test"}
Console.WriteLine(value.Value)
Console.WriteLine(value.Name)
End Sub

Public Sub AnonymousTypeAsArgument()
Console.WriteLine(New With {Key .Value = 1, Key .Name = "test"}.ToString())
End Sub

Public Sub SelectAnonymousType(items As IEnumerable(Of Integer))
Dim query = From i In items
Select New With {Key .Value = i, Key .Square = i * i}
For Each item In query
Console.WriteLine(item.Value)
Console.WriteLine(item.Square)
Next
End Sub

Public Sub LetWhereSelect(items As IEnumerable(Of Integer))
Dim query = From i In items
Let square = i * i
Where square > 4
Select i, square
For Each item In query
Console.WriteLine(item.i)
Console.WriteLine(item.square)
Next
End Sub

Public Sub JoinSelect(left As IEnumerable(Of Integer), right As IEnumerable(Of Integer))
Dim query = From x In left
Join y In right On x Equals y
Select x, y
For Each item In query
Console.WriteLine(item.x)
Console.WriteLine(item.y)
Next
End Sub

Public Sub OrderBySelect(items As IEnumerable(Of Integer))
Dim query = From i In items
Let doubled = i * 2
Order By doubled
Select i, doubled
For Each item In query
Console.WriteLine(item.i)
Console.WriteLine(item.doubled)
Next
End Sub
End Module
7 changes: 7 additions & 0 deletions ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ public async Task Select([ValueSource(nameof(defaultOptions))] CompilerOptions o
await Run(options: options | CompilerOptions.Library);
}

[Test]
public async Task VBAnonymousTypes([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
IgnoreIfVbRuntimeSubstituted(options);
await Run(options: options | CompilerOptions.Library);
}

[Test]
public async Task Issue1906([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
Expand Down
10 changes: 8 additions & 2 deletions ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ public static List<IAstTransform> GetAstTransforms()
new CombineQueryExpressions(),
new NormalizeBlockStatements(),
new FlattenSwitchBlocks(),
new RenameVisualBasicAnonymousTypes(), // must run before FixNameCollisions
new FixNameCollisions(),
new AddXmlDocumentationTransform(),
};
Expand Down Expand Up @@ -645,8 +646,13 @@ static bool IsClosureType(SRM.TypeDefinition type, MetadataReader metadata)

internal static bool IsTransparentIdentifier(string identifier)
{
return identifier.StartsWith("<>", StringComparison.Ordinal)
&& (identifier.Contains("TransparentIdentifier") || identifier.Contains("TranspIdent"));
if (identifier.StartsWith("<>", StringComparison.Ordinal))
{
return identifier.Contains("TransparentIdentifier") || identifier.Contains("TranspIdent");
}
// The VB compiler names the carriers of its query range variables
// $VB$It, $VB$It1, $VB$It2 and $VB$ItAnonymous.
return identifier.StartsWith("$VB$It", StringComparison.Ordinal);
}
#endregion

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System.Linq;

using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.TypeSystem;

namespace ICSharpCode.Decompiler.CSharp.Transforms
{
/// <summary>
/// Gives the anonymous types of a VB assembly that are declared rather than written as C#
/// anonymous types a name that is legal in C#.
/// </summary>
/// <remarks>
/// An anonymous type with a settable property has no C# anonymous type equivalent
/// (see <see cref="NRExtensions.IsAnonymousType(IType)"/>), so its declaration is emitted and
/// referred to by name. The VB compiler separates the parts of that name with '$', which is
/// not a legal identifier character in C#, and does the same for the backing fields.
/// Renaming happens per identifier via its symbol, so a reference is renamed the same way
/// whether or not the declaration is part of the same output.
/// </remarks>
public class RenameVisualBasicAnonymousTypes : IAstTransform
{
public void Run(AstNode rootNode, TransformContext context)
{
foreach (var identifier in rootNode.DescendantsAndSelf.OfType<Identifier>())
{
if (!identifier.Name.Contains("$"))
continue;
// A field declaration carries its symbol on the FieldDeclaration, not on the
// VariableInitializer holding the name.
if (FindEntity(identifier.Parent) is not IEntity entity || entity.Name != identifier.Name)
continue;
var declaringType = entity as ITypeDefinition ?? entity.DeclaringTypeDefinition;
if (declaringType == null || !declaringType.IsAnonymousTypeDeclaredAsNamedType())
continue;
identifier.Name = identifier.Name.Replace('$', '_');
}

foreach (var typeDeclaration in rootNode.DescendantsAndSelf.OfType<TypeDeclaration>())
{
if (typeDeclaration.GetSymbol() is not ITypeDefinition type
|| !type.IsAnonymousTypeDeclaredAsNamedType())
{
continue;
}
typeDeclaration.AddLeadingTrivia(new Comment(
" A VB anonymous type. Its properties are settable and only those declared 'Key'"));
typeDeclaration.AddLeadingTrivia(new Comment(
" take part in Equals and GetHashCode, so it cannot be written as a C# anonymous"));
typeDeclaration.AddLeadingTrivia(new Comment(
" type and is declared here instead."));
}

static IEntity FindEntity(AstNode node)
{
return node?.GetSymbol() as IEntity ?? node?.Parent?.GetSymbol() as IEntity;
}
}
}
}
6 changes: 6 additions & 0 deletions ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,12 @@ static string CleanUpVariableName(string name)

if (name.Length == 0)
return "obj";
if (!IsValidName(name))
{
// A name taken from metadata may be legal there but not in C#; VB, for one,
// separates the parts of its generated names with '$'.
return null;
}
string lowerCaseName = char.ToLower(name[0]) + name.Substring(1);
if (CSharp.OutputVisitor.CSharpOutputVisitor.IsKeyword(lowerCaseName))
return null;
Expand Down
Loading
Loading