Add Analyzer rule to warn of `FirstOrDefault` on list of structs

This commit is contained in:
YoshiRulz 2022-07-28 03:05:27 +10:00
parent 8453c0e44d
commit 140e340a8d
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
7 changed files with 70 additions and 10 deletions

View File

@ -28,6 +28,9 @@
<!-- Don't call typeof(T).ToString(), use nameof operator or typeof(T).FullName -->
<Rule Id="BHI1103" Action="Error" />
<!-- Call to FirstOrDefault when elements are of a value type; FirstOrNull may have been intended -->
<Rule Id="BHI3100" Action="Error" />
<!-- Throw NotImplementedException from methods/props marked [FeatureNotImplemented] -->
<Rule Id="BHI3300" Action="Error" />
</Rules>

View File

@ -0,0 +1,51 @@
namespace BizHawk.Analyzers;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class FirstOrDefaultOnStructAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor DiagUseFirstOrNull = new(
id: "BHI3100",
title: "Call to FirstOrDefault when elements are of a value type; FirstOrNull may have been intended",
messageFormat: "Call to FirstOrDefault when elements are of a value type; did you mean FirstOrNull?",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagUseFirstOrNull);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(initContext =>
{
if (initContext.Compilation.GetTypeByMetadataName("BizHawk.Common.CollectionExtensions.CollectionExtensions") is null) return; // project does not have BizHawk.Common dependency
var linqExtClassSym = initContext.Compilation.GetTypeByMetadataName("System.Linq.Enumerable")!;
IMethodSymbol? firstOrDefaultNoPredSym = null;
IMethodSymbol? firstOrDefaultWithPredSym = null;
foreach (var sym in linqExtClassSym.GetMembers("FirstOrDefault").Cast<IMethodSymbol>())
{
if (sym.Parameters.Length is 2) firstOrDefaultWithPredSym = sym;
else firstOrDefaultNoPredSym = sym;
}
initContext.RegisterOperationAction(
oac =>
{
var operation = (IInvocationOperation) oac.Operation;
var calledSym = operation.TargetMethod.ConstructedFrom;
if (!(firstOrDefaultWithPredSym!.Matches(calledSym) || firstOrDefaultNoPredSym!.Matches(calledSym))) return;
var receiverExprType = (INamedTypeSymbol) operation.SemanticModel.GetTypeInfo((CSharpSyntaxNode) operation.Arguments[0].Syntax)!.ConvertedType!;
if (receiverExprType.TypeArguments[0].IsValueType) oac.ReportDiagnostic(Diagnostic.Create(DiagUseFirstOrNull, operation.Syntax.GetLocation()));
},
OperationKind.Invocation);
});
}
}

Binary file not shown.

View File

@ -6,6 +6,7 @@ using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
namespace BizHawk.Client.EmuHawk
{
@ -197,7 +198,7 @@ namespace BizHawk.Client.EmuHawk
{
if (e.IsPressed(Keys.Enter) || e.IsPressed(Keys.Tab))
{
var k = HotkeyInfo.AllHotkeys.FirstOrDefault(kvp => string.Compare(kvp.Value.DisplayName, SearchBox.Text, true) is 0).Key;
var k = HotkeyInfo.AllHotkeys.FirstOrNull(kvp => string.Compare(kvp.Value.DisplayName, SearchBox.Text, true) is 0)?.Key;
// Found
if (k is not null)

View File

@ -16,6 +16,7 @@ using BizHawk.Emulation.Common;
using BizHawk.Client.Common;
using BizHawk.Client.EmuHawk.Properties;
using BizHawk.Client.EmuHawk.ToolExtensions;
using BizHawk.Common.CollectionExtensions;
namespace BizHawk.Client.EmuHawk
{
@ -242,13 +243,13 @@ namespace BizHawk.Client.EmuHawk
{
if (_textTable.Any())
{
var byteArr = new List<byte>();
foreach (var chr in str)
var byteArr = new byte[str.Length];
for (var i = 0; i < str.Length; i++)
{
byteArr.Add((byte)_textTable.FirstOrDefault(kvp => kvp.Value == chr).Key);
var c = str[i];
byteArr[i] = (byte) (_textTable.FirstOrNull(kvp => kvp.Value == c)?.Key ?? 0);
}
return byteArr.ToArray();
return byteArr;
}
return str.Select(Convert.ToByte).ToArray();

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using BizHawk.Common.CollectionExtensions;
namespace BizHawk.Common
{
/// <summary>
@ -257,7 +259,8 @@ namespace BizHawk.Common
}
/// <summary>finds an ArchiveItem with the specified name (path) within the archive; returns null if it doesnt exist</summary>
public HawkArchiveFileItem? FindArchiveMember(string? name) => ArchiveItems.FirstOrDefault(ai => ai.Name == name);
public HawkArchiveFileItem? FindArchiveMember(string? name)
=> ArchiveItems.FirstOrNull(ai => ai.Name == name);
/// <returns>a stream for the currently bound file</returns>
/// <exception cref="InvalidOperationException">no stream bound (haven't called <see cref="BindArchiveMember(int)"/> or overload)</exception>

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
//this would be a good place for structural validation
//after this step, we won't want to have to do stuff like that (it will gunk up already sticky code)
@ -109,10 +110,10 @@ namespace BizHawk.Emulation.DiscSystem.CUE
public override string ToString()
{
var idx = Indexes.FirstOrDefault(cci => cci.Number == 1);
if (idx.Number != 1) return $"T#{Number:D2} NO INDEX 1";
var idx = Indexes.FirstOrNull(static cci => cci.Number is 1);
if (idx is null) return $"T#{Number:D2} NO INDEX 1";
var indexlist = string.Join("|", Indexes);
return $"T#{Number:D2} {BlobIndex}:{idx.FileMSF} ({indexlist})";
return $"T#{Number:D2} {BlobIndex}:{idx.Value.FileMSF} ({indexlist})";
}
}