Skip to content

Commit 268dbf2

Browse files
Make switch case labels const where possible
Fixes part of #544
1 parent 883bbac commit 268dbf2

8 files changed

Lines changed: 131 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
3232
* Converted non-static field initializers moved to constructor - [#281](https://github.com/icsharpcode/CodeConverter/issues/281)
3333
* Convert assignments using "Mid" built-in function
3434
* Improve conversion of array initializer types
35+
* Improve detection of compile-time constant case labels
3536

3637
### C# -> VB
3738

CodeConverter/CSharp/ExpressionEvaluator.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,26 @@ private static IEnumerable<MethodInfo> GetStringsMethods()
4343
.Where(g => g.Count() == 1).SelectMany(ms => ms);
4444
}
4545

46-
public ExpressionSyntax GetConstantOrNull(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol type, ExpressionSyntax csNode)
46+
public (ExpressionSyntax Expr, bool IsCorrectType) GetConstantOrNull(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol type, TypeConversionAnalyzer.TypeConversionKind analyzedConversionKind, ExpressionSyntax csNode)
4747
{
4848
var vbOperation = _semanticModel.GetOperation(vbNode).SkipParens(true);
4949

5050
// Guideline tradeoff: Usually would aim for erring on the side of correct runtime behaviour. But making lots of simple constants unreadable for the sake of an edge case that will turn into an easily fixed compile error seems overkill.
5151
// See https://github.com/icsharpcode/CodeConverter/blob/master/.github/CONTRIBUTING.md#deciding-what-the-output-should-be
52-
if (Equals(vbOperation.Type, type) && IsProbablyConstExpression(vbOperation)) return csNode;
53-
54-
if (TryCompileTimeEvaluate(vbOperation, out var result) && ConversionsTypeFullNames.TryGetValue(type.GetFullMetadataName(), out var method)) {
55-
result = method.Invoke(null, new[] { result });
56-
return LiteralConversions.GetLiteralExpression(result, convertedType: type);
52+
bool isExactType = analyzedConversionKind == TypeConversionAnalyzer.TypeConversionKind.Identity;
53+
if ((isExactType || analyzedConversionKind == TypeConversionAnalyzer.TypeConversionKind.NonDestructiveCast)
54+
&& IsProbablyConstExpression(vbOperation)) return (csNode, isExactType);
55+
56+
if (TryCompileTimeEvaluate(vbOperation, out var result)) {
57+
if (type.Name == "Char" && result is int resultInt) {
58+
result = Strings.ChrW(resultInt);
59+
} else if (ConversionsTypeFullNames.TryGetValue(type.GetFullMetadataName(), out var method)) {
60+
result = method.Invoke(null, new[] { result });
61+
}
62+
return (LiteralConversions.GetLiteralExpression(result, convertedType: type), true);
5763
}
5864

59-
return null;
65+
return (null, false);
6066
}
6167

6268
/// <remarks>Deal with cases like "2*PI" without inlining the const</remarks>
@@ -68,16 +74,19 @@ private bool IsProbablyConstExpression(IOperation op)
6874
return true;
6975
}
7076

71-
if (op is IBinaryOperation bo && IsProbablyConstExpression(bo.LeftOperand) && IsProbablyConstExpression(bo.RightOperand)) {
72-
return true;
77+
if (op is IBinaryOperation bo) {
78+
return IsProbablyConstExpression(bo.LeftOperand) && IsProbablyConstExpression(bo.RightOperand);
79+
}
80+
81+
if (op is IUnaryOperation uo) {
82+
return IsProbablyConstExpression(uo.Operand);
7383
}
7484

7585
return false;
7686
}
7787

7888
private bool TryCompileTimeEvaluate(IOperation vbOperation, out object result)
7989
{
80-
result = null;
8190
if (vbOperation.ConstantValue.HasValue) {
8291
result = vbOperation.ConstantValue.Value;
8392
return true;

CodeConverter/CSharp/ExpressionNodeVisitor.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,12 +1537,12 @@ private ArgumentListSyntax CreateArgList(IMethodSymbol methodSymbol)
15371537

15381538
private async Task<CSharpSyntaxNode> SubstituteVisualBasicMethodOrNullAsync(VBasic.Syntax.InvocationExpressionSyntax node)
15391539
{
1540-
CastExpressionSyntax cSharpSyntaxNode = null;
1540+
ExpressionSyntax cSharpSyntaxNode = null;
15411541
var symbol = _semanticModel.GetSymbolInfo(node.Expression).ExtractBestMatch<ISymbol>();
15421542
if (symbol?.Name == "ChrW" || symbol?.Name == "Chr") {
1543-
var args = await ConvertArgumentsAsync(node.ArgumentList);
1544-
cSharpSyntaxNode = ValidSyntaxFactory.CastExpression(SyntaxFactory.ParseTypeName("char"),
1545-
args.Single().Expression);
1543+
var vbArg = node.ArgumentList.Arguments.Single().GetExpression();
1544+
var csArg = (ExpressionSyntax) await vbArg.AcceptAsync(TriviaConvertingExpressionVisitor);
1545+
cSharpSyntaxNode = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(vbArg, csArg, true, true, true, forceTargetType: _semanticModel.GetTypeInfo(node).Type);
15461546
}
15471547

15481548
return cSharpSyntaxNode;

CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -647,16 +647,12 @@ public override async Task<SyntaxList<StatementSyntax>> VisitSelectBlock(VBSynta
647647
var originalExpressionSyntax = (ExpressionSyntax)await s.Value.AcceptAsync(_expressionVisitor);
648648
// CSharp requires an explicit cast from the base type (e.g. int) in most cases switching on an enum
649649
var typeConversionKind = CommonConversions.TypeConversionAnalyzer.AnalyzeConversion(s.Value);
650-
var expressionSyntax = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(s.Value, originalExpressionSyntax, typeConversionKind, true);
651-
SwitchLabelSyntax caseSwitchLabelSyntax = SyntaxFactory.CaseSwitchLabel(expressionSyntax);
650+
var correctTypeExpressionSyntax = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(s.Value, originalExpressionSyntax, typeConversionKind, true, true);
652651
var constantValue = _semanticModel.GetConstantValue(s.Value);
653-
var isRepeatedConstantValue = constantValue.HasValue && !usedConstantValues.Add(constantValue);
654-
if (!constantValue.HasValue || isRepeatedConstantValue ||
655-
(typeConversionKind != TypeConversionAnalyzer.TypeConversionKind.NonDestructiveCast &&
656-
typeConversionKind != TypeConversionAnalyzer.TypeConversionKind.Identity)) {
657-
caseSwitchLabelSyntax =
658-
WrapInCasePatternSwitchLabelSyntax(node, expressionSyntax);
659-
}
652+
var notAlreadyUsed = !constantValue.HasValue || usedConstantValues.Add(constantValue.Value);
653+
var caseSwitchLabelSyntax = correctTypeExpressionSyntax.IsConst && notAlreadyUsed
654+
? (SwitchLabelSyntax)SyntaxFactory.CaseSwitchLabel(correctTypeExpressionSyntax.Expr)
655+
: WrapInCasePatternSwitchLabelSyntax(node, correctTypeExpressionSyntax.Expr);
660656
labels.Add(caseSwitchLabelSyntax);
661657
} else if (c is VBSyntax.ElseCaseClauseSyntax) {
662658
labels.Add(SyntaxFactory.DefaultSwitchLabel());
@@ -731,6 +727,7 @@ private CasePatternSwitchLabelSyntax WrapInCasePatternSwitchLabelSyntax(VBSyntax
731727
SyntaxFactory.DiscardDesignation());
732728
} else {
733729
var varName = CommonConversions.CsEscapedIdentifier(GetUniqueVariableNameInScope(node, "case"));
730+
//CodeAnalysis upgrade to 3.0.0 needed for VarPattern. Correct text comes out, but tree is invalid so the tests this will generate "CS0825: The contextual keyword 'var' may only appear within a local variable declaration or in script code"
734731
patternMatch = SyntaxFactory.DeclarationPattern(
735732
ValidSyntaxFactory.VarType, SyntaxFactory.SingleVariableDesignation(varName));
736733
expression = SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, SyntaxFactory.IdentifierName(varName), expression);

CodeConverter/CSharp/TypeConversionAnalyzer.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using Microsoft.CodeAnalysis.Editing;
1010
using Microsoft.CodeAnalysis.FindSymbols;
1111
using Microsoft.CodeAnalysis.Operations;
12-
using Microsoft.CodeAnalysis.VisualBasic;
12+
using VBasic = Microsoft.CodeAnalysis.VisualBasic;
1313
using Microsoft.VisualBasic;
1414
using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax;
1515
using Microsoft.VisualBasic.CompilerServices;
@@ -24,6 +24,7 @@
2424
using TypeSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax;
2525
using Microsoft.CodeAnalysis.CSharp.Syntax;
2626
using System.Linq.Expressions;
27+
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
2728

2829
namespace ICSharpCode.CodeConverter.CSharp
2930
{
@@ -53,29 +54,42 @@ public ExpressionSyntax AddExplicitConversion(Microsoft.CodeAnalysis.VisualBasic
5354
csNode = addParenthesisIfNeeded && (conversionKind == TypeConversionKind.DestructiveCast || conversionKind == TypeConversionKind.NonDestructiveCast)
5455
? VbSyntaxNodeExtensions.ParenthesizeIfPrecedenceCouldChange(vbNode, csNode)
5556
: csNode;
56-
return AddExplicitConversion(vbNode, csNode, conversionKind, addParenthesisIfNeeded, isConst, forceSourceType: forceSourceType, forceTargetType: forceTargetType);
57+
return AddExplicitConversion(vbNode, csNode, conversionKind, addParenthesisIfNeeded, isConst, forceSourceType: forceSourceType, forceTargetType: forceTargetType).Expr;
5758
}
5859

59-
public ExpressionSyntax AddExplicitConversion(Microsoft.CodeAnalysis.VisualBasic.Syntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, TypeConversionKind conversionKind, bool addParenthesisIfNeeded = false, bool isConst = false, ITypeSymbol forceSourceType = null, ITypeSymbol forceTargetType = null)
60+
public (ExpressionSyntax Expr, bool IsConst) AddExplicitConversion(Microsoft.CodeAnalysis.VisualBasic.Syntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, TypeConversionKind conversionKind, bool addParenthesisIfNeeded = false, bool requiresConst = false, ITypeSymbol forceSourceType = null, ITypeSymbol forceTargetType = null)
6061
{
6162
var typeInfo = ModelExtensions.GetTypeInfo(_semanticModel, vbNode);
6263
var vbType = forceSourceType ?? typeInfo.Type;
6364
var vbConvertedType = forceTargetType ?? typeInfo.ConvertedType;
65+
bool resultConst = false;
6466

65-
if (isConst && _expressionEvaluator.GetConstantOrNull(vbNode, vbConvertedType, csNode) is ExpressionSyntax constLiteral) {
66-
return constLiteral;
67+
if (requiresConst) {
68+
var (constExpression, isCorrectType) = _expressionEvaluator.GetConstantOrNull(vbNode, vbConvertedType, conversionKind, csNode);
69+
if (isCorrectType) {
70+
return (constExpression, true);
71+
}
72+
if (constExpression != null) {
73+
csNode = constExpression ?? csNode;
74+
resultConst = true;
75+
}
6776
}
6877

69-
switch (conversionKind)
70-
{
78+
var typeConvertedResult = AddTypeConversion(vbNode, csNode, conversionKind, addParenthesisIfNeeded, vbType, vbConvertedType);
79+
return (typeConvertedResult, resultConst);
80+
}
81+
82+
private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, TypeConversionKind conversionKind, bool addParenthesisIfNeeded, ITypeSymbol vbType, ITypeSymbol vbConvertedType)
83+
{
84+
switch (conversionKind) {
7185
case TypeConversionKind.Unknown:
7286
case TypeConversionKind.Identity:
7387
return addParenthesisIfNeeded ? VbSyntaxNodeExtensions.ParenthesizeIfPrecedenceCouldChange(vbNode, csNode) : csNode;
7488
case TypeConversionKind.DestructiveCast:
7589
case TypeConversionKind.NonDestructiveCast:
7690
return CreateCast(csNode, vbConvertedType);
7791
case TypeConversionKind.Conversion:
78-
return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType);;
92+
return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType); ;
7993
case TypeConversionKind.NullableBool:
8094
return SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, csNode,
8195
LiteralConversions.GetLiteralExpression(true));
@@ -120,7 +134,7 @@ public TypeConversionKind AnalyzeConversion(Microsoft.CodeAnalysis.VisualBasic.S
120134
}
121135
}
122136

123-
var vbCompilation = (VisualBasicCompilation) _semanticModel.Compilation;
137+
var vbCompilation = (VBasic.VisualBasicCompilation) _semanticModel.Compilation;
124138
var vbConversion = vbCompilation.ClassifyConversion(vbType, vbConvertedType);
125139
var csType = GetCSType(vbType, vbNode);
126140
var csConvertedType = GetCSType(vbConvertedType);
@@ -149,7 +163,7 @@ private ITypeSymbol GetCSType(ITypeSymbol vbType, VBSyntax.ExpressionSyntax vbNo
149163

150164
private bool TryAnalyzeCsConversion(Microsoft.CodeAnalysis.VisualBasic.Syntax.ExpressionSyntax vbNode, ITypeSymbol csType,
151165
ITypeSymbol csConvertedType, Conversion vbConversion, ITypeSymbol vbConvertedType, ITypeSymbol vbType,
152-
VisualBasicCompilation vbCompilation, bool isConst, out TypeConversionKind typeConversionKind)
166+
VBasic.VisualBasicCompilation vbCompilation, bool isConst, out TypeConversionKind typeConversionKind)
153167
{
154168
var csConversion = _csCompilation.ClassifyConversion(csType, csConvertedType);
155169

Tests/CSharp/MissingSemanticModelInfo/ExpressionTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,44 @@ public string PositionEnumStringFromConstant(PositionEnum pS)
258258
CS0246: The type or namespace name 'MissingType' could not be found (are you missing a using directive or an assembly reference?)");
259259
}
260260

261+
[Fact]
262+
public async Task CastToSameTypeAsync()
263+
{
264+
await TestConversionVisualBasicToCSharpAsync(@"Public Class CastToSameTypeTest
265+
266+
Sub PositionEnumFromString(ByVal c As Char)
267+
Select Case c
268+
Case CChar(""."")
269+
Console.WriteLine(1)
270+
Case CChar("","")
271+
Console.WriteLine(2)
272+
End Select
273+
End Sub
274+
End Class",
275+
@"using System;
276+
277+
public partial class CastToSameTypeTest
278+
{
279+
public void PositionEnumFromString(char c)
280+
{
281+
switch (c)
282+
{
283+
case '.':
284+
{
285+
Console.WriteLine(1);
286+
break;
287+
}
288+
289+
case ',':
290+
{
291+
Console.WriteLine(2);
292+
break;
293+
}
294+
}
295+
}
296+
}") ;
297+
}
298+
261299
[Fact]
262300
public async Task UnknownTypeInvocationAsync()
263301
{

Tests/CSharp/SpecialConversionTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ private string numstr(double aDouble)
206206
207207
return str_Txt;
208208
}
209+
}");
210+
}
211+
212+
[Fact]
213+
public async Task TestConstCharacterConversionsAsync()
214+
{
215+
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Data
216+
217+
Class TestConstCharacterConversions
218+
Function GetItem(dr As DataRow) As Object
219+
Const a As String = Chr(7)
220+
Const b As String = ChrW(8)
221+
Const t As String = Chr(9)
222+
Const n As String = ChrW(10)
223+
Const v As String = Chr(11)
224+
Const f As String = ChrW(12)
225+
Const r As String = Chr(13)
226+
Const x As String = Chr(14)
227+
Const 字 As String = ChrW(&H5B57)
228+
End Function
229+
End Class", @"using System.Data;
230+
231+
internal partial class TestConstCharacterConversions
232+
{
233+
public object GetItem(DataRow dr)
234+
{
235+
const string a = ""\a"";
236+
const string b = ""\b"";
237+
const string t = ""\t"";
238+
const string n = ""\n"";
239+
const string v = ""\v"";
240+
const string f = ""\f"";
241+
const string r = ""\r"";
242+
const string x = ""\u000e"";
243+
const string 字 = ""字"";
244+
return default;
245+
}
209246
}");
210247
}
211248
}

Tests/CSharp/TypeCastTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ await TestConversionVisualBasicToCSharpAsync(
289289
End Sub
290290
", @"private void Test()
291291
{
292-
char CR = (char)0xF;
292+
char CR = '\u000f';
293293
}
294294
");
295295
}
@@ -303,7 +303,7 @@ await TestConversionVisualBasicToCSharpAsync(
303303
End Sub
304304
", @"private void Test()
305305
{
306-
char CR = (char)0xF;
306+
char CR = '\u000f';
307307
}
308308
");
309309
}

0 commit comments

Comments
 (0)