Clean up registration in `BizHawk.Analyzer`
will rebuild in later commit
This commit is contained in:
parent
57921a9206
commit
03bf156a91
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue