Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 18 additions & 142 deletions src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ namespace Microsoft.CodeAnalysis.Diagnostics
{
internal static class DiagnosticHelper
{
/// <summary>
/// See InternalDiagnosticSeverity.Void.
/// </summary>
private const DiagnosticSeverity VoidSeverity = (DiagnosticSeverity)(-2);

private static readonly Diagnostic s_suppressedDiagnostic = Diagnostic.Create(
id: "SUPPRESSED",
category: "",
message: "",
severity: VoidSeverity,
defaultSeverity: VoidSeverity,
isEnabledByDefault: false,
warningLevel: 1,
isSuppressed: true);

/// <summary>
/// Creates a <see cref="Diagnostic"/> instance.
/// </summary>
Expand Down Expand Up @@ -48,17 +63,12 @@ public static Diagnostic Create(
throw new ArgumentNullException(nameof(descriptor));
}

LocalizableString message;
if (messageArgs == null || messageArgs.Length == 0)
{
message = descriptor.MessageFormat;
}
else
if (effectiveSeverity == ReportDiagnostic.Suppress)
{
message = new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs);
return s_suppressedDiagnostic;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. Even suppressed diagnostics need to be created with correct fields, such as ID, message, etc. Note that source suppressed diagnostics show up in the error list (not by default, but can be enabled by changing the filter on Suppression State column) and can also be unsuppressed by right clicking the entry in the error list and executing the Remove Suppression command.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mavasani I thought the (DiagnosticSeverity)(-2) will make the compiler discard the diagnostic completely in

else if (d.Severity == InternalDiagnosticSeverity.Void)
{
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want that - suppressed diagnostics need to be present in the IDE diagnostic system for bunch of features around them to keep working.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point out what feature(s) need them?

}

return CreateWithMessage(descriptor, location, effectiveSeverity, additionalLocations, properties, message);
return Diagnostic.Create(descriptor, location, effectiveSeverity.ToDiagnosticSeverity() ?? descriptor.DefaultSeverity, additionalLocations, properties, messageArgs);
Comment on lines -52 to +72

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public compiler API doesn't have any API that accepts message format and message arguments along with isSuppressed flag.

The only API that takes isSuppressed takes a whole message. I'm using a static diagnostic that should be discarded by the compiler instead of IsSuppressed.

}

/// <summary>
Expand Down Expand Up @@ -206,55 +216,6 @@ static string EncodeIndices(IEnumerable<int> indices, int additionalLocationsLen
}
}

/// <summary>
/// Creates a <see cref="Diagnostic"/> instance.
/// </summary>
/// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
/// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
/// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
/// <param name="additionalLocations">
/// An optional set of additional locations related to the diagnostic.
/// Typically, these are locations of other items referenced in the message.
/// If null, <see cref="Diagnostic.AdditionalLocations"/> will return an empty list.
/// </param>
/// <param name="properties">
/// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic
/// can convey more detailed information to the fixer. If null, <see cref="Diagnostic.Properties"/> will return
/// <see cref="ImmutableDictionary{TKey, TValue}.Empty"/>.
/// </param>
/// <param name="message">Localizable message for the diagnostic.</param>
/// <returns>The <see cref="Diagnostic"/> instance.</returns>
public static Diagnostic CreateWithMessage(
DiagnosticDescriptor descriptor,
Location location,
ReportDiagnostic effectiveSeverity,
IEnumerable<Location>? additionalLocations,
ImmutableDictionary<string, string?>? properties,
LocalizableString message)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}

return Diagnostic.Create(
descriptor.Id,
descriptor.Category,
message,
effectiveSeverity.ToDiagnosticSeverity() ?? descriptor.DefaultSeverity,
descriptor.DefaultSeverity,
descriptor.IsEnabledByDefault,
warningLevel: effectiveSeverity.WithDefaultSeverity(descriptor.DefaultSeverity) == ReportDiagnostic.Error ? 0 : 1,
effectiveSeverity == ReportDiagnostic.Suppress,
descriptor.Title,
descriptor.Description,
descriptor.HelpLinkUri,
location,
additionalLocations,
descriptor.CustomTags,
properties);
}

public static string? GetHelpLinkForDiagnosticId(string id)
{
// TODO: Add documentation for Regex and Json analyzer
Expand All @@ -268,90 +229,5 @@ public static Diagnostic CreateWithMessage(
Debug.Assert(id.StartsWith("IDE", StringComparison.Ordinal));
return $"https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/{id.ToLowerInvariant()}";
}

public sealed class LocalizableStringWithArguments : LocalizableString, IObjectWritable
{
private readonly LocalizableString _messageFormat;
private readonly string[] _formatArguments;

static LocalizableStringWithArguments()
=> ObjectBinder.RegisterTypeReader(typeof(LocalizableStringWithArguments), reader => new LocalizableStringWithArguments(reader));

public LocalizableStringWithArguments(LocalizableString messageFormat, params object[] formatArguments)
{
if (messageFormat == null)
{
throw new ArgumentNullException(nameof(messageFormat));
}

if (formatArguments == null)
{
throw new ArgumentNullException(nameof(formatArguments));
}

_messageFormat = messageFormat;
_formatArguments = new string[formatArguments.Length];
for (var i = 0; i < formatArguments.Length; i++)
{
_formatArguments[i] = $"{formatArguments[i]}";
}
}

private LocalizableStringWithArguments(ObjectReader reader)
{
_messageFormat = (LocalizableString)reader.ReadValue();

var length = reader.ReadInt32();
if (length == 0)
{
_formatArguments = Array.Empty<string>();
}
else
{
using var argumentsBuilderDisposer = ArrayBuilder<string>.GetInstance(length, out var argumentsBuilder);
for (var i = 0; i < length; i++)
{
argumentsBuilder.Add(reader.ReadString());
}

_formatArguments = argumentsBuilder.ToArray();
}
}

bool IObjectWritable.ShouldReuseInSerialization => false;

void IObjectWritable.WriteTo(ObjectWriter writer)
{
writer.WriteValue(_messageFormat);
var length = _formatArguments.Length;
writer.WriteInt32(length);
for (var i = 0; i < length; i++)
{
writer.WriteString(_formatArguments[i]);
}
}

protected override string GetText(IFormatProvider? formatProvider)
{
var messageFormat = _messageFormat.ToString(formatProvider);
return messageFormat != null ?
(_formatArguments.Length > 0 ? string.Format(formatProvider, messageFormat, _formatArguments) : messageFormat) :
string.Empty;
}

protected override bool AreEqual(object? other)
{
return other is LocalizableStringWithArguments otherResourceString &&
_messageFormat.Equals(otherResourceString._messageFormat) &&
_formatArguments.SequenceEqual(otherResourceString._formatArguments, (a, b) => a == b);
}

protected override int GetHash()
{
return Hash.Combine(
_messageFormat.GetHashCode(),
Hash.CombineValues(_formatArguments));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,22 @@ internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumenta
new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_can_be_removed_as_the_value_assigned_to_it_is_never_read), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
isUnnecessary: true);

internal static readonly DiagnosticDescriptor s_removeUnreadMembersRuleForProperties = CreateDescriptor(
IDEDiagnosticIds.RemoveUnreadMembersDiagnosticId,
EnforceOnBuildValues.RemoveUnreadMembers,
new LocalizableResourceString(nameof(AnalyzersResources.Remove_unread_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
new LocalizableResourceString(nameof(AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
isUnnecessary: true);

internal static readonly DiagnosticDescriptor s_removeUnreadMembersRuleForMethods = CreateDescriptor(
IDEDiagnosticIds.RemoveUnreadMembersDiagnosticId,
EnforceOnBuildValues.RemoveUnreadMembers,
new LocalizableResourceString(nameof(AnalyzersResources.Remove_unread_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
new LocalizableResourceString(nameof(AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
isUnnecessary: true);

protected AbstractRemoveUnusedMembersDiagnosticAnalyzer()
: base(ImmutableArray.Create(s_removeUnusedMembersRule, s_removeUnreadMembersRule),
: base(ImmutableArray.Create(s_removeUnusedMembersRule, s_removeUnreadMembersRule, s_removeUnreadMembersRuleForProperties, s_removeUnreadMembersRuleForMethods),
GeneratedCodeAnalysisFlags.Analyze) // We want to analyze references in generated code, but not report unused members in generated code.
{
}
Expand Down Expand Up @@ -461,7 +475,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo
// Report IDE0051 or IDE0052 based on whether the underlying member has any Write/WritableRef/NonReadWriteRef references or not.
var rule = !valueUsageInfo.IsWrittenTo() && !valueUsageInfo.IsNameOnly() && !symbolsReferencedInDocComments.Contains(member)
? s_removeUnusedMembersRule
: s_removeUnreadMembersRule;
: GetUnreadMembersRule(member);

// Do not flag write-only properties that are not read.
// Write-only properties are assumed to have side effects
Expand All @@ -475,13 +489,13 @@ member is IPropertySymbol property &&

// Most of the members should have a single location, except for partial methods.
// We report the diagnostic on the first location of the member.
var diagnostic = DiagnosticHelper.CreateWithMessage(
var diagnostic = DiagnosticHelper.Create(
rule,
member.Locations[0],
rule.GetEffectiveSeverity(symbolEndContext.Compilation.Options),
additionalLocations: null,
properties: null,
GetMessage(rule, member));
$"{member.ContainingType.Name}.{member.Name}");
symbolEndContext.ReportDiagnostic(diagnostic);
}
}
Expand All @@ -495,35 +509,27 @@ member is IPropertySymbol property &&
return;
}

private LocalizableString GetMessage(
DiagnosticDescriptor rule,
ISymbol member)
private DiagnosticDescriptor GetUnreadMembersRule(ISymbol member)
{
var messageFormat = rule.MessageFormat;
if (rule == s_removeUnreadMembersRule)
// IDE0052 has a different message for method and property symbols.
switch (member)
{
// IDE0052 has a different message for method and property symbols.
switch (member)
{
case IMethodSymbol _:
messageFormat = AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked;
break;

case IPropertySymbol property:
// We change the message only if both 'get' and 'set' accessors are present and
// there are no shadow 'get' accessor usages. Otherwise the message will be confusing
if (property.GetMethod != null && property.SetMethod != null &&
!_propertiesWithShadowGetAccessorUsages.Contains(property))
{
messageFormat = AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked;
}
case IMethodSymbol:
return s_removeUnreadMembersRuleForMethods;

case IPropertySymbol property:
// We change the message only if both 'get' and 'set' accessors are present and
// there are no shadow 'get' accessor usages. Otherwise the message will be confusing
if (property.GetMethod != null && property.SetMethod != null &&
!_propertiesWithShadowGetAccessorUsages.Contains(property))
{
return s_removeUnreadMembersRuleForProperties;
}

break;
}
break;
}

var memberName = $"{member.ContainingType.Name}.{member.Name}";
return new DiagnosticHelper.LocalizableStringWithArguments(messageFormat, memberName);
return s_removeUnreadMembersRule;
}

private static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,41 +144,36 @@ private void ReportUnusedParameterDiagnostic(
return;
}

var message = GetMessageForUnusedParameterDiagnostic(
parameter.Name,
var rule = GetDescriptorForUnusedParameterDiagnostic(
hasReference,
isPublicApiParameter: parameter.ContainingSymbol.HasPublicResultantVisibility(),
isLocalFunctionParameter: parameter.ContainingSymbol.IsLocalFunction());

var diagnostic = DiagnosticHelper.CreateWithMessage(s_unusedParameterRule, location,
option.Notification.Severity, additionalLocations: null, properties: null, message);
var diagnostic = DiagnosticHelper.Create(rule, location,
option.Notification.Severity, additionalLocations: null, properties: null, parameter.Name);
reportDiagnostic(diagnostic);
}

private static LocalizableString GetMessageForUnusedParameterDiagnostic(
string parameterName,
private static DiagnosticDescriptor GetDescriptorForUnusedParameterDiagnostic(
bool hasReference,
bool isPublicApiParameter,
bool isLocalFunctionParameter)
{
LocalizableString messageFormat;
if (isPublicApiParameter &&
!isLocalFunctionParameter)
{
messageFormat = hasReference
? AnalyzersResources.Parameter_0_can_be_removed_if_it_is_not_part_of_a_shipped_public_API_its_initial_value_is_never_used
: AnalyzersResources.Remove_unused_parameter_0_if_it_is_not_part_of_a_shipped_public_API;
return hasReference
? s_unusedParameterRuleForPublicApiHasReference
: s_unusedParameterRuleForPublicApiNoReference;
}
else if (hasReference)
{
messageFormat = AnalyzersResources.Parameter_0_can_be_removed_its_initial_value_is_never_used;
return s_unusedParameterRuleForNonPublicApiHasReference;
}
else
{
messageFormat = s_unusedParameterRule.MessageFormat;
return s_unusedParameterRule;
}

return new DiagnosticHelper.LocalizableStringWithArguments(messageFormat, parameterName);
}

private static IEnumerable<INamedTypeSymbol> GetAttributesForMethodsToIgnore(Compilation compilation)
Expand Down
Loading