-
Notifications
You must be signed in to change notification settings - Fork 14
feat: implement separate diagnostics for variables named '_' and unnecessary typed discards #889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2ff47e5
e258323
6a77a15
bfa4aca
b37baeb
52157e6
4c09002
55b6e25
eace4ef
73e78d6
65d633b
debb243
5886208
31a38e3
3cbca61
20f5320
3b354e8
7f47db2
2cf2205
ca958e1
96df7cd
e259040
2caf4c6
ff42244
bb980db
f9ec0d3
3d6c231
23cb422
9271a0f
fe12417
d1b2b7e
1f4965b
9eb5794
4701aa2
14f6f88
28f94a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # PH2152: Avoid unnecessary typed discard | ||
|
|
||
| | Property | Value | | ||
| |--|--| | ||
| | Package | [Philips.CodeAnalysis.MaintainabilityAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MaintainabilityAnalyzers) | | ||
| | Diagnostic ID | PH2152 | | ||
| | Category | [Naming](../Naming.md) | | ||
| | Analyzer | [AvoidVariableNamedUnderscoreAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MaintainabilityAnalyzers/Naming/AvoidVariableNamedUnderscoreAnalyzer.cs) | ||
| | CodeFix | No | | ||
| | Severity | Error | | ||
| | Enabled By Default | Yes | | ||
|
|
||
| ## Introduction | ||
|
|
||
| This rule flags unnecessary typed discards like `out string _` when anonymous discard `out _` would work equally well. | ||
|
|
||
| This rule does not flag typed discards that are necessary for overload resolution. | ||
|
|
||
| ## Reason | ||
|
|
||
| Anonymous discards have clearer intent and are more concise than typed discards when the type information is not needed for overload resolution. | ||
|
|
||
| ## How to fix violations | ||
|
|
||
| Replace typed discards with anonymous discards when the type is not needed for overload resolution. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Unnecessary Typed Discards (Flagged) | ||
|
|
||
| ```csharp | ||
| // These are flagged - unnecessary typed discards when anonymous would work | ||
| GetValue(out string _); // Should be: GetValue(out _); | ||
| TryParseHelper("123", out int _); // Should be: TryParseHelper("123", out _); | ||
|
|
||
| public void GetValue(out string value) { value = "test"; } | ||
| public bool TryParseHelper(string input, out int result) { result = 42; return true; } | ||
| ``` | ||
|
|
||
| ### Necessary Typed Discards (NOT Flagged) | ||
|
|
||
| ```csharp | ||
| // These are NOT flagged - necessary for overload resolution | ||
| Parse("123", out int _); // Disambiguates from Parse(string, out string) | ||
| Parse("test", out string _); // Disambiguates from Parse(string, out int) | ||
|
|
||
| // Example overloaded methods | ||
| public bool Parse(string input, out int result) { result = 42; return true; } | ||
| public bool Parse(string input, out string result) { result = input; return true; } | ||
| ``` | ||
|
|
||
| ### Correct Usage (NOT Flagged) | ||
|
|
||
| ```csharp | ||
| // Use proper anonymous discards - these are NOT flagged | ||
| GetValue(out _); // Anonymous discard in out parameter | ||
| TryParseHelper("123", out _); // Anonymous discard in out parameter | ||
|
|
||
| // Typed discards are allowed when needed for overload resolution | ||
| Parse("123", out int _); // Needed to select correct overload | ||
| Parse("test", out string _); // Needed to select correct overload | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| This rule is enabled by default with Error severity. | ||
|
|
||
| ## Suppression | ||
|
|
||
| ```csharp | ||
| [SuppressMessage("Philips.CodeAnalysis.MaintainabilityAnalyzers", "PH2152:Avoid unnecessary typed discard", Justification = "Reviewed.")] | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,16 +10,38 @@ | |
| namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Naming | ||
| { | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public class AvoidVariableNamedUnderscoreAnalyzer : SingleDiagnosticAnalyzer | ||
| public class AvoidVariableNamedUnderscoreAnalyzer : DiagnosticAnalyzerBase | ||
| { | ||
| private const string Title = @"Avoid variables named exactly '_'"; | ||
| private const string MessageFormat = @"Variable '{0}' is named '_' which can be confused with a discard. Consider using a discard or a more descriptive variable name"; | ||
| private const string Description = @"Variables named exactly '_' can be confused with C# discards. Use either a proper discard or a more descriptive variable name."; | ||
|
|
||
| public AvoidVariableNamedUnderscoreAnalyzer() | ||
| : base(DiagnosticId.AvoidVariableNamedUnderscore, Title, MessageFormat, Description, Categories.Naming) | ||
| { | ||
| } | ||
| private const string VariableNamedUnderscoreTitle = @"Avoid variables named '_'"; | ||
| private const string VariableNamedUnderscoreMessageFormat = @"Avoid variable named '{0}' as it can be confused with discards"; | ||
| private const string VariableNamedUnderscoreDescription = @"Avoid variables named exactly '_' which can be confused with discards."; | ||
|
|
||
| private const string UnnecessaryTypedDiscardTitle = @"Avoid unnecessary typed discard"; | ||
| private const string UnnecessaryTypedDiscardMessageFormat = @"Use anonymous discard '_' instead of typed discard '{0}' when type is not needed"; | ||
| private const string UnnecessaryTypedDiscardDescription = @"Prefer anonymous discards over typed discards when the type is not needed for overload resolution."; | ||
|
|
||
| public static readonly DiagnosticDescriptor VariableNamedUnderscoreRule = new( | ||
| DiagnosticId.AvoidVariableNamedUnderscore.ToId(), | ||
| VariableNamedUnderscoreTitle, | ||
| VariableNamedUnderscoreMessageFormat, | ||
| Categories.Naming, | ||
| DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true, | ||
| description: VariableNamedUnderscoreDescription, | ||
| helpLinkUri: DiagnosticId.AvoidVariableNamedUnderscore.ToHelpLinkUrl()); | ||
|
|
||
| public static readonly DiagnosticDescriptor UnnecessaryTypedDiscardRule = new( | ||
| DiagnosticId.AvoidUnnecessaryTypedDiscard.ToId(), | ||
| UnnecessaryTypedDiscardTitle, | ||
| UnnecessaryTypedDiscardMessageFormat, | ||
| Categories.Naming, | ||
| DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true, | ||
| description: UnnecessaryTypedDiscardDescription, | ||
| helpLinkUri: DiagnosticId.AvoidUnnecessaryTypedDiscard.ToHelpLinkUrl()); | ||
|
|
||
| public override System.Collections.Immutable.ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => | ||
| System.Collections.Immutable.ImmutableArray.Create(VariableNamedUnderscoreRule, UnnecessaryTypedDiscardRule); | ||
|
|
||
| protected override void InitializeCompilation(CompilationStartAnalysisContext context) | ||
| { | ||
|
|
@@ -43,7 +65,7 @@ private void AnalyzeForEachStatement(SyntaxNodeAnalysisContext context) | |
| } | ||
|
|
||
| Location location = foreachStatement.GetLocation(); | ||
| var diagnostic = Diagnostic.Create(Rule, location, foreachStatement.Identifier.ValueText); | ||
| var diagnostic = Diagnostic.Create(VariableNamedUnderscoreRule, location, foreachStatement.Identifier.ValueText); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
|
|
||
|
|
@@ -68,7 +90,7 @@ private void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context) | |
| } | ||
|
|
||
| Location location = variableDeclaration.GetLocation(); | ||
| var diagnostic = Diagnostic.Create(Rule, location, identifier); | ||
| var diagnostic = Diagnostic.Create(VariableNamedUnderscoreRule, location, identifier); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| } | ||
|
|
@@ -88,21 +110,99 @@ private void AnalyzeArgument(SyntaxNodeAnalysisContext context) | |
| return; | ||
| } | ||
|
|
||
| // Check if it's a variable declaration | ||
| // Check if it's a typed discard (e.g., out int _) | ||
| if (argument.Expression is DeclarationExpressionSyntax declaration && | ||
| declaration.Designation is SingleVariableDesignationSyntax variable) | ||
| declaration.Designation is DiscardDesignationSyntax discard) | ||
| { | ||
| if (variable.Identifier.ValueText != "_") | ||
| // This is a typed discard (e.g., out int _) | ||
| // Only flag if the type is not needed for overload resolution | ||
| if (!IsTypedDiscardNecessaryForOverloadResolution(context, argument)) | ||
| { | ||
| return; | ||
| Location location = discard.GetLocation(); | ||
| var diagnostic = Diagnostic.Create(UnnecessaryTypedDiscardRule, location, "_"); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Check if it's a variable declaration with identifier "_" | ||
| if (argument.Expression is DeclarationExpressionSyntax declaration2 && | ||
| declaration2.Designation is SingleVariableDesignationSyntax variable && | ||
| variable.Identifier.ValueText == "_") | ||
| { | ||
| Location location = variable.Identifier.GetLocation(); | ||
| var diagnostic = Diagnostic.Create(Rule, location, variable.Identifier.ValueText); | ||
| var diagnostic = Diagnostic.Create(VariableNamedUnderscoreRule, location, variable.Identifier.ValueText); | ||
| context.ReportDiagnostic(diagnostic); | ||
| // Note: DiscardDesignationSyntax represents proper discards (e.g., "out _" or "out string _") | ||
| // and should not be flagged, so we don't handle this case. | ||
| } | ||
|
|
||
| // Note: We don't flag IdentifierNameSyntax("_") because that represents | ||
| // anonymous discards like "out _" which are the preferred form | ||
| } | ||
|
|
||
| private static bool IsTypedDiscardNecessaryForOverloadResolution(SyntaxNodeAnalysisContext context, ArgumentSyntax argument) | ||
| { | ||
| // Find the method call containing this argument | ||
| InvocationExpressionSyntax invocation = argument.Ancestors() | ||
| .OfType<InvocationExpressionSyntax>() | ||
| .FirstOrDefault(); | ||
| if (invocation is null) | ||
| { | ||
| return false; // Can't find method call, allow the flag | ||
| } | ||
|
|
||
| // Early check: if the invocation doesn't look like it could have overloads, | ||
| // we can avoid semantic model access entirely | ||
| if (invocation.Expression is not (MemberAccessExpressionSyntax or IdentifierNameSyntax)) | ||
| { | ||
| return false; // Complex expression, allow the flag | ||
| } | ||
|
|
||
| // Get semantic information about the method - moved lower for performance | ||
| SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, maybe you can do without the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved the SemanticModel access lower in the method and added an early syntax check to avoid semantic analysis when possible. This improves performance by reducing unnecessary semantic model calls. Also simplified the LINQ expression to count overloads more efficiently. Commit 14f6f88. |
||
| if (symbolInfo.Symbol is not IMethodSymbol method) | ||
| { | ||
| return false; // Can't resolve method, allow the flag | ||
| } | ||
|
|
||
| // Get the argument position - handle named arguments | ||
| var argumentIndex = GetArgumentIndex(invocation.ArgumentList.Arguments, argument, method); | ||
| if (argumentIndex < 0 || argumentIndex >= method.Parameters.Length) | ||
| { | ||
| return false; // Invalid position, allow the flag | ||
| } | ||
|
|
||
| // Simple heuristic: check if there are method overloads with different out parameter types at this position | ||
| INamedTypeSymbol containingType = method.ContainingType; | ||
| System.Collections.Generic.IEnumerable<IMethodSymbol> methodsWithSameName = containingType.GetMembers(method.Name).OfType<IMethodSymbol>(); | ||
|
|
||
| // Count how many overloads have an out parameter at this position | ||
| var overloadCount = methodsWithSameName | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This returns true in extraneous cases. There could be an overload at this position, but other things could be happening at other positions such that disambiguation works. Use ReplaceNode and then do a speculative binding (GetSpeculativeSymbolInfo), and see if it picks the same method.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implemented speculative binding with ReplaceNode and GetSpeculativeSymbolInfo as suggested. The analyzer now creates a speculative invocation with anonymous discard and checks if the same method would be selected. If the speculative binding resolves to a different method or fails, the typed discard is considered necessary for overload resolution. Currently fine-tuning the implementation to handle edge cases correctly. Commit 3d6c231. |
||
| .Where(m => argumentIndex < m.Parameters.Length) | ||
| .Count(m => m.Parameters[argumentIndex].RefKind == RefKind.Out); | ||
|
|
||
| // If there are multiple overloads with out parameters at this position, | ||
| // then the typed discard might be necessary for overload resolution | ||
| return overloadCount > 1; | ||
| } | ||
|
|
||
| private static int GetArgumentIndex(SeparatedSyntaxList<ArgumentSyntax> arguments, ArgumentSyntax targetArgument, IMethodSymbol method) | ||
| { | ||
| // Handle named arguments by checking if the target argument has a name | ||
| if (targetArgument.NameColon != null) | ||
| { | ||
| var parameterName = targetArgument.NameColon.Name.Identifier.ValueText; | ||
| for (var i = 0; i < method.Parameters.Length; i++) | ||
| { | ||
| if (method.Parameters[i].Name == parameterName) | ||
| { | ||
| return i; | ||
| } | ||
| } | ||
| return -1; // Parameter name not found | ||
| } | ||
|
|
||
| // For positional arguments, just get the index | ||
| return arguments.IndexOf(targetArgument); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If easy to implement, let's have more accurate Title/MessageFormat/Description for this scenario. E.g., "Avoid variable named '_'" or "Avoid unnecessary typed discard"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the diagnostic title to "Avoid unnecessary typed discards and variables named '_'" and improved the message format to be more generic, covering both typed discard and variable naming scenarios. Commit e259040.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... maybe we should have a separate Diagnostic Id and message (with same class)? If so, claim PH2153 next.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented separate diagnostic IDs as suggested: PH2147 for variables named '_' and PH2153 for unnecessary typed discards. This provides more precise diagnostic messages and allows for different handling of each scenario. The analyzer now uses DiagnosticAnalyzerBase with dual diagnostic support. Commit 3d6c231.