diff --git a/ExternalProjects/BizHawk.Analyzer/AmbiguousMoneyToFloatConversionAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/AmbiguousMoneyToFloatConversionAnalyzer.cs index 55d02b1f53..60fd92cbcb 100644 --- a/ExternalProjects/BizHawk.Analyzer/AmbiguousMoneyToFloatConversionAnalyzer.cs +++ b/ExternalProjects/BizHawk.Analyzer/AmbiguousMoneyToFloatConversionAnalyzer.cs @@ -19,54 +19,52 @@ public sealed class AmbiguousMoneyToFloatConversionAnalyzer : DiagnosticAnalyzer { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(initContext => - { - initContext.RegisterOperationAction(oac => + context.RegisterOperationAction( + oac => + { + var conversionOp = (IConversionOperation) oac.Operation; + var typeOutput = conversionOp.Type?.SpecialType ?? SpecialType.None; + var typeInput = conversionOp.Operand.Type?.SpecialType ?? SpecialType.None; + bool isToDecimal; + bool isDoublePrecision; + if (typeOutput is SpecialType.System_Decimal) { - var conversionOp = (IConversionOperation) oac.Operation; - var typeOutput = conversionOp.Type?.SpecialType ?? SpecialType.None; - var typeInput = conversionOp.Operand.Type?.SpecialType ?? SpecialType.None; - bool isToDecimal; - bool isDoublePrecision; - if (typeOutput is SpecialType.System_Decimal) - { - if (typeInput is SpecialType.System_Double) isDoublePrecision = true; - else if (typeInput is SpecialType.System_Single) isDoublePrecision = false; - else return; - isToDecimal = true; - } - else if (typeInput is SpecialType.System_Decimal) - { - if (typeOutput is SpecialType.System_Double) isDoublePrecision = true; - else if (typeOutput is SpecialType.System_Single) isDoublePrecision = false; - else return; - isToDecimal = false; - } - else - { - return; - } - var conversionSyn = conversionOp.Syntax; - //TODO check the suggested methods are accessible (i.e. BizHawk.Common is referenced) - oac.ReportDiagnostic(Diagnostic.Create( - DiagAmbiguousMoneyToFloatConversion, - (conversionSyn.Parent?.Kind() is SyntaxKind.CheckedExpression or SyntaxKind.UncheckedExpression - ? conversionSyn.Parent - : conversionSyn).GetLocation(), - conversionOp.IsChecked ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, - additionalLocations: null, - properties: null, - messageArgs: isToDecimal - ? [ - $"new decimal({(isDoublePrecision ? "double" : "float")})", // "checked" - "static NumberExtensions.ConvertToMoneyTruncated", // "unchecked" - ] - : [ - $"decimal.{(isDoublePrecision ? "ConvertToF64" : "ConvertToF32")} ext. (from NumberExtensions)", // "checked" - $"static Decimal.{(isDoublePrecision ? "ToDouble" : "ToSingle")}", // "unchecked" - ])); - }, - OperationKind.Conversion); - }); + if (typeInput is SpecialType.System_Double) isDoublePrecision = true; + else if (typeInput is SpecialType.System_Single) isDoublePrecision = false; + else return; + isToDecimal = true; + } + else if (typeInput is SpecialType.System_Decimal) + { + if (typeOutput is SpecialType.System_Double) isDoublePrecision = true; + else if (typeOutput is SpecialType.System_Single) isDoublePrecision = false; + else return; + isToDecimal = false; + } + else + { + return; + } + var conversionSyn = conversionOp.Syntax; + //TODO check the suggested methods are accessible (i.e. BizHawk.Common is referenced) + oac.ReportDiagnostic(Diagnostic.Create( + DiagAmbiguousMoneyToFloatConversion, + (conversionSyn.Parent?.Kind() is SyntaxKind.CheckedExpression or SyntaxKind.UncheckedExpression + ? conversionSyn.Parent + : conversionSyn).GetLocation(), + conversionOp.IsChecked ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, + additionalLocations: null, + properties: null, + messageArgs: isToDecimal + ? [ + $"new decimal({(isDoublePrecision ? "double" : "float")})", // "checked" + "static NumberExtensions.ConvertToMoneyTruncated", // "unchecked" + ] + : [ + $"decimal.{(isDoublePrecision ? "ConvertToF64" : "ConvertToF32")} ext. (from NumberExtensions)", // "checked" + $"static Decimal.{(isDoublePrecision ? "ToDouble" : "ToSingle")}", // "unchecked" + ])); + }, + OperationKind.Conversion); } } diff --git a/ExternalProjects/BizHawk.Analyzer/ExprBodiedMemberFlowAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/ExprBodiedMemberFlowAnalyzer.cs index 8275c8acc5..27a294f961 100644 --- a/ExternalProjects/BizHawk.Analyzer/ExprBodiedMemberFlowAnalyzer.cs +++ b/ExternalProjects/BizHawk.Analyzer/ExprBodiedMemberFlowAnalyzer.cs @@ -20,102 +20,99 @@ public sealed class ExprBodiedMemberFlowAnalyzer : DiagnosticAnalyzer { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(initContext => - { - var ARROW_ONE_LINE = (' ', ' '); -// var ARROW_POST_SIG = (' ', '\n'); - var ARROW_PRE_BODY = ('\n', ' '); - initContext.RegisterSyntaxNodeAction( - snac => + var ARROW_ONE_LINE = (' ', ' '); +// var ARROW_POST_SIG = (' ', '\n'); + var ARROW_PRE_BODY = ('\n', ' '); + context.RegisterSyntaxNodeAction( + snac => + { + var aecs = (ArrowExpressionClauseSyntax) snac.Node; + (char Before, char After) expectedWhitespace; + string kind; + var parent = aecs.Parent; + if (parent is null) { - var aecs = (ArrowExpressionClauseSyntax) snac.Node; - (char Before, char After) expectedWhitespace; - string kind; - var parent = aecs.Parent; - if (parent is null) - { - snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, aecs.GetLocation(), "Syntax node for expression-bodied member was orphaned?")); + snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, aecs.GetLocation(), "Syntax node for expression-bodied member was orphaned?")); + return; + } + void Flag(string message) + => snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, parent.GetLocation(), message)); + switch (parent) + { + case MethodDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "method"; + break; + case PropertyDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "get-only prop"; + break; + case AccessorDeclarationSyntax ads: + expectedWhitespace = ARROW_ONE_LINE; + switch (ads.Keyword.Text) + { + case "get": + kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "get-indexer" : "getter"; + break; + case "set": + kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "set-indexer" : "setter"; + break; + case "init": + kind = "setter"; + break; + case "add": + kind = "event sub"; + break; + case "remove": + kind = "event unsub"; + break; + default: + Flag($"Expression-bodied accessor was of an unexpected kind: {ads.Parent!.Parent!.GetType().FullName}"); + return; + } + break; + case ConstructorDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "constructor"; + break; + case LocalFunctionStatementSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "local method"; + break; + case IndexerDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "get-only indexer"; + break; + case OperatorDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "overloaded operator"; + break; + case ConversionOperatorDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "overloaded cast operator"; + break; + case DestructorDeclarationSyntax: + expectedWhitespace = ARROW_PRE_BODY; + kind = "finalizer"; + break; + default: + Flag($"Expression-bodied member was of an unexpected kind: {parent.GetType().FullName}"); return; - } - void Flag(string message) - => snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, parent.GetLocation(), message)); - switch (parent) - { - case MethodDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "method"; - break; - case PropertyDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "get-only prop"; - break; - case AccessorDeclarationSyntax ads: - expectedWhitespace = ARROW_ONE_LINE; - switch (ads.Keyword.Text) - { - case "get": - kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "get-indexer" : "getter"; - break; - case "set": - kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "set-indexer" : "setter"; - break; - case "init": - kind = "setter"; - break; - case "add": - kind = "event sub"; - break; - case "remove": - kind = "event unsub"; - break; - default: - Flag($"Expression-bodied accessor was of an unexpected kind: {ads.Parent!.Parent!.GetType().FullName}"); - return; - } - break; - case ConstructorDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "constructor"; - break; - case LocalFunctionStatementSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "local method"; - break; - case IndexerDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "get-only indexer"; - break; - case OperatorDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "overloaded operator"; - break; - case ConversionOperatorDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "overloaded cast operator"; - break; - case DestructorDeclarationSyntax: - expectedWhitespace = ARROW_PRE_BODY; - kind = "finalizer"; - break; - default: - Flag($"Expression-bodied member was of an unexpected kind: {parent.GetType().FullName}"); - return; - } - static string EscapeChar(char c) - => c is '\n' ? "\\n" : c.ToString(); - void Fail() - => Flag($"Whitespace around {kind} arrow syntax should be `{EscapeChar(expectedWhitespace.Before)}=>{EscapeChar(expectedWhitespace.After)}`"); - if ((aecs.ArrowToken.HasLeadingTrivia ? '\n' : ' ') != expectedWhitespace.Before) - { - Fail(); - return; - } + } + static string EscapeChar(char c) + => c is '\n' ? "\\n" : c.ToString(); + void Fail() + => Flag($"Whitespace around {kind} arrow syntax should be `{EscapeChar(expectedWhitespace.Before)}=>{EscapeChar(expectedWhitespace.After)}`"); + if ((aecs.ArrowToken.HasLeadingTrivia ? '\n' : ' ') != expectedWhitespace.Before) + { + Fail(); + return; + } #pragma warning disable BHI3102 // LINQ `Contains(char)` is fine here - var hasLineBreakAfterArrow = aecs.ArrowToken.HasTrailingTrivia && aecs.ArrowToken.TrailingTrivia.ToFullString().Contains('\n'); + var hasLineBreakAfterArrow = aecs.ArrowToken.HasTrailingTrivia && aecs.ArrowToken.TrailingTrivia.ToFullString().Contains('\n'); #pragma warning restore BHI3102 - if ((hasLineBreakAfterArrow ? '\n' : ' ') != expectedWhitespace.After) Fail(); - }, - SyntaxKind.ArrowExpressionClause); - }); + if ((hasLineBreakAfterArrow ? '\n' : ' ') != expectedWhitespace.After) Fail(); + }, + SyntaxKind.ArrowExpressionClause); } } diff --git a/ExternalProjects/BizHawk.Analyzer/HawkSourceAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/HawkSourceAnalyzer.cs index 3a4938b3b7..c1f42a2d8e 100644 --- a/ExternalProjects/BizHawk.Analyzer/HawkSourceAnalyzer.cs +++ b/ExternalProjects/BizHawk.Analyzer/HawkSourceAnalyzer.cs @@ -113,74 +113,72 @@ public class HawkSourceAnalyzer : DiagnosticAnalyzer => aes.OperatorToken.RawKind is (int) SyntaxKind.EqualsToken && aes.Left is IdentifierNameSyntax { Identifier.Text: "_" }; context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - INamedTypeSymbol? invalidOperationExceptionSym = null; - INamedTypeSymbol? switchExpressionExceptionSym = null; - context.RegisterSyntaxNodeAction( - snac => - { - if (invalidOperationExceptionSym is null) + context.RegisterCompilationStartAction(initContext => + { + var invalidOperationExceptionSym = initContext.Compilation.GetTypeByMetadataName("System.InvalidOperationException")!; + var switchExpressionExceptionSym = initContext.Compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.SwitchExpressionException"); + initContext.RegisterSyntaxNodeAction( + snac => { - invalidOperationExceptionSym = snac.Compilation.GetTypeByMetadataName("System.InvalidOperationException")!; - switchExpressionExceptionSym = snac.Compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.SwitchExpressionException"); - } - switch (snac.Node) - { - case AnonymousMethodExpressionSyntax: - snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonDelegates, snac.Node.GetLocation())); - break; - case AnonymousObjectCreationExpressionSyntax: - snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonClasses, snac.Node.GetLocation())); - break; - case AssignmentExpressionSyntax aes: - if (!IsDiscard(aes)) break; - if (snac.SemanticModel.GetSymbolInfo(aes.Right, snac.CancellationToken).Symbol?.Kind is not SymbolKind.Local) break; - snac.ReportDiagnostic(Diagnostic.Create(DiagNoDiscardingLocals, snac.Node.GetLocation())); - break; - case CollectionExpressionSyntax ces: - var cesError = CheckSpacingInList(ces.Elements, ces.OpenBracketToken, ces.ToString); - if (cesError is not null) snac.ReportDiagnostic(Diagnostic.Create(DiagListExprSpacing, ces.GetLocation(), cesError)); - break; - case InterpolatedStringExpressionSyntax ises: - if (ises.StringStartToken.Text[0] is '@') snac.ReportDiagnostic(Diagnostic.Create(DiagInterpStringIsDollarAt, ises.GetLocation())); - break; - case ListPatternSyntax lps: - var lpsError = CheckSpacingInList(lps.Patterns, lps.OpenBracketToken, lps.ToString); - if (lpsError is not null) snac.ReportDiagnostic(Diagnostic.Create(DiagListExprSpacing, lps.GetLocation(), lpsError)); - break; - case QueryExpressionSyntax: - snac.ReportDiagnostic(Diagnostic.Create(DiagNoQueryExpression, snac.Node.GetLocation())); - break; - case RecordDeclarationSyntax rds when rds.ClassOrStructKeyword.ToString() is not "class": // `record struct`s don't use this kind - snac.ReportDiagnostic(Diagnostic.Create(DiagRecordImplicitlyRefType, rds.GetLocation())); - break; - case SwitchExpressionArmSyntax { WhenClause: null, Pattern: DiscardPatternSyntax, Expression: ThrowExpressionSyntax tes }: - var thrownExceptionType = snac.SemanticModel.GetThrownExceptionType(tes); - if (thrownExceptionType is null) - { - snac.ReportDiagnostic(Diagnostic.Create( - DiagSwitchShouldThrowIOE, - tes.GetLocation(), - DiagnosticSeverity.Warning, - additionalLocations: null, - properties: null, - ERR_MSG_SWITCH_THROWS_UNKNOWN)); - } - else if (!invalidOperationExceptionSym.Matches(thrownExceptionType) && switchExpressionExceptionSym?.Matches(thrownExceptionType) != true) - { - snac.ReportDiagnostic(Diagnostic.Create(DiagSwitchShouldThrowIOE, tes.GetLocation(), ERR_MSG_SWITCH_THROWS_WRONG_TYPE)); - } - // else correct usage, do not flag - break; - } - }, - SyntaxKind.AnonymousObjectCreationExpression, - SyntaxKind.AnonymousMethodExpression, - SyntaxKind.CollectionExpression, - SyntaxKind.InterpolatedStringExpression, - SyntaxKind.ListPattern, - SyntaxKind.QueryExpression, - SyntaxKind.RecordDeclaration, - SyntaxKind.SimpleAssignmentExpression, - SyntaxKind.SwitchExpressionArm); + switch (snac.Node) + { + case AnonymousMethodExpressionSyntax: + snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonDelegates, snac.Node.GetLocation())); + break; + case AnonymousObjectCreationExpressionSyntax: + snac.ReportDiagnostic(Diagnostic.Create(DiagNoAnonClasses, snac.Node.GetLocation())); + break; + case AssignmentExpressionSyntax aes: + if (!IsDiscard(aes)) break; + if (snac.SemanticModel.GetSymbolInfo(aes.Right, snac.CancellationToken).Symbol?.Kind is not SymbolKind.Local) break; + snac.ReportDiagnostic(Diagnostic.Create(DiagNoDiscardingLocals, snac.Node.GetLocation())); + break; + case CollectionExpressionSyntax ces: + var cesError = CheckSpacingInList(ces.Elements, ces.OpenBracketToken, ces.ToString); + if (cesError is not null) snac.ReportDiagnostic(Diagnostic.Create(DiagListExprSpacing, ces.GetLocation(), cesError)); + break; + case InterpolatedStringExpressionSyntax ises: + if (ises.StringStartToken.Text[0] is '@') snac.ReportDiagnostic(Diagnostic.Create(DiagInterpStringIsDollarAt, ises.GetLocation())); + break; + case ListPatternSyntax lps: + var lpsError = CheckSpacingInList(lps.Patterns, lps.OpenBracketToken, lps.ToString); + if (lpsError is not null) snac.ReportDiagnostic(Diagnostic.Create(DiagListExprSpacing, lps.GetLocation(), lpsError)); + break; + case QueryExpressionSyntax: + snac.ReportDiagnostic(Diagnostic.Create(DiagNoQueryExpression, snac.Node.GetLocation())); + break; + case RecordDeclarationSyntax rds when rds.ClassOrStructKeyword.ToString() is not "class": // `record struct`s don't use this kind + snac.ReportDiagnostic(Diagnostic.Create(DiagRecordImplicitlyRefType, rds.GetLocation())); + break; + case SwitchExpressionArmSyntax { WhenClause: null, Pattern: DiscardPatternSyntax, Expression: ThrowExpressionSyntax tes }: + var thrownExceptionType = snac.SemanticModel.GetThrownExceptionType(tes); + if (thrownExceptionType is null) + { + snac.ReportDiagnostic(Diagnostic.Create( + DiagSwitchShouldThrowIOE, + tes.GetLocation(), + DiagnosticSeverity.Warning, + additionalLocations: null, + properties: null, + ERR_MSG_SWITCH_THROWS_UNKNOWN)); + } + else if (!invalidOperationExceptionSym.Matches(thrownExceptionType) && switchExpressionExceptionSym?.Matches(thrownExceptionType) != true) + { + snac.ReportDiagnostic(Diagnostic.Create(DiagSwitchShouldThrowIOE, tes.GetLocation(), ERR_MSG_SWITCH_THROWS_WRONG_TYPE)); + } + // else correct usage, do not flag + break; + } + }, + SyntaxKind.AnonymousObjectCreationExpression, + SyntaxKind.AnonymousMethodExpression, + SyntaxKind.CollectionExpression, + SyntaxKind.InterpolatedStringExpression, + SyntaxKind.ListPattern, + SyntaxKind.QueryExpression, + SyntaxKind.RecordDeclaration, + SyntaxKind.SimpleAssignmentExpression, + SyntaxKind.SwitchExpressionArm); + }); } } diff --git a/ExternalProjects/BizHawk.Analyzer/TernaryInferredTypeMismatchAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/TernaryInferredTypeMismatchAnalyzer.cs index c16b99a076..6f82cc696f 100644 --- a/ExternalProjects/BizHawk.Analyzer/TernaryInferredTypeMismatchAnalyzer.cs +++ b/ExternalProjects/BizHawk.Analyzer/TernaryInferredTypeMismatchAnalyzer.cs @@ -19,64 +19,62 @@ public sealed class TernaryInferredTypeMismatchAnalyzer : DiagnosticAnalyzer { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(initContext => - { - initContext.RegisterOperationAction(oac => - { - var ifelseOrTernaryOp = (IConditionalOperation) oac.Operation; - if (ifelseOrTernaryOp.WhenFalse is null) return; - var parent = ifelseOrTernaryOp.Parent!; - if (parent.Kind is OperationKind.Conversion) parent = parent.Parent!; - if (parent.Kind is not OperationKind.Interpolation) return; - var ternaryOp = ifelseOrTernaryOp; - var typeTernary = ternaryOp.Type!; + context.RegisterOperationAction( + oac => + { + var ifelseOrTernaryOp = (IConditionalOperation) oac.Operation; + if (ifelseOrTernaryOp.WhenFalse is null) return; + var parent = ifelseOrTernaryOp.Parent!; + if (parent.Kind is OperationKind.Conversion) parent = parent.Parent!; + if (parent.Kind is not OperationKind.Interpolation) return; + var ternaryOp = ifelseOrTernaryOp; + var typeTernary = ternaryOp.Type!; #if false // never hit; either both branches are string and there are no conversions, or conversions are necessary - if (typeTernary.SpecialType is SpecialType.System_String) return; + if (typeTernary.SpecialType is SpecialType.System_String) return; #endif - var lhs = ternaryOp.WhenTrue; - var rhs = ternaryOp.WhenFalse; + var lhs = ternaryOp.WhenTrue; + var rhs = ternaryOp.WhenFalse; - static IOperation TrimImplicitCast(IOperation op) - => op is IConversionOperation { Conversion.IsImplicit: true } implCastOp ? implCastOp.Operand : op; - var typeLHS = TrimImplicitCast(lhs).Type!; - var typeRHS = TrimImplicitCast(rhs).Type!; - if (typeLHS.Matches(typeRHS)) return; // unnecessary conversion operators on each branch? seen with `? this : this` + static IOperation TrimImplicitCast(IOperation op) + => op is IConversionOperation { Conversion.IsImplicit: true } implCastOp ? implCastOp.Operand : op; + var typeLHS = TrimImplicitCast(lhs).Type!; + var typeRHS = TrimImplicitCast(rhs).Type!; + if (typeLHS.Matches(typeRHS)) return; // unnecessary conversion operators on each branch? seen with `? this : this` - const string ERR_MSG_OBJECT = "missing ToString means ternary branches are upcast to object"; - var fatal = false; - IOperation flaggedOp = ternaryOp; - string message; - if (typeLHS.SpecialType is SpecialType.System_String) - { - flaggedOp = rhs; - message = ERR_MSG_OBJECT; - } - else if (typeRHS.SpecialType is SpecialType.System_String) - { - flaggedOp = lhs; - message = ERR_MSG_OBJECT; - } - else if (typeTernary.SpecialType is SpecialType.System_Object) - { - fatal = true; - message = "ternary branches are upcast to object! add ToString calls, or convert one to the other's type"; - } - else - { - // if one's already an e.g. int literal, flag the e.g. char literal - if (typeTernary.Matches(typeLHS)) flaggedOp = rhs; - else if (typeTernary.Matches(typeRHS)) flaggedOp = lhs; - message = $"ternary branches are converted to {typeTernary} before serialisation, possibly unintended"; - } - oac.ReportDiagnostic(Diagnostic.Create( - DiagTernaryInferredTypeMismatch, - flaggedOp.Syntax.GetLocation(), - fatal ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, - additionalLocations: null, - properties: null, - messageArgs: message)); - }, - OperationKind.Conditional); - }); + const string ERR_MSG_OBJECT = "missing ToString means ternary branches are upcast to object"; + var fatal = false; + IOperation flaggedOp = ternaryOp; + string message; + if (typeLHS.SpecialType is SpecialType.System_String) + { + flaggedOp = rhs; + message = ERR_MSG_OBJECT; + } + else if (typeRHS.SpecialType is SpecialType.System_String) + { + flaggedOp = lhs; + message = ERR_MSG_OBJECT; + } + else if (typeTernary.SpecialType is SpecialType.System_Object) + { + fatal = true; + message = "ternary branches are upcast to object! add ToString calls, or convert one to the other's type"; + } + else + { + // if one's already an e.g. int literal, flag the e.g. char literal + if (typeTernary.Matches(typeLHS)) flaggedOp = rhs; + else if (typeTernary.Matches(typeRHS)) flaggedOp = lhs; + message = $"ternary branches are converted to {typeTernary} before serialisation, possibly unintended"; + } + oac.ReportDiagnostic(Diagnostic.Create( + DiagTernaryInferredTypeMismatch, + flaggedOp.Syntax.GetLocation(), + fatal ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, + additionalLocations: null, + properties: null, + messageArgs: message)); + }, + OperationKind.Conditional); } } diff --git a/ExternalProjects/BizHawk.Analyzer/UseNameofOperatorAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/UseNameofOperatorAnalyzer.cs index 3ebf94a453..dd1dd582af 100644 --- a/ExternalProjects/BizHawk.Analyzer/UseNameofOperatorAnalyzer.cs +++ b/ExternalProjects/BizHawk.Analyzer/UseNameofOperatorAnalyzer.cs @@ -27,39 +27,41 @@ public sealed class UseNameofOperatorAnalyzer : DiagnosticAnalyzer { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - ISymbol? memberInfoDotNameSym = null; - ISymbol? typeDotToStringSym = null; - context.RegisterSyntaxNodeAction( - snac => - { - memberInfoDotNameSym ??= snac.Compilation.GetTypeByMetadataName("System.Reflection.MemberInfo")!.GetMembers("Name")[0]; - typeDotToStringSym ??= snac.Compilation.GetTypeByMetadataName("System.Type")! - .GetMembers(WellKnownMemberNames.ObjectToString)[0]; - var toes = (TypeOfExpressionSyntax) snac.Node; - switch (toes.Parent) + context.RegisterCompilationStartAction(initContext => + { + var memberInfoDotNameSym = initContext.Compilation.GetTypeByMetadataName("System.Reflection.MemberInfo")! + .GetMembers("Name")[0]; + var typeDotToStringSym = initContext.Compilation.GetTypeByMetadataName("System.Type")! + .GetMembers(WellKnownMemberNames.ObjectToString)[0]; + initContext.RegisterSyntaxNodeAction( + snac => { - case BinaryExpressionSyntax bes: - if ((ReferenceEquals(toes, bes.Left) ? bes.Right : bes.Left) is LiteralExpressionSyntax { Token.RawKind: (int) SyntaxKind.StringLiteralToken }) - { - snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, toes.GetLocation(), toes.Type.GetText(), " in string concatenation")); - } - break; - case InterpolationSyntax: - snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, toes.GetLocation(), toes.Type.GetText(), " in string interpolation")); - break; - case MemberAccessExpressionSyntax maes1: - var accessed = snac.SemanticModel.GetSymbolInfo(maes1.Name, snac.CancellationToken).Symbol; - if (memberInfoDotNameSym.Matches(accessed)) - { - snac.ReportDiagnostic(Diagnostic.Create(DiagUseNameof, maes1.GetLocation(), toes.Type.GetText())); - } - else if (typeDotToStringSym.Matches(accessed)) - { - snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, maes1.GetLocation(), toes.Type.GetText(), ".ToString()")); - } - break; - } - }, - SyntaxKind.TypeOfExpression); + var toes = (TypeOfExpressionSyntax) snac.Node; + switch (toes.Parent) + { + case BinaryExpressionSyntax bes: + if ((ReferenceEquals(toes, bes.Left) ? bes.Right : bes.Left) is LiteralExpressionSyntax { Token.RawKind: (int) SyntaxKind.StringLiteralToken }) + { + snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, toes.GetLocation(), toes.Type.GetText(), " in string concatenation")); + } + break; + case InterpolationSyntax: + snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, toes.GetLocation(), toes.Type.GetText(), " in string interpolation")); + break; + case MemberAccessExpressionSyntax maes1: + var accessed = snac.SemanticModel.GetSymbolInfo(maes1.Name, snac.CancellationToken).Symbol; + if (memberInfoDotNameSym.Matches(accessed)) + { + snac.ReportDiagnostic(Diagnostic.Create(DiagUseNameof, maes1.GetLocation(), toes.Type.GetText())); + } + else if (typeDotToStringSym.Matches(accessed)) + { + snac.ReportDiagnostic(Diagnostic.Create(DiagNoToStringOnType, maes1.GetLocation(), toes.Type.GetText(), ".ToString()")); + } + break; + } + }, + SyntaxKind.TypeOfExpression); + }); } }