Skip to content

Commit fa361ce

Browse files
committed
Add Analyzer for finding errors with ternaries in interpolated strings
why is this not part of the SDK
1 parent 3a3f567 commit fa361ce

File tree

8 files changed

+130
-4
lines changed

8 files changed

+130
-4
lines changed

.global.editorconfig.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ dotnet_diagnostic.BHI1120.severity = silent
3636

3737
# Check result of IDictionary.TryGetValue, or discard it if default(T) is desired
3838
dotnet_diagnostic.BHI1200.severity = error
39+
# Inferred type of bramches of ternary expression in interpolation don't match
40+
dotnet_diagnostic.BHI1210.severity = error
41+
3942
# Call to FirstOrDefault when elements are of a value type; FirstOrNull may have been intended
4043
dotnet_diagnostic.BHI3100.severity = error
4144
# Use .Order()/.OrderDescending() shorthand
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
namespace BizHawk.Analyzers;
2+
3+
using System.Collections.Immutable;
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Operations;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class TernaryInferredTypeMismatchAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly DiagnosticDescriptor DiagTernaryInferredTypeMismatch = new(
13+
id: "BHI1210",
14+
title: "Inferred type of bramches of ternary expression in interpolation don't match",
15+
// messageFormat: "Inferred type of ternary expression is object (missing ToString call?)",
16+
messageFormat: "{0}",
17+
category: "Usage",
18+
defaultSeverity: DiagnosticSeverity.Warning,
19+
isEnabledByDefault: true);
20+
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagTernaryInferredTypeMismatch);
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
context.RegisterCompilationStartAction(initContext =>
28+
{
29+
var objectSym = initContext.Compilation.GetTypeByMetadataName("System.Object")!;
30+
var stringSym = initContext.Compilation.GetTypeByMetadataName("System.String")!;
31+
initContext.RegisterOperationAction(oac =>
32+
{
33+
var ifelseOrTernaryOp = (IConditionalOperation) oac.Operation;
34+
if (ifelseOrTernaryOp.WhenFalse is null) return;
35+
var parent = ifelseOrTernaryOp.Parent!;
36+
if (parent.Kind is OperationKind.Conversion) parent = parent.Parent!;
37+
if (parent.Kind is not OperationKind.Interpolation) return;
38+
var ternaryOp = ifelseOrTernaryOp;
39+
var typeTernary = ternaryOp.Type!;
40+
#if false // never hit; either both branches are string and there are no conversions, or conversions are necessary
41+
if (stringSym.Matches(typeTernary)) return;
42+
#endif
43+
var lhs = ternaryOp.WhenTrue;
44+
var rhs = ternaryOp.WhenFalse;
45+
46+
static IOperation TrimImplicitCast(IOperation op)
47+
=> op is IConversionOperation { Conversion.IsImplicit: true } implCastOp ? implCastOp.Operand : op;
48+
var typeLHS = TrimImplicitCast(lhs).Type!;
49+
var typeRHS = TrimImplicitCast(rhs).Type!;
50+
if (typeLHS.Matches(typeRHS)) return; // unnecessary conversion operators on each branch? seen with `? this : this`
51+
52+
const string ERR_MSG_OBJECT = "missing ToString means ternary branches are upcast to object";
53+
var fatal = false;
54+
IOperation flaggedOp = ternaryOp;
55+
string message;
56+
if (stringSym.Matches(typeLHS))
57+
{
58+
flaggedOp = rhs;
59+
message = ERR_MSG_OBJECT;
60+
}
61+
else if (stringSym.Matches(typeRHS))
62+
{
63+
flaggedOp = lhs;
64+
message = ERR_MSG_OBJECT;
65+
}
66+
else if (objectSym.Matches(typeTernary))
67+
{
68+
fatal = true;
69+
message = "ternary branches are upcast to object! add ToString calls, or convert one to the other's type";
70+
}
71+
else
72+
{
73+
// if one's already an e.g. int literal, flag the e.g. char literal
74+
if (typeTernary.Matches(typeLHS)) flaggedOp = rhs;
75+
else if (typeTernary.Matches(typeRHS)) flaggedOp = lhs;
76+
message = $"ternary branches are converted to {typeTernary} before serialisation, possibly unintended";
77+
}
78+
oac.ReportDiagnostic(Diagnostic.Create(
79+
DiagTernaryInferredTypeMismatch,
80+
flaggedOp.Syntax.GetLocation(),
81+
fatal ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
82+
additionalLocations: null,
83+
properties: null,
84+
messageArgs: message));
85+
},
86+
OperationKind.Conditional);
87+
});
88+
}
89+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace BizHawk.Tests.Analyzers;
2+
3+
using System.Threading.Tasks;
4+
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
8+
BizHawk.Analyzers.TernaryInferredTypeMismatchAnalyzer,
9+
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
10+
11+
[TestClass]
12+
public sealed class TernaryInferredTypeMismatchAnalyzerTests
13+
{
14+
[TestMethod]
15+
public Task CheckMisuseOfTernariesInInterpolations()
16+
=> Verify.VerifyAnalyzerAsync("""
17+
using System.Collections.Generic;
18+
using System.Diagnostics;
19+
public static class Cases {
20+
private static string X(bool cond)
21+
=> $"{(cond ? 'p'.ToString() : 9.ToString())}";
22+
private static string Y(bool cond)
23+
=> $"{(cond ? "p" : 9.ToString())}";
24+
private static string Z(bool cond)
25+
=> $"{(cond ? "p" : "q")}";
26+
private static string A(bool cond)
27+
=> $"{(cond ? "p" : {|BHI1210:9|})}";
28+
private static string B(bool cond)
29+
=> $"{(cond ? {|BHI1210:'p'|} : 9)}";
30+
private static string C(bool cond, Process p, Queue<int> q)
31+
=> $"{({|BHI1210:cond ? p : q|})}";
32+
}
33+
""");
34+
}

References/BizHawk.Analyzer.dll

2 KB
Binary file not shown.

src/BizHawk.Client.EmuHawk/MainForm.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private void MainForm_Load(object sender, EventArgs e)
6868
var temp = new ToolStripMenuItemEx
6969
{
7070
Tag = i,
71-
Text = $"{(quotient > 0 ? quotient : "")}&{remainder}x"
71+
Text = $"{(quotient is not 0L ? quotient.ToString() : string.Empty)}&{remainder}x",
7272
};
7373
temp.Click += this.WindowSize_Click;
7474
WindowSizeSubMenu.DropDownItems.Insert(i - 1, temp);

src/BizHawk.Emulation.Common/filetype_detectors/SatellaviewFileTypeDetector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public SatellaviewHeader(ReadOnlySpan<byte> header)
8787
=> _header = header;
8888

8989
public override string ToString()
90-
=> $"[{ContentTypeField >> 4:X1}] {Title} r{Revision} ({(IsSelfDestructing ? RemainingPlays : "unlimited")} plays left)";
90+
=> $"[{ContentTypeField >> 4:X1}] {Title} r{Revision} ({(IsSelfDestructing ? RemainingPlays.ToString() : "unlimited")} plays left)";
9191

9292
public bool VerifyChecksum(ReadOnlySpan<byte> rom)
9393
=> true; //TODO need to parse page mapping from offset 0x20..0x23 in order to calculate this

src/BizHawk.Emulation.Cores/CPUs/FairchildF8/F3850.Disassembler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ private static string Result(string format, Func<ushort, byte> read, ref ushort
2828
if (format.ContainsOrdinal('d'))
2929
{
3030
var b = unchecked((sbyte)read(addr++));
31-
format = format.Replace("d", $"{(b < 0 ? '-' : '+')}{(b < 0 ? -b : b):X2}h");
31+
format = format.Replace("d", $"{(b < 0 ? '-' : '+')}{Math.Abs((short) b):X2}h");
3232
}
3333

3434
return format;

src/BizHawk.Emulation.Cores/CPUs/Z80A/NewDisassembler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ private static string Result(string format, Func<ushort, byte> read, ref ushort
374374
if (format.ContainsOrdinal('d'))
375375
{
376376
var b = unchecked ((sbyte) read(addr++));
377-
format = format.Replace("d", $"{(b < 0 ? '-' : '+')}{(b < 0 ? -b : b):X2}h");
377+
format = format.Replace("d", $"{(b < 0 ? '-' : '+')}{Math.Abs((short) b):X2}h");
378378
}
379379

380380
return format;

0 commit comments

Comments
 (0)