diff --git a/.github/scripts/update-testdata-targetframework.ps1 b/.github/scripts/update-testdata-targetframework.ps1 new file mode 100644 index 00000000..67f6ef00 --- /dev/null +++ b/.github/scripts/update-testdata-targetframework.ps1 @@ -0,0 +1,57 @@ +param() + +$ErrorActionPreference = 'Stop' + +Write-Host "Creating global.json to enforce .NET 8 for MSBuild" +$globalJson = '{"sdk":{"version":"8.0.0","rollForward":"latestFeature"}}' +[System.IO.File]::WriteAllText('global.json', $globalJson, [System.Text.Encoding]::UTF8) + +Write-Host "Searching for project files under Tests/TestData..." +$projFiles = Get-ChildItem -Path Tests/TestData -Recurse -Include *.csproj,*.vbproj,*.fsproj -File -ErrorAction SilentlyContinue + +if (-not $projFiles) { + Write-Host "No project files found under Tests/TestData" + exit 0 +} + +$changed = $false +foreach ($f in $projFiles) { + $path = $f.FullName + Write-Host "Processing: $path" + + # Use StreamReader to detect encoding and preserve it when writing back + $sr = [System.IO.StreamReader]::new($path, $true) + try { + $content = $sr.ReadToEnd() + $encoding = $sr.CurrentEncoding + } finally { + $sr.Close() + } + + # Replace net10.0 and net10.0-windows with net8.0 / net8.0-windows + $updated = [System.Text.RegularExpressions.Regex]::Replace($content, 'net10\.0(-windows)?', 'net8.0$1') + + if ($updated -ne $content) { + Write-Host "Updating TargetFramework in: $path" + # Write back preserving detected encoding and internal newlines + [System.IO.File]::WriteAllText($path, $updated, $encoding) + $changed = $true + } +} + +if ($changed) { + Write-Host "Changes detected — committing to local repo so working tree is clean for tests" + git config user.name "github-actions[bot]" + if ($env:GITHUB_ACTOR) { + git config user.email "$($env:GITHUB_ACTOR)@users.noreply.github.com" + } else { + git config user.email "actions@github.com" + } + git add -A + git commit -m "CI: Update Tests/TestData TargetFramework -> net8.0 for .NET 8 run" || Write-Host "No commit created (maybe no staged changes)" + Write-Host "Committed changes locally." +} else { + Write-Host "No TargetFramework updates required." +} + +Write-Host "Done." diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index dee7375f..0fe9bc6f 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -19,7 +19,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -30,11 +29,19 @@ jobs: with: vs-version: ${{ inputs.vs-version }} + - name: Build + run: dotnet build DotNetBuildable.slnf /p:Configuration=Release + + - name: Create global.json to enforce .NET 8 for MSBuild and update Tests/TestData projects + if: inputs.dotnet-version == '8.0.x' + shell: pwsh + run: ./.github/scripts/update-testdata-targetframework.ps1 + - name: Log MSBuild version run: msbuild -version - - name: Build - run: dotnet build DotNetBuildable.slnf /p:Configuration=Release + - name: Log .NET version + run: dotnet --info - name: Execute unit tests - run: dotnet test Tests/bin/Release/ICSharpCode.CodeConverter.Tests.dll + run: dotnet test Tests/Tests.csproj -c Release --framework net${{ inputs.dotnet-version == '8.0.x' && '8.0' || '10.0' }} --no-build diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index c3bada0b..bef5811f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -7,7 +7,7 @@ on: branches: [ master, main ] env: - BuildVersion: '10.0.0' + BuildVersion: '10.0.1' jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cfb598..1622181e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### C# -> VB +## [10.0.1] - 2026-02-28 + +* Reintroduce tentative legacy support for dotnet 8 and VS2022 +* Support slnx format [1195](https://github.com/icsharpcode/CodeConverter/issues/1195) + +### VB -> C# +* Fix for ReDim Preserve of array property - [#1156](https://github.com/icsharpcode/CodeConverter/issues/1156) +* Fix for with block conversion with null conditional [#1174](https://github.com/icsharpcode/CodeConverter/issues/1174) +Fixes #1195 + + ## [10.0.0] - 2026-02-06 * Support for net framework dropped. Please use an older version if you are converting projects that still use it. diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 6d8eed5e..8f2a4856 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -320,8 +320,18 @@ private async Task> ConvertRedimClauseAsync(VBSyntax var csTargetArrayExpression = await node.Expression.AcceptAsync(_expressionVisitor); var convertedBounds = (await CommonConversions.ConvertArrayBoundsAsync(node.ArrayBounds)).Sizes.ToList(); if (preserve && convertedBounds.Count == 1) { - var argumentList = new[] { csTargetArrayExpression, convertedBounds.Single() }.CreateCsArgList(SyntaxKind.RefKeyword); + bool isProperty = _semanticModel.GetSymbolInfo(node.Expression).Symbol?.IsKind(SymbolKind.Property) == true; + var arrayToResize = isProperty ? CreateLocalVariableWithUniqueName(node.Expression, "arg" + csTargetArrayExpression.ToString().Split('.').Last(), csTargetArrayExpression) : default; + var resizeArg = isProperty ? (ExpressionSyntax)arrayToResize.Reference : csTargetArrayExpression; + + var argumentList = new[] { resizeArg, convertedBounds.Single() }.CreateCsArgList(SyntaxKind.RefKeyword); var arrayResize = SyntaxFactory.InvocationExpression(ValidSyntaxFactory.MemberAccess(nameof(Array), nameof(Array.Resize)), argumentList); + + if (isProperty) { + var assignment = SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, csTargetArrayExpression, arrayToResize.Reference); + return SyntaxFactory.List(new StatementSyntax[] { arrayToResize.Declaration, SyntaxFactory.ExpressionStatement(arrayResize), SyntaxFactory.ExpressionStatement(assignment) }); + } + return SingleStatement(arrayResize); } var newArrayAssignment = CreateNewArrayAssignment(node.Expression, csTargetArrayExpression, convertedBounds); diff --git a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs index f9be5753..1b39251f 100644 --- a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Globalization; using ICSharpCode.CodeConverter.CSharp.Replacements; using ICSharpCode.CodeConverter.Util.FromRoslyn; @@ -688,14 +688,19 @@ private static QualifiedNameSyntax Qualify(string qualification, ExpressionSynta private static bool IsSubPartOfConditionalAccess(VBasic.Syntax.MemberAccessExpressionSyntax node) { - var firstPossiblyConditionalAncestor = node.Parent; - while (firstPossiblyConditionalAncestor != null && - firstPossiblyConditionalAncestor.IsKind(VBasic.SyntaxKind.InvocationExpression, - VBasic.SyntaxKind.SimpleMemberAccessExpression)) { - firstPossiblyConditionalAncestor = firstPossiblyConditionalAncestor.Parent; + static bool IsMemberAccessChain(SyntaxNode exp) => + exp?.IsKind(VBasic.SyntaxKind.InvocationExpression, + VBasic.SyntaxKind.SimpleMemberAccessExpression, + VBasic.SyntaxKind.ParenthesizedExpression, + VBasic.SyntaxKind.ConditionalAccessExpression) == true; + + for (SyntaxNode child = node, parent = node.Parent; IsMemberAccessChain(parent); child = parent, parent = parent.Parent) { + if (parent is VBSyntax.ConditionalAccessExpressionSyntax cae && cae.WhenNotNull == child) { + return true; // On right hand side of a ?. conditional access + } } - return firstPossiblyConditionalAncestor?.IsKind(VBasic.SyntaxKind.ConditionalAccessExpression) == true; + return false; } private static CSharpSyntaxNode ReplaceRightmostIdentifierText(CSharpSyntaxNode expr, SyntaxToken idToken, string overrideIdentifier) diff --git a/CodeConverter/Common/SolutionFileTextEditor.cs b/CodeConverter/Common/SolutionFileTextEditor.cs index ba209af3..50981dce 100644 --- a/CodeConverter/Common/SolutionFileTextEditor.cs +++ b/CodeConverter/Common/SolutionFileTextEditor.cs @@ -15,9 +15,21 @@ public class SolutionFileTextEditor : ISolutionFileTextEditor var projectReferenceReplacements = new List<(string Find, string Replace, bool FirstOnly)>(); foreach (var relativeProjPath in relativeProjPaths) { - var escapedProjPath = Regex.Escape(relativeProjPath); - var newProjPath = PathConverter.TogglePathExtension(relativeProjPath); - projectReferenceReplacements.Add((escapedProjPath, newProjPath, false)); + // Add replacements for both backslash and forward-slash variants so .slnx files using either separator are handled + var nativeVariant = relativeProjPath; + var altVariant = relativeProjPath.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + // native (likely backslashes on Windows) + var escapedNative = Regex.Escape(nativeVariant); + var newNative = PathConverter.TogglePathExtension(nativeVariant); + projectReferenceReplacements.Add((escapedNative, newNative, false)); + + // alternate (forward slashes) + if (altVariant != nativeVariant) { + var escapedAlt = Regex.Escape(altVariant); + var newAlt = PathConverter.TogglePathExtension(altVariant); + projectReferenceReplacements.Add((escapedAlt, newAlt, false)); + } } return projectReferenceReplacements; diff --git a/Directory.Build.props b/Directory.Build.props index 5b95257b..c048c3fe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,9 +8,9 @@ 4 true 14.0 - 10.0.0.0 - 10.0.0.0 - 10.0.0 + 10.0.1.0 + 10.0.1.0 + 10.0.1 ICSharpCode Copyright (c) 2017-2023 AlphaSierraPapa for the CodeConverter team ICSharpCode diff --git a/Tests/CSharp/StatementTests/MethodStatementTests.cs b/Tests/CSharp/StatementTests/MethodStatementTests.cs index 39bfd1c7..42c13f4d 100644 --- a/Tests/CSharp/StatementTests/MethodStatementTests.cs +++ b/Tests/CSharp/StatementTests/MethodStatementTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using ICSharpCode.CodeConverter.Tests.TestRunners; using Xunit; @@ -1673,4 +1673,44 @@ public object Func() } }"); } -} \ No newline at end of file + + [Fact] + public async Task WithBlockWithNullConditionalAccessAsync() + { + await TestConversionVisualBasicToCSharpAsync(@" +Public Class Class1 + Public Property x As Class1 + Public Property Name As String +End Class + +Public Class TestClass + Private _Data As Class1 + Private x As String + + Public Sub TestMethod() + With _Data + x = .x?.Name + End With + End Sub +End Class", @" +public partial class Class1 +{ + public Class1 x { get; set; } + public string Name { get; set; } +} + +public partial class TestClass +{ + private Class1 _Data; + private string x; + + public void TestMethod() + { + { + ref var withBlock = ref _Data; + x = withBlock.x?.Name; + } + } +}"); + } +} diff --git a/Tests/CSharp/StatementTests/RedimPreserveTests.cs b/Tests/CSharp/StatementTests/RedimPreserveTests.cs new file mode 100644 index 00000000..b54930ed --- /dev/null +++ b/Tests/CSharp/StatementTests/RedimPreserveTests.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using ICSharpCode.CodeConverter.Tests.TestRunners; +using Xunit; + +namespace ICSharpCode.CodeConverter.Tests.CSharp.StatementTests; + +public class RedimPreserveTests : ConverterTestBase +{ + [Fact] + public async Task RedimPreserveOnPropertyAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class TestClass + Public Property NumArray1 As Integer() + + Public Sub New() + ReDim Preserve NumArray1(4) + End Sub +End Class", @"using System; + +public partial class TestClass +{ + public int[] NumArray1 { get; set; } + + public TestClass() + { + var argNumArray1 = NumArray1; + Array.Resize(ref argNumArray1, 5); + NumArray1 = argNumArray1; + } +}"); + } +} diff --git a/Tests/LanguageAgnostic/SolutionFileTextEditorTests.cs b/Tests/LanguageAgnostic/SolutionFileTextEditorTests.cs index 6766e3f2..f502a06a 100644 --- a/Tests/LanguageAgnostic/SolutionFileTextEditorTests.cs +++ b/Tests/LanguageAgnostic/SolutionFileTextEditorTests.cs @@ -58,6 +58,31 @@ public void ConvertSolutionFile_WhenInSolutionBaseDirThenUpdated() Assert.Equal(expectedSlnFile, Utils.HomogenizeEol(convertedSlnFile)); } + [Fact] + public void ConvertSlnxSolutionFile_ProjectElementsWithForwardSlashesAreUpdated() + { + //Arrange + var slnxContents = "\r\n \r\n"; + var slnxSln = CreateTestSolution(@"C:\MySolution\MySolution.slnx"); + var projectId = ProjectId.CreateNewId(); + var projInfo = ProjectInfo.Create(projectId, VersionStamp.Create(), "ConsoleApp1", "ConsoleApp1", + LanguageNames.VisualBasic, @"C:\MySolution\ConsoleApp1\ConsoleApp1.vbproj"); + slnxSln = slnxSln.AddProject(projInfo); + var testProject = slnxSln.GetProject(projectId); + + _fsMock.Setup(mock => mock.File.ReadAllText(It.IsAny())).Returns(""); + + var slnConverter = SolutionConverter.CreateFor(new List { testProject }, + fileSystem: _fsMock.Object, solutionContents: slnxContents); + + //Act + var convertedSlnFile = slnConverter.ConvertSolutionFile().ConvertedCode; + + //Assert + var expectedSlnFile = "\r\n \r\n"; + Assert.Equal(expectedSlnFile, Utils.HomogenizeEol(convertedSlnFile)); + } + [Fact] public void ConvertSolutionFile_WhenInProjectFolderThenUpdated() { diff --git a/Tests/TestConstants.cs b/Tests/TestConstants.cs index 4fa077c9..20bf60b8 100644 --- a/Tests/TestConstants.cs +++ b/Tests/TestConstants.cs @@ -19,7 +19,7 @@ public static class TestConstants public static string GetTestDataDirectory() { var assembly = Assembly.GetExecutingAssembly(); - var solutionDir = new FileInfo(new Uri(assembly.Location).LocalPath).Directory?.Parent?.Parent ?? + var solutionDir = new FileInfo(new Uri(assembly.Location).LocalPath).Directory?.Parent?.Parent?.Parent ?? throw new InvalidOperationException(assembly.Location); return Path.Combine(solutionDir.FullName, "TestData"); } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index e1f42328..f25cb7b9 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,6 +1,7 @@ - + - net10.0 + net10.0;net8.0 + true Library ICSharpCode.CodeConverter.Tests ICSharpCode.CodeConverter.Tests diff --git a/Vsix/source.extension.vsixmanifest b/Vsix/source.extension.vsixmanifest index aad6a73e..961647bf 100644 --- a/Vsix/source.extension.vsixmanifest +++ b/Vsix/source.extension.vsixmanifest @@ -1,7 +1,7 @@  - + Code Converter (VB - C#) Convert VB.NET to C# and vice versa with this roslyn based converter https://github.com/icsharpcode/CodeConverter diff --git a/web/web.esproj b/web/web.esproj index 7f0a5977..57639216 100644 --- a/web/web.esproj +++ b/web/web.esproj @@ -1,5 +1,7 @@ - + + false + false npm run dev src\ Vitest @@ -8,5 +10,4 @@ $(MSBuildProjectDirectory)\dist - \ No newline at end of file