diff --git a/analyzers/rspec/cs/S1144.html b/analyzers/rspec/cs/S1144.html index 0bf9b1254a4..9284f1debd9 100644 --- a/analyzers/rspec/cs/S1144.html +++ b/analyzers/rspec/cs/S1144.html @@ -43,6 +43,8 @@

Exceptions

href="https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute">System.SerializableAttribute attribute
  • internal members in assemblies that have a System.Runtime.CompilerServices.InternalsVisibleToAttribute attribute.
  • +
  • types and members decorated with the System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute attribute (available in .NET 5.0+) or a custom attribute named DynamicallyAccessedMembersAttribute.
  • Resources

    Documentation

    diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSymbolUsageCollector.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSymbolUsageCollector.cs index b7dc1d889ee..33db5e93128 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSymbolUsageCollector.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSymbolUsageCollector.cs @@ -20,413 +20,430 @@ using SonarAnalyzer.Common.Walkers; -namespace SonarAnalyzer.Helpers +namespace SonarAnalyzer.Helpers; + +/// +/// Collects all symbol usages from a class declaration. Ignores symbols whose names are not present +/// in the knownSymbolNames collection for performance reasons. +/// +internal class CSharpSymbolUsageCollector : SafeCSharpSyntaxWalker { - /// - /// Collects all symbol usages from a class declaration. Ignores symbols whose names are not present - /// in the knownSymbolNames collection for performance reasons. - /// - internal class CSharpSymbolUsageCollector : SafeCSharpSyntaxWalker + [Flags] + private enum SymbolAccess { - [Flags] - private enum SymbolAccess - { - None = 0, - Read = 1, - Write = 2, - ReadWrite = Read | Write - } - - private static readonly ISet IncrementKinds = new HashSet - { - SyntaxKind.PostIncrementExpression, - SyntaxKind.PreIncrementExpression, - SyntaxKind.PostDecrementExpression, - SyntaxKind.PreDecrementExpression - }; - - private readonly Compilation compilation; - private readonly HashSet knownSymbolNames; - private SemanticModel semanticModel; + None = 0, + Read = 1, + Write = 2, + ReadWrite = Read | Write + } - public ISet UsedSymbols { get; } = new HashSet(); - public IDictionary FieldSymbolUsages { get; } = new Dictionary(); - public HashSet DebuggerDisplayValues { get; } = []; - public Dictionary PropertyAccess { get; } = []; + private static readonly ISet IncrementKinds = new HashSet + { + SyntaxKind.PostIncrementExpression, + SyntaxKind.PreIncrementExpression, + SyntaxKind.PostDecrementExpression, + SyntaxKind.PreDecrementExpression + }; + + private readonly Compilation compilation; + private readonly HashSet knownSymbolNames; + private SemanticModel model; + + public ISet UsedSymbols { get; } = new HashSet(); + public IDictionary FieldSymbolUsages { get; } = new Dictionary(); + public HashSet DebuggerDisplayValues { get; } = []; + public Dictionary PropertyAccess { get; } = []; + public HashSet TypesUsedWithReflection { get; } = []; + + public CSharpSymbolUsageCollector(Compilation compilation, IEnumerable knownSymbols) + { + this.compilation = compilation; + knownSymbolNames = knownSymbols.Select(GetName).ToHashSet(); + } - public CSharpSymbolUsageCollector(Compilation compilation, IEnumerable knownSymbols) + public override void Visit(SyntaxNode node) + { + model = node.EnsureCorrectSemanticModelOrDefault(model ?? compilation.GetSemanticModel(node.SyntaxTree)); + if (model is not null) { - this.compilation = compilation; - knownSymbolNames = knownSymbols.Select(GetName).ToHashSet(); + if (node.IsKind(SyntaxKindEx.ImplicitObjectCreationExpression) + && knownSymbolNames.Contains(ObjectCreationFactory.Create(node).TypeAsString(model))) + { + UsedSymbols.UnionWith(GetSymbols(node)); + } + else if (node.IsKind(SyntaxKindEx.LocalFunctionStatement) + && ((LocalFunctionStatementSyntaxWrapper)node) is { AttributeLists.Count: > 0 } + && model.GetDeclaredSymbol(node) is IMethodSymbol localFunctionSymbol) + { + UsedSymbols.UnionWith(localFunctionSymbol + .GetAttributes() + .Where(a => knownSymbolNames.Contains(a.AttributeClass.Name)) + .Select(a => a.AttributeClass)); + } + else if (node.IsKind(SyntaxKindEx.PrimaryConstructorBaseType) + && knownSymbolNames.Contains(((PrimaryConstructorBaseTypeSyntaxWrapper)node).Type.GetName())) + { + UsedSymbols.UnionWith(GetSymbols(node)); + } + base.Visit(node); } + } - public override void Visit(SyntaxNode node) + // TupleExpression "(a, b) = qix" + // ParenthesizedVariableDesignation "var (a, b) = quix" inside a DeclarationExpression + public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) + { + var leftTupleCount = GetTupleCount(node.Left); + if (leftTupleCount != 0) { - semanticModel = node.EnsureCorrectSemanticModelOrDefault(semanticModel ?? compilation.GetSemanticModel(node.SyntaxTree)); - if (semanticModel != null) + var assignmentRight = node.Right; + var namedTypeSymbol = model.GetSymbolInfo(assignmentRight).Symbol?.GetSymbolType(); + if (namedTypeSymbol is not null) { - if (node.IsKind(SyntaxKindEx.ImplicitObjectCreationExpression) - && knownSymbolNames.Contains(ObjectCreationFactory.Create(node).TypeAsString(semanticModel))) - { - UsedSymbols.UnionWith(GetSymbols(node)); - } - else if (node.IsKind(SyntaxKindEx.LocalFunctionStatement) - && ((LocalFunctionStatementSyntaxWrapper)node) is { AttributeLists: { Count: > 0 } } - && semanticModel.GetDeclaredSymbol(node) is IMethodSymbol localFunctionSymbol) + var deconstructors = namedTypeSymbol.GetMembers("Deconstruct"); + if (deconstructors.Length == 1) { - UsedSymbols.UnionWith(localFunctionSymbol - .GetAttributes() - .Where(a => knownSymbolNames.Contains(a.AttributeClass.Name)) - .Select(a => a.AttributeClass)); + UsedSymbols.Add(deconstructors.First()); } - else if (node.IsKind(SyntaxKindEx.PrimaryConstructorBaseType) - && knownSymbolNames.Contains(((PrimaryConstructorBaseTypeSyntaxWrapper)node).Type.GetName())) + else if (deconstructors.Length > 1 && FindDeconstructor(deconstructors, leftTupleCount) is {} deconstructor) { - UsedSymbols.UnionWith(GetSymbols(node)); + UsedSymbols.Add(deconstructor); } - base.Visit(node); } } + base.VisitAssignmentExpression(node); - // TupleExpression "(a, b) = qix" - // ParenthesizedVariableDesignation "var (a, b) = quix" inside a DeclarationExpression - public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) + static int GetTupleCount(ExpressionSyntax assignmentLeft) { - var leftTupleCount = GetTupleCount(node.Left); - if (leftTupleCount != 0) + var result = 0; + if (TupleExpressionSyntaxWrapper.IsInstance(assignmentLeft)) { - var assignmentRight = node.Right; - var namedTypeSymbol = semanticModel.GetSymbolInfo(assignmentRight).Symbol?.GetSymbolType(); - if (namedTypeSymbol != null) - { - var deconstructors = namedTypeSymbol.GetMembers("Deconstruct"); - if (deconstructors.Length == 1) - { - UsedSymbols.Add(deconstructors.First()); - } - else if (deconstructors.Length > 1 && FindDeconstructor(deconstructors, leftTupleCount) is {} deconstructor) - { - UsedSymbols.Add(deconstructor); - } - } + result = ((TupleExpressionSyntaxWrapper)assignmentLeft).Arguments.Count; } - base.VisitAssignmentExpression(node); - - static int GetTupleCount(ExpressionSyntax assignmentLeft) + else if (DeclarationExpressionSyntaxWrapper.IsInstance(assignmentLeft) + && (DeclarationExpressionSyntaxWrapper)assignmentLeft is { } leftDeclaration + && ParenthesizedVariableDesignationSyntaxWrapper.IsInstance(leftDeclaration.Designation)) { - var result = 0; - if (TupleExpressionSyntaxWrapper.IsInstance(assignmentLeft)) - { - result = ((TupleExpressionSyntaxWrapper)assignmentLeft).Arguments.Count; - } - else if (DeclarationExpressionSyntaxWrapper.IsInstance(assignmentLeft) - && (DeclarationExpressionSyntaxWrapper)assignmentLeft is { } leftDeclaration - && ParenthesizedVariableDesignationSyntaxWrapper.IsInstance(leftDeclaration.Designation)) - { - result = ((ParenthesizedVariableDesignationSyntaxWrapper)leftDeclaration.Designation).Variables.Count; - } - return result; + result = ((ParenthesizedVariableDesignationSyntaxWrapper)leftDeclaration.Designation).Variables.Count; } - - static ISymbol FindDeconstructor(IEnumerable deconstructors, int numberOfArguments) => - deconstructors.FirstOrDefault(m => m.GetParameters().Count() == numberOfArguments && m.DeclaredAccessibility.IsAccessibleOutsideTheType()); + return result; } - public override void VisitAttribute(AttributeSyntax node) - { - var symbol = semanticModel.GetSymbolInfo(node).Symbol; - if (symbol != null - && symbol.ContainingType.Is(KnownType.System_Diagnostics_DebuggerDisplayAttribute) - && node.ArgumentList != null) - { - var arguments = node.ArgumentList.Arguments - .Where(IsValueNameOrType) - .Select(a => semanticModel.GetConstantValue(a.Expression)) - .Where(o => o.HasValue) - .Select(o => o.Value) - .OfType(); - - DebuggerDisplayValues.UnionWith(arguments); - } - base.VisitAttribute(node); - - static bool IsValueNameOrType(AttributeArgumentSyntax a) => - a.NameColon == null // Value - || a.NameColon.Name.Identifier.ValueText == "Value" - || a.NameColon.Name.Identifier.ValueText == "Name" - || a.NameColon.Name.Identifier.ValueText == "Type"; - } + static ISymbol FindDeconstructor(IEnumerable deconstructors, int numberOfArguments) => + deconstructors.FirstOrDefault(m => m.GetParameters().Count() == numberOfArguments && m.DeclaredAccessibility.IsAccessibleOutsideTheType()); + } - public override void VisitIdentifierName(IdentifierNameSyntax node) + public override void VisitAttribute(AttributeSyntax node) + { + // Some members that seem unused might be dynamically accessed through reflection. + // The DynamicallyAccessedMembersAttribute was introduced to inform tools about such uses. + // The attribute is not available on NetFramework and we want to enable this mechanism for these users. + // Therefore, we only check the name, but not the namespace and let the users define their own custom version. + // https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute + const string DynamicallyAccessedMembers = "DynamicallyAccessedMembers"; + if (node.GetName() is DynamicallyAccessedMembers or $"{DynamicallyAccessedMembers}Attribute" + && node is { Parent: AttributeListSyntax { Parent: BaseTypeDeclarationSyntax typeDeclaration } } + && model.GetDeclaredSymbol(typeDeclaration) is { } typeSymbol) { - if (IsKnownIdentifier(node.Identifier)) - { - var symbols = GetSymbols(node); - TryStoreFieldAccess(node, symbols); - UsedSymbols.UnionWith(symbols); - TryStorePropertyAccess(node, symbols); - } - base.VisitIdentifierName(node); + TypesUsedWithReflection.Add(typeSymbol); } - - public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) + else if (model.GetSymbolInfo(node).Symbol is IMethodSymbol { MethodKind: MethodKind.Constructor, ContainingType: ITypeSymbol attribute } + && attribute.Is(KnownType.System_Diagnostics_DebuggerDisplayAttribute) + && node.ArgumentList is not null) { - if (knownSymbolNames.Contains(node.Type.GetName())) - { - UsedSymbols.UnionWith(GetSymbols(node)); - } - base.VisitObjectCreationExpression(node); + var arguments = node.ArgumentList.Arguments + .Where(IsValueNameOrType) + .Select(x => model.GetConstantValue(x.Expression)) + .Where(x => x.HasValue) + .Select(x => x.Value) + .OfType(); + + DebuggerDisplayValues.UnionWith(arguments); } + base.VisitAttribute(node); - public override void VisitGenericName(GenericNameSyntax node) + static bool IsValueNameOrType(AttributeArgumentSyntax a) => + a.NameColon is null // Value + || a.NameColon.Name.Identifier.ValueText == "Value" + || a.NameColon.Name.Identifier.ValueText == "Name" + || a.NameColon.Name.Identifier.ValueText == "Type"; + } + + public override void VisitIdentifierName(IdentifierNameSyntax node) + { + if (IsKnownIdentifier(node.Identifier)) { - if (IsKnownIdentifier(node.Identifier)) - { - UsedSymbols.UnionWith(GetSymbols(node)); - } - base.VisitGenericName(node); + var symbols = GetSymbols(node); + TryStoreFieldAccess(node, symbols); + UsedSymbols.UnionWith(symbols); + TryStorePropertyAccess(node, symbols); } + base.VisitIdentifierName(node); + } - public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) + public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) + { + if (knownSymbolNames.Contains(node.Type.GetName())) { - if (node.Expression.IsKind(SyntaxKind.ThisExpression) || knownSymbolNames.Contains(node.Expression.GetIdentifier()?.ValueText)) - { - var symbols = GetSymbols(node); - UsedSymbols.UnionWith(symbols); - TryStorePropertyAccess(node, symbols); - } - base.VisitElementAccessExpression(node); + UsedSymbols.UnionWith(GetSymbols(node)); } + base.VisitObjectCreationExpression(node); + } - public override void VisitConstructorInitializer(ConstructorInitializerSyntax node) + public override void VisitGenericName(GenericNameSyntax node) + { + if (IsKnownIdentifier(node.Identifier)) { - // In this case (":base()") we cannot check at the syntax level if the constructor name is in the list - // of known names so we have to check for symbols. UsedSymbols.UnionWith(GetSymbols(node)); - base.VisitConstructorInitializer(node); } + base.VisitGenericName(node); + } - public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) + { + if (node.Expression.IsKind(SyntaxKind.ThisExpression) || knownSymbolNames.Contains(node.Expression.GetIdentifier()?.ValueText)) { - // We are visiting a ctor with no initializer and the compiler will automatically - // call the default constructor of the type if declared, or the base type if the - // current type does not declare a default constructor. - if (node.Initializer == null && IsKnownIdentifier(node.Identifier)) - { - var constructor = semanticModel.GetDeclaredSymbol(node); - var implicitlyCalledConstructor = GetImplicitlyCalledConstructor(constructor); - if (implicitlyCalledConstructor != null) - { - UsedSymbols.Add(implicitlyCalledConstructor); - } - } - base.VisitConstructorDeclaration(node); + var symbols = GetSymbols(node); + UsedSymbols.UnionWith(symbols); + TryStorePropertyAccess(node, symbols); } + base.VisitElementAccessExpression(node); + } - public override void VisitVariableDeclarator(VariableDeclaratorSyntax node) + public override void VisitConstructorInitializer(ConstructorInitializerSyntax node) + { + // In this case (":base()") we cannot check at the syntax level if the constructor name is in the list + // of known names so we have to check for symbols. + UsedSymbols.UnionWith(GetSymbols(node)); + base.VisitConstructorInitializer(node); + } + + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + // We are visiting a ctor with no initializer and the compiler will automatically + // call the default constructor of the type if declared, or the base type if the + // current type does not declare a default constructor. + if (node.Initializer is null && IsKnownIdentifier(node.Identifier)) { - if (IsKnownIdentifier(node.Identifier)) + var constructor = model.GetDeclaredSymbol(node); + var implicitlyCalledConstructor = GetImplicitlyCalledConstructor(constructor); + if (implicitlyCalledConstructor is not null) { - var usage = GetFieldSymbolUsage(semanticModel.GetDeclaredSymbol(node)); - usage.Declaration = node; - if (node.Initializer != null) - { - usage.Initializer = node; - } + UsedSymbols.Add(implicitlyCalledConstructor); } - base.VisitVariableDeclarator(node); } + base.VisitConstructorDeclaration(node); + } - public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + public override void VisitVariableDeclarator(VariableDeclaratorSyntax node) + { + if (IsKnownIdentifier(node.Identifier)) { - if (node.Initializer != null && IsKnownIdentifier(node.Identifier)) + var usage = GetFieldSymbolUsage(model.GetDeclaredSymbol(node)); + usage.Declaration = node; + if (node.Initializer is not null) { - var symbol = semanticModel.GetDeclaredSymbol(node); - UsedSymbols.Add(symbol); - StorePropertyAccess(symbol, AccessorAccess.Set); + usage.Initializer = node; } - base.VisitPropertyDeclaration(node); } + base.VisitVariableDeclarator(node); + } - private SymbolAccess ParentAccessType(SyntaxNode node) => - node.Parent switch - { - // (node) - ParenthesizedExpressionSyntax parenthesizedExpression => ParentAccessType(parenthesizedExpression), - // node; - ExpressionStatementSyntax _ => SymbolAccess.None, - // node(_) : - InvocationExpressionSyntax invocation => node == invocation.Expression ? SymbolAccess.Read : SymbolAccess.None, - // _.node : node._ - MemberAccessExpressionSyntax memberAccess => node == memberAccess.Name ? ParentAccessType(memberAccess) : SymbolAccess.Read, - // _?.node : node?._ - MemberBindingExpressionSyntax memberBinding => node == memberBinding.Name ? ParentAccessType(memberBinding) : SymbolAccess.Read, - // node ??= _ : _ ??= node - AssignmentExpressionSyntax assignment when assignment.IsKind(SyntaxKindEx.CoalesceAssignmentExpression) => - node == assignment.Left ? SymbolAccess.ReadWrite : SymbolAccess.Read, - // Ignoring distinction assignmentExpression.IsKind(SyntaxKind.SimpleAssignmentExpression) between - // "node = _" and "node += _" both are considered as Write and rely on the parent to know if its read. - // node = _ : _ = node - AssignmentExpressionSyntax assignment => node == assignment.Left ? SymbolAccess.Write | ParentAccessType(assignment) : SymbolAccess.Read, - // Invocation(node), Invocation(out node), Invocation(ref node) - ArgumentSyntax argument => ArgumentAccessType(argument), - // node++ - ExpressionSyntax expressionSyntax when expressionSyntax.IsAnyKind(IncrementKinds) => SymbolAccess.Write | ParentAccessType(expressionSyntax), - // => node - ArrowExpressionClauseSyntax arrowExpressionClause when arrowExpressionClause.Parent is MethodDeclarationSyntax arrowMethod => - arrowMethod.ReturnType != null && arrowMethod.ReturnType.IsKnownType(KnownType.Void, semanticModel) - ? SymbolAccess.None - : SymbolAccess.Read, - _ => SymbolAccess.Read - }; - - private static SymbolAccess ArgumentAccessType(ArgumentSyntax argument) => - argument.RefOrOutKeyword.Kind() switch - { - // out Type node : out node - SyntaxKind.OutKeyword => SymbolAccess.Write, - // ref node - SyntaxKind.RefKeyword => SymbolAccess.ReadWrite, - _ => SymbolAccess.Read - }; - - /// - /// Given a node, it tries to get the symbol or the candidate symbols (if the compiler cannot find the symbol, - /// .e.g when the code cannot compile). - /// - /// List of symbols - private ImmutableArray GetSymbols(TSyntaxNode node) - where TSyntaxNode : SyntaxNode + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (node.Initializer is not null && IsKnownIdentifier(node.Identifier)) { - var symbolInfo = semanticModel.GetSymbolInfo(node); - - return new[] { symbolInfo.Symbol } - .Concat(symbolInfo.CandidateSymbols) - .Select(GetOriginalDefinition) - .WhereNotNull() - .ToImmutableArray(); - - static ISymbol GetOriginalDefinition(ISymbol candidateSymbol) => - candidateSymbol is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.ReducedExtension - ? methodSymbol.ReducedFrom?.OriginalDefinition - : candidateSymbol?.OriginalDefinition; + var symbol = model.GetDeclaredSymbol(node); + UsedSymbols.Add(symbol); + StorePropertyAccess(symbol, AccessorAccess.Set); } + base.VisitPropertyDeclaration(node); + } - private void TryStorePropertyAccess(ExpressionSyntax node, IEnumerable identifierSymbols) + private SymbolAccess ParentAccessType(SyntaxNode node) => + node.Parent switch { - var propertySymbols = identifierSymbols.OfType().ToList(); - if (propertySymbols.Any()) + // (node) + ParenthesizedExpressionSyntax parenthesizedExpression => ParentAccessType(parenthesizedExpression), + // node; + ExpressionStatementSyntax _ => SymbolAccess.None, + // node(_) : + InvocationExpressionSyntax invocation => node == invocation.Expression ? SymbolAccess.Read : SymbolAccess.None, + // _.node : node._ + MemberAccessExpressionSyntax memberAccess => node == memberAccess.Name ? ParentAccessType(memberAccess) : SymbolAccess.Read, + // _?.node : node?._ + MemberBindingExpressionSyntax memberBinding => node == memberBinding.Name ? ParentAccessType(memberBinding) : SymbolAccess.Read, + // node ??= _ : _ ??= node + AssignmentExpressionSyntax assignment when assignment.IsKind(SyntaxKindEx.CoalesceAssignmentExpression) => + node == assignment.Left ? SymbolAccess.ReadWrite : SymbolAccess.Read, + // Ignoring distinction assignmentExpression.IsKind(SyntaxKind.SimpleAssignmentExpression) between + // "node = _" and "node += _" both are considered as Write and rely on the parent to know if its read. + // node = _ : _ = node + AssignmentExpressionSyntax assignment => node == assignment.Left ? SymbolAccess.Write | ParentAccessType(assignment) : SymbolAccess.Read, + // Invocation(node), Invocation(out node), Invocation(ref node) + ArgumentSyntax argument => ArgumentAccessType(argument), + // node++ + ExpressionSyntax expressionSyntax when expressionSyntax.IsAnyKind(IncrementKinds) => SymbolAccess.Write | ParentAccessType(expressionSyntax), + // => node + ArrowExpressionClauseSyntax arrowExpressionClause when arrowExpressionClause.Parent is MethodDeclarationSyntax arrowMethod => + arrowMethod.ReturnType is not null && arrowMethod.ReturnType.IsKnownType(KnownType.Void, model) + ? SymbolAccess.None + : SymbolAccess.Read, + _ => SymbolAccess.Read + }; + + private static SymbolAccess ArgumentAccessType(ArgumentSyntax argument) => + argument.RefOrOutKeyword.Kind() switch + { + // out Type node : out node + SyntaxKind.OutKeyword => SymbolAccess.Write, + // ref node + SyntaxKind.RefKeyword => SymbolAccess.ReadWrite, + _ => SymbolAccess.Read + }; + + /// + /// Given a node, it tries to get the symbol or the candidate symbols (if the compiler cannot find the symbol, + /// .e.g when the code cannot compile). + /// + /// List of symbols + private ImmutableArray GetSymbols(TSyntaxNode node) + where TSyntaxNode : SyntaxNode + { + var symbolInfo = model.GetSymbolInfo(node); + + return new[] { symbolInfo.Symbol } + .Concat(symbolInfo.CandidateSymbols) + .Select(GetOriginalDefinition) + .WhereNotNull() + .ToImmutableArray(); + + static ISymbol GetOriginalDefinition(ISymbol candidateSymbol) => + candidateSymbol is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.ReducedExtension + ? methodSymbol.ReducedFrom?.OriginalDefinition + : candidateSymbol?.OriginalDefinition; + } + + private void TryStorePropertyAccess(ExpressionSyntax node, IEnumerable identifierSymbols) + { + var propertySymbols = identifierSymbols.OfType().ToList(); + if (propertySymbols.Any()) + { + var access = EvaluatePropertyAccesses(node); + foreach (var propertySymbol in propertySymbols) { - var access = EvaluatePropertyAccesses(node); - foreach (var propertySymbol in propertySymbols) - { - StorePropertyAccess(propertySymbol, access); - } + StorePropertyAccess(propertySymbol, access); } } + } - private void StorePropertyAccess(IPropertySymbol propertySymbol, AccessorAccess access) + private void StorePropertyAccess(IPropertySymbol propertySymbol, AccessorAccess access) + { + if (PropertyAccess.ContainsKey(propertySymbol)) { - if (PropertyAccess.ContainsKey(propertySymbol)) - { - PropertyAccess[propertySymbol] |= access; - } - else - { - PropertyAccess[propertySymbol] = access; - } + PropertyAccess[propertySymbol] |= access; + } + else + { + PropertyAccess[propertySymbol] = access; } + } - private AccessorAccess EvaluatePropertyAccesses(ExpressionSyntax node) + private AccessorAccess EvaluatePropertyAccesses(ExpressionSyntax node) + { + var topmostSyntax = GetTopmostSyntaxWithTheSameSymbol(node); + if (topmostSyntax.Parent is AssignmentExpressionSyntax assignmentExpression) { - var topmostSyntax = GetTopmostSyntaxWithTheSameSymbol(node); - if (topmostSyntax.Parent is AssignmentExpressionSyntax assignmentExpression) + if (assignmentExpression.IsKind(SyntaxKind.SimpleAssignmentExpression)) { - if (assignmentExpression.IsKind(SyntaxKind.SimpleAssignmentExpression)) - { - // Prop = value --> set - // value = Prop --> get - return assignmentExpression.Left == topmostSyntax ? AccessorAccess.Set : AccessorAccess.Get; - } - else - { - // Prop += value --> get/set - return AccessorAccess.Both; - } - } - else if (topmostSyntax.Parent is ArgumentSyntax argument && argument.IsInTupleAssignmentTarget()) - { - return AccessorAccess.Set; - } - else if (node.IsInNameOfArgument(semanticModel)) - { - // nameof(Prop) --> get/set - return AccessorAccess.Both; + // Prop = value --> set + // value = Prop --> get + return assignmentExpression.Left == topmostSyntax ? AccessorAccess.Set : AccessorAccess.Get; } else { - // Prop++ --> get/set - return topmostSyntax.Parent.IsAnyKind(IncrementKinds) ? AccessorAccess.Both : AccessorAccess.Get; + // Prop += value --> get/set + return AccessorAccess.Both; } } + else if (topmostSyntax.Parent is ArgumentSyntax argument && argument.IsInTupleAssignmentTarget()) + { + return AccessorAccess.Set; + } + else if (node.IsInNameOfArgument(model)) + { + // nameof(Prop) --> get/set + return AccessorAccess.Both; + } + else + { + // Prop++ --> get/set + return topmostSyntax.Parent.IsAnyKind(IncrementKinds) ? AccessorAccess.Both : AccessorAccess.Get; + } + } - private bool IsKnownIdentifier(SyntaxToken identifier) => - knownSymbolNames.Contains(identifier.ValueText); + private bool IsKnownIdentifier(SyntaxToken identifier) => + knownSymbolNames.Contains(identifier.ValueText); - private void TryStoreFieldAccess(IdentifierNameSyntax node, IEnumerable symbols) + private void TryStoreFieldAccess(IdentifierNameSyntax node, IEnumerable symbols) + { + var access = ParentAccessType(node); + var fieldSymbolUsagesList = GetFieldSymbolUsagesList(symbols); + if (HasFlag(access, SymbolAccess.Read)) { - var access = ParentAccessType(node); - var fieldSymbolUsagesList = GetFieldSymbolUsagesList(symbols); - if (HasFlag(access, SymbolAccess.Read)) + foreach (var symbolUsage in fieldSymbolUsagesList) { - fieldSymbolUsagesList.ForEach(usage => usage.Readings.Add(node)); + symbolUsage.Readings.Add(node); } + } - if (HasFlag(access, SymbolAccess.Write)) + if (HasFlag(access, SymbolAccess.Write)) + { + foreach (var symbolUsage in fieldSymbolUsagesList) { - fieldSymbolUsagesList.ForEach(usage => usage.Writings.Add(node)); + symbolUsage.Writings.Add(node); } - - static bool HasFlag(SymbolAccess symbolAccess, SymbolAccess flag) => (symbolAccess & flag) != 0; } - private List GetFieldSymbolUsagesList(IEnumerable symbols) => - symbols.Select(GetFieldSymbolUsage).ToList(); + static bool HasFlag(SymbolAccess symbolAccess, SymbolAccess flag) => (symbolAccess & flag) != 0; + } - private SymbolUsage GetFieldSymbolUsage(ISymbol symbol) => - FieldSymbolUsages.GetOrAdd(symbol, s => new SymbolUsage(s)); + private List GetFieldSymbolUsagesList(IEnumerable symbols) => + symbols.Select(GetFieldSymbolUsage).ToList(); - private static SyntaxNode GetTopmostSyntaxWithTheSameSymbol(SyntaxNode identifier) => - // All of the cases below could be parts of invocation or other expressions - identifier.Parent switch - { - // this.identifier or a.identifier or ((a)).identifier, but not identifier.other - MemberAccessExpressionSyntax memberAccess when memberAccess.Name == identifier => memberAccess.GetSelfOrTopParenthesizedExpression(), - // this?.identifier or a?.identifier or ((a))?.identifier, but not identifier?.other - MemberBindingExpressionSyntax memberBinding when memberBinding.Name == identifier => memberBinding.Parent.GetSelfOrTopParenthesizedExpression(), - // identifier or ((identifier)) - _ => identifier.GetSelfOrTopParenthesizedExpression() - }; - - private static IMethodSymbol GetImplicitlyCalledConstructor(IMethodSymbol constructor) => - // In case there is no other explicitly called constructor in a constructor declaration - // the compiler will automatically put a call to the current class' default constructor, - // or if the declaration is the default constructor or there is no default constructor, - // the compiler will put a call the base class' default constructor. - IsDefaultConstructor(constructor) - ? GetDefaultConstructor(constructor.ContainingType.BaseType) - : GetDefaultConstructor(constructor.ContainingType) ?? GetDefaultConstructor(constructor.ContainingType.BaseType); - - private static IMethodSymbol GetDefaultConstructor(INamedTypeSymbol namedType) => - // See https://github.com/SonarSource/sonar-dotnet/issues/3155 - namedType != null && namedType.InstanceConstructors != null - ? namedType.InstanceConstructors.FirstOrDefault(IsDefaultConstructor) - : null; - - private static bool IsDefaultConstructor(IMethodSymbol constructor) => - constructor.Parameters.Length == 0; - - private static string GetName(ISymbol symbol) => - symbol.IsConstructor() ? symbol.ContainingType.Name : symbol.Name; - } + private SymbolUsage GetFieldSymbolUsage(ISymbol symbol) => + FieldSymbolUsages.GetOrAdd(symbol, x => new SymbolUsage(x)); + + private static SyntaxNode GetTopmostSyntaxWithTheSameSymbol(SyntaxNode identifier) => + // All of the cases below could be parts of invocation or other expressions + identifier.Parent switch + { + // this.identifier or a.identifier or ((a)).identifier, but not identifier.other + MemberAccessExpressionSyntax memberAccess when memberAccess.Name == identifier => memberAccess.GetSelfOrTopParenthesizedExpression(), + // this?.identifier or a?.identifier or ((a))?.identifier, but not identifier?.other + MemberBindingExpressionSyntax memberBinding when memberBinding.Name == identifier => memberBinding.Parent.GetSelfOrTopParenthesizedExpression(), + // identifier or ((identifier)) + _ => identifier.GetSelfOrTopParenthesizedExpression() + }; + + private static IMethodSymbol GetImplicitlyCalledConstructor(IMethodSymbol constructor) => + // In case there is no other explicitly called constructor in a constructor declaration + // the compiler will automatically put a call to the current class' default constructor, + // or if the declaration is the default constructor or there is no default constructor, + // the compiler will put a call the base class' default constructor. + IsDefaultConstructor(constructor) + ? GetDefaultConstructor(constructor.ContainingType.BaseType) + : GetDefaultConstructor(constructor.ContainingType) ?? GetDefaultConstructor(constructor.ContainingType.BaseType); + + private static IMethodSymbol GetDefaultConstructor(INamedTypeSymbol namedType) => + // See https://github.com/SonarSource/sonar-dotnet/issues/3155 + namedType != null && namedType.InstanceConstructors != null + ? namedType.InstanceConstructors.FirstOrDefault(IsDefaultConstructor) + : null; + + private static bool IsDefaultConstructor(IMethodSymbol constructor) => + constructor.Parameters.Length == 0; + + private static string GetName(ISymbol symbol) => + symbol.IsConstructor() ? symbol.ContainingType.Name : symbol.Name; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs index 9c8617113b3..976afa1d1ad 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs @@ -151,6 +151,20 @@ private static void ReportUnusedPrivateMembers(SonarCompilationReporti ReportDiagnosticsForMembers(context, unusedSymbols, accessibility, fieldLikeSymbols); } + private static bool IsUsedWithReflection(ISymbol symbol, HashSet symbolsUsedWithReflection) + { + var currentSymbol = symbol; + while (currentSymbol is not null) + { + if (symbolsUsedWithReflection.Contains(currentSymbol)) + { + return true; + } + currentSymbol = currentSymbol.ContainingSymbol; + } + return false; + } + private static bool IsMentionedInDebuggerDisplay(ISymbol symbol, CSharpSymbolUsageCollector usageCollector) => usageCollector.DebuggerDisplayValues.Any(x => x.Contains(symbol.Name)); @@ -161,7 +175,9 @@ private static void ReportUsedButUnreadFields(SonarSymbolReportingContext contex var usedButUnreadFields = usageCollector.FieldSymbolUsages.Values .Where(x => x.Symbol.DeclaredAccessibility == Accessibility.Private || x.Symbol.ContainingType?.DeclaredAccessibility == Accessibility.Private) .Where(x => x.Symbol.Kind == SymbolKind.Field || x.Symbol.Kind == SymbolKind.Event) - .Where(x => !unusedSymbols.Contains(x.Symbol) && !IsMentionedInDebuggerDisplay(x.Symbol, usageCollector)) + .Where(x => !unusedSymbols.Contains(x.Symbol) + && !IsMentionedInDebuggerDisplay(x.Symbol, usageCollector) + && !IsUsedWithReflection(x.Symbol, usageCollector.TypesUsedWithReflection)) .Where(x => x.Declaration is not null && !x.Readings.Any()); foreach (var usage in usedButUnreadFields) @@ -173,7 +189,9 @@ private static void ReportUsedButUnreadFields(SonarSymbolReportingContext contex private static HashSet GetUnusedSymbols(CSharpSymbolUsageCollector usageCollector, IEnumerable removableSymbols) => removableSymbols .Except(usageCollector.UsedSymbols) - .Where(x => !IsMentionedInDebuggerDisplay(x, usageCollector) && !IsAccessorUsed(x, usageCollector)) + .Where(x => !IsMentionedInDebuggerDisplay(x, usageCollector) + && !IsAccessorUsed(x, usageCollector) + && !IsUsedWithReflection(x, usageCollector.TypesUsedWithReflection)) .ToHashSet(); private static bool IsAccessorUsed(ISymbol symbol, CSharpSymbolUsageCollector usageCollector) => @@ -232,10 +250,10 @@ static IEnumerable GetSiblingDeclarators(SyntaxNode va _ => Enumerable.Empty(), }; - static Location GetIdentifierLocation(SyntaxNode syntaxNode) => - syntaxNode.GetIdentifier() is { } identifier + static Location GetIdentifierLocation(SyntaxNode node) => + node.GetIdentifier() is { } identifier ? identifier.GetLocation() - : syntaxNode.GetLocation(); + : node.GetLocation(); } private static void ReportProperty(SonarCompilationReportingContextBase context, @@ -278,12 +296,9 @@ bool IsGenerated(SyntaxReference syntaxReference) => private static CSharpRemovableSymbolWalker RetrieveRemovableSymbols(INamedTypeSymbol namedType, Compilation compilation, SonarSymbolReportingContext context) { var removableSymbolsCollector = new CSharpRemovableSymbolWalker(compilation.GetSemanticModel, namedType.DeclaredAccessibility); - if (!VisitDeclaringReferences(namedType, removableSymbolsCollector, context, includeGeneratedFile: false)) - { - return null; - } - - return removableSymbolsCollector; + return VisitDeclaringReferences(namedType, removableSymbolsCollector, context, includeGeneratedFile: false) + ? removableSymbolsCollector + : null; } private static void CopyRetrievedSymbols(CSharpRemovableSymbolWalker removableSymbolCollector, diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index e7e27702090..a3f18294b1e 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -364,6 +364,7 @@ public sealed partial class KnownType public static readonly KnownType System_DateTimeOffset = new("System.DateTimeOffset"); public static readonly KnownType System_Decimal = new("System.Decimal"); public static readonly KnownType System_Delegate = new("System.Delegate"); + public static readonly KnownType System_Diagnostics_CodeAnalysis_DynamicallyAccessedMembersAttribute = new("System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute"); public static readonly KnownType System_Diagnostics_CodeAnalysis_ExcludeFromCodeCoverageAttribute = new("System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute"); public static readonly KnownType System_Diagnostics_CodeAnalysis_SuppressMessageAttribute = new("System.Diagnostics.CodeAnalysis.SuppressMessageAttribute"); public static readonly KnownType System_Diagnostics_CodeAnalysis_StringSyntaxAttribute = new("System.Diagnostics.CodeAnalysis.StringSyntaxAttribute"); diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Constructors.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Constructors.cs index 188d0f74ac0..4595fdb65cd 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Constructors.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Constructors.cs @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarAnalyzer.Test.Rules +namespace SonarAnalyzer.Test.Rules; + +public partial class UnusedPrivateMemberTest { - public partial class UnusedPrivateMemberTest - { - [TestMethod] - public void UnusedPrivateMember_Constructor_Accessibility() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Constructor_Accessibility() => + builder.AddSnippet(@" public class PrivateConstructors { private PrivateConstructors(int i) { var x = 5; } // Noncompliant {{Remove the unused private constructor 'PrivateConstructors'.}} @@ -62,40 +62,40 @@ public class InnerPublicClass } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_Constructor_DirectReferences() => - builder.AddSnippet(""" - public abstract class PrivateConstructors + [TestMethod] + public void UnusedPrivateMember_Constructor_DirectReferences() => + builder.AddSnippet(""" + public abstract class PrivateConstructors + { + public class Constructor1 + { + public static readonly Constructor1 Instance = new Constructor1(); + private Constructor1() { var x = 5; } + } + + public class Constructor2 + { + public Constructor2(int a) { } + private Constructor2() { var x = 5; } // Compliant - FN + } + + public class Constructor3 + { + public Constructor3(int a) : this() { } + private Constructor3() { var x = 5; } + } + + public class Constructor4 { - public class Constructor1 - { - public static readonly Constructor1 Instance = new Constructor1(); - private Constructor1() { var x = 5; } - } - - public class Constructor2 - { - public Constructor2(int a) { } - private Constructor2() { var x = 5; } // Compliant - FN - } - - public class Constructor3 - { - public Constructor3(int a) : this() { } - private Constructor3() { var x = 5; } - } - - public class Constructor4 - { - static Constructor4() { var x = 5; } - } + static Constructor4() { var x = 5; } } + } - """).VerifyNoIssues(); + """).VerifyNoIssues(); - [TestMethod] - public void UnusedPrivateMember_Constructor_Inheritance() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Constructor_Inheritance() => + builder.AddSnippet(@" public class Inheritance { private abstract class BaseClass1 @@ -121,21 +121,21 @@ public DerivedClass2() { } } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_Empty_Constructors() => - builder.AddSnippet(""" - public class PrivateConstructors - { - private PrivateConstructors(int i) { } // Compliant, empty ctors are reported from another rule - } + [TestMethod] + public void UnusedPrivateMember_Empty_Constructors() => + builder.AddSnippet(""" + public class PrivateConstructors + { + private PrivateConstructors(int i) { } // Compliant, empty ctors are reported from another rule + } - """).VerifyNoIssues(); + """).VerifyNoIssues(); - [TestMethod] - public void UnusedPrivateMember_Illegal_Interface_Constructor() => - // While typing code in IDE, we can end up in a state where an interface has a constructor defined. - // Even though this results in a compiler error (CS0526), IDE will still trigger rules on the interface. - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Illegal_Interface_Constructor() => + // While typing code in IDE, we can end up in a state where an interface has a constructor defined. + // Even though this results in a compiler error (CS0526), IDE will still trigger rules on the interface. + builder.AddSnippet(@" public interface IInterface { // UnusedPrivateMember rule does not trigger AD0001 error from NullReferenceException @@ -143,13 +143,13 @@ public interface IInterface } ").Verify(); - [DataTestMethod] - [DataRow("private", "Remove the unused private constructor 'Foo'.")] - [DataRow("protected", "Remove unused constructor of private type 'Foo'.")] - [DataRow("internal", "Remove unused constructor of private type 'Foo'.")] - [DataRow("public", "Remove unused constructor of private type 'Foo'.")] - public void UnusedPrivateMember_NonPrivateConstructorInPrivateClass(string accessModifier, string expectedMessage) => - builder.AddSnippet($$$""" + [DataTestMethod] + [DataRow("private", "Remove the unused private constructor 'Foo'.")] + [DataRow("protected", "Remove unused constructor of private type 'Foo'.")] + [DataRow("internal", "Remove unused constructor of private type 'Foo'.")] + [DataRow("public", "Remove unused constructor of private type 'Foo'.")] + public void UnusedPrivateMember_NonPrivateConstructorInPrivateClass(string accessModifier, string expectedMessage) => + builder.AddSnippet($$$""" public class Some { private class Foo // Noncompliant @@ -164,26 +164,26 @@ private class Foo // Noncompliant #if NET - [TestMethod] - public void UnusedPrivateMember_RecordPositionalConstructor() => - builder.AddSnippet(""" - // https://github.com/SonarSource/sonar-dotnet/issues/5381 - public abstract record Foo + [TestMethod] + public void UnusedPrivateMember_RecordPositionalConstructor() => + builder.AddSnippet(""" + // https://github.com/SonarSource/sonar-dotnet/issues/5381 + public abstract record Foo + { + Foo(string value) { - Foo(string value) - { - Value = value; - } + Value = value; + } - public string Value { get; } + public string Value { get; } - public sealed record Bar(string Value) : Foo(Value); - } - """).WithOptions(ParseOptionsHelper.FromCSharp9).VerifyNoIssues(); + public sealed record Bar(string Value) : Foo(Value); + } + """).WithOptions(ParseOptionsHelper.FromCSharp9).VerifyNoIssues(); - [TestMethod] - public void UnusedPrivateMember_NonExistentRecordPositionalConstructor() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_NonExistentRecordPositionalConstructor() => + builder.AddSnippet(@" public abstract record Foo { public sealed record Bar(string Value) : RandomRecord(Value); // Error [CS0246, CS1729] no suitable method found to override @@ -191,5 +191,4 @@ public sealed record Bar(string Value) : RandomRecord(Value); // Error [CS0246, #endif - } } diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Fields.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Fields.cs index 6a3cae33ffa..d22b852ffe5 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Fields.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Fields.cs @@ -18,33 +18,44 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarAnalyzer.Test.Rules +namespace SonarAnalyzer.Test.Rules; + +public partial class UnusedPrivateMemberTest { - public partial class UnusedPrivateMemberTest - { - [TestMethod] - public void UnusedPrivateMember_Field_Accessibility() => - builder.AddSnippet(""" - public class PrivateMembers + [TestMethod] + public void UnusedPrivateMember_Field_Accessibility() => + builder.AddSnippet(""" + public class PrivateMembers + { + private int privateField; // Noncompliant {{Remove the unused private field 'privateField'.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + private static int privateStaticField; // Noncompliant + + private class InnerPrivateClass // Noncompliant { - private int privateField; // Noncompliant {{Remove the unused private field 'privateField'.}} - // ^^^^^^^^^^^^^^^^^^^^^^^^^ - private static int privateStaticField; // Noncompliant - - private class InnerPrivateClass // Noncompliant - { - internal int internalField; // Noncompliant - protected int protectedField; // Noncompliant - protected internal int protectedInternalField; // Noncompliant - public int publicField; // Noncompliant - internal static int internalStaticField; // Noncompliant - protected static int protectedStaticField; // Noncompliant - protected internal static int protectedInternalStaticField; // Noncompliant - public static int publicStaticField; // Noncompliant - } + internal int internalField; // Noncompliant + protected int protectedField; // Noncompliant + protected internal int protectedInternalField; // Noncompliant + public int publicField; // Noncompliant + internal static int internalStaticField; // Noncompliant + protected static int protectedStaticField; // Noncompliant + protected internal static int protectedInternalStaticField; // Noncompliant + public static int publicStaticField; // Noncompliant } - - public class NonPrivateMembers + } + + public class NonPrivateMembers + { + internal int internalField; + protected int protectedField; + protected internal int protectedInternalField; + public int publicField; + internal static int internalStaticField; + protected static int protectedStaticField; + protected internal static int protectedInternalStaticField; + public static int publicStaticField; + + public class InnerPublicClass { internal int internalField; protected int protectedField; @@ -54,109 +65,97 @@ public class NonPrivateMembers protected static int protectedStaticField; protected internal static int protectedInternalStaticField; public static int publicStaticField; - - public class InnerPublicClass - { - internal int internalField; - protected int protectedField; - protected internal int protectedInternalField; - public int publicField; - internal static int internalStaticField; - protected static int protectedStaticField; - protected internal static int protectedInternalStaticField; - public static int publicStaticField; - } } - """).Verify(); - - [TestMethod] - public void UnusedPrivateMember_Field_MultipleDeclarations() => - builder.AddSnippet(""" - public class PrivateMembers + } + """).Verify(); + + [TestMethod] + public void UnusedPrivateMember_Field_MultipleDeclarations() => + builder.AddSnippet(""" + public class PrivateMembers + { + private int x, y, z; // Noncompliant {{Remove the unused private field 'x'.}} + // ^^^^^^^^^^^^^^^^^^^^ + + private int a, b, c; + // ^ {{Remove the unused private field 'a'.}} + // ^ @-1 {{Remove the unused private field 'c'.}} + + public int Method1() => b; + } + """).Verify(); + + [TestMethod] + public void UnusedPrivateMember_Fields_DirectReferences() => + builder.AddSnippet(""" + using System; + + public class FieldUsages + { + private int field1; // Noncompliant {{Remove this unread private field 'field1' or refactor the code to use its value.}} + private int field2; // Noncompliant {{Remove this unread private field 'field2' or refactor the code to use its value.}} + private int field3; // Noncompliant {{Remove this unread private field 'field3' or refactor the code to use its value.}} + private int field4; + private int field5; + private int field6; + public int Method1() { - private int x, y, z; // Noncompliant {{Remove the unused private field 'x'.}} - // ^^^^^^^^^^^^^^^^^^^^ - - private int a, b, c; - // ^ {{Remove the unused private field 'a'.}} - // ^ @-1 {{Remove the unused private field 'c'.}} - - public int Method1() => b; + field1 = 0; + this.field2 = 0; + int.TryParse("1", out field3); + Console.Write(field4); + Func x = () => field5; + return field6; } - """).Verify(); - - [TestMethod] - public void UnusedPrivateMember_Fields_DirectReferences() => - builder.AddSnippet(""" - using System; - public class FieldUsages - { - private int field1; // Noncompliant {{Remove this unread private field 'field1' or refactor the code to use its value.}} - private int field2; // Noncompliant {{Remove this unread private field 'field2' or refactor the code to use its value.}} - private int field3; // Noncompliant {{Remove this unread private field 'field3' or refactor the code to use its value.}} - private int field4; - private int field5; - private int field6; - public int Method1() - { - field1 = 0; - this.field2 = 0; - int.TryParse("1", out field3); - Console.Write(field4); - Func x = () => field5; - return field6; - } - - private int field7; - public int ExpressionBodyMethod() => field7; + private int field7; + public int ExpressionBodyMethod() => field7; - private static int field8; - public int Property { get; set; } = field8; + private static int field8; + public int Property { get; set; } = field8; - public FieldUsages(int number) { } + public FieldUsages(int number) { } - private static int field9; - public FieldUsages() : this(field9) { } + private static int field9; + public FieldUsages() : this(field9) { } - private int field10; - private int field11; // Compliant nameof(field11) - public object Method2() - { - var x = new[] { field10 }; - var name = nameof(field11); - return null; - } + private int field10; + private int field11; // Compliant nameof(field11) + public object Method2() + { + var x = new[] { field10 }; + var name = nameof(field11); + return null; } - """).Verify(); - - [TestMethod] - public void UnusedPrivateMember_Fields_StructLayout() => - builder.AddSnippet(""" - // https://github.com/SonarSource/sonar-dotnet/issues/6912 - using System.Runtime.InteropServices; - - public class Foo + } + """).Verify(); + + [TestMethod] + public void UnusedPrivateMember_Fields_StructLayout() => + builder.AddSnippet(""" + // https://github.com/SonarSource/sonar-dotnet/issues/6912 + using System.Runtime.InteropServices; + + public class Foo + { + [StructLayout(LayoutKind.Sequential)] + private struct NetResource { - [StructLayout(LayoutKind.Sequential)] - private struct NetResource - { - public string LocalName; // Compliant: Unused members in a struct with StructLayout attribute are compliant - public string RemoteName; - } + public string LocalName; // Compliant: Unused members in a struct with StructLayout attribute are compliant + public string RemoteName; + } - [DllImport("mpr.dll")] - private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags); + [DllImport("mpr.dll")] + private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags); - public void Bar() + public void Bar() + { + var netResource = new NetResource { - var netResource = new NetResource - { - RemoteName = "foo" - }; - WNetAddConnection2(netResource, "password", "username", 0); - } + RemoteName = "foo" + }; + WNetAddConnection2(netResource, "password", "username", 0); } - """).VerifyNoIssues(); - } + } + """).VerifyNoIssues(); } diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Methods.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Methods.cs index 8efbdba276e..15c39533aba 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Methods.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Methods.cs @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarAnalyzer.Test.Rules +namespace SonarAnalyzer.Test.Rules; + +public partial class UnusedPrivateMemberTest { - public partial class UnusedPrivateMemberTest - { - [TestMethod] - public void UnusedPrivateMember_Method_Accessibility() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Method_Accessibility() => + builder.AddSnippet(@" public class PrivateMembers { private int PrivateMethod() { return 0; } // Noncompliant {{Remove the unused private method 'PrivateMethod'.}} @@ -79,60 +79,60 @@ public class InterfaceImpl : IInterface } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_Methods_DirectReferences() => - builder.AddSnippet(""" - using System; - using System.Linq; - public class MethodUsages + [TestMethod] + public void UnusedPrivateMember_Methods_DirectReferences() => + builder.AddSnippet(""" + using System; + using System.Linq; + public class MethodUsages + { + private int Method1() { return 0; } + private int Method2() { return 0; } + private int Method3() { return 0; } + private int Method4() { return 0; } + private int Method5() { return 0; } + private int Method6() { return 0; } + private int Method7() { return 0; } + public int Test1(MethodUsages other) { - private int Method1() { return 0; } - private int Method2() { return 0; } - private int Method3() { return 0; } - private int Method4() { return 0; } - private int Method5() { return 0; } - private int Method6() { return 0; } - private int Method7() { return 0; } - public int Test1(MethodUsages other) - { - int i; - i = Method1(); - i = this.Method2(); - Console.Write(Method3()); - new MethodUsages().Method4(); - Func x = () => Method5(); - other.Method6(); - return Method7(); - } - - private int Method8() { return 0; } - public int ExpressionBodyMethod() => Method8(); - - private static int Method9() { return 0; } - public MethodUsages(int number) { } - public MethodUsages() : this(Method9()) { } - - private int Method10() { return 0; } - private int Method11() { return 0; } - public object Test2() - { - var x = new[] { Method10() }; - var name = nameof(Method11); - return null; - } - - private int Method12(int i) { return 0; } - public void Test3() - { - new[] { 1, 2, 3 }.Select(Method12); - } + int i; + i = Method1(); + i = this.Method2(); + Console.Write(Method3()); + new MethodUsages().Method4(); + Func x = () => Method5(); + other.Method6(); + return Method7(); } - """).VerifyNoIssues(); + private int Method8() { return 0; } + public int ExpressionBodyMethod() => Method8(); + + private static int Method9() { return 0; } + public MethodUsages(int number) { } + public MethodUsages() : this(Method9()) { } + + private int Method10() { return 0; } + private int Method11() { return 0; } + public object Test2() + { + var x = new[] { Method10() }; + var name = nameof(Method11); + return null; + } - [TestMethod] - public void UnusedPrivateMember_Methods_Main() => - builder.AddSnippet(@" + private int Method12(int i) { return 0; } + public void Test3() + { + new[] { 1, 2, 3 }.Select(Method12); + } + } + + """).VerifyNoIssues(); + + [TestMethod] + public void UnusedPrivateMember_Methods_Main() => + builder.AddSnippet(@" using System.Threading.Tasks; public class NewClass1 { @@ -160,5 +160,4 @@ public class NewClass5 static async Task Main(string[] args) { return ""a""; } // Noncompliant } ").Verify(); - } } diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Types.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Types.cs index ebcc9d101b4..618769f6e19 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Types.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UnusedPrivateMemberTest.Types.cs @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarAnalyzer.Test.Rules +namespace SonarAnalyzer.Test.Rules; + +public partial class UnusedPrivateMemberTest { - public partial class UnusedPrivateMemberTest - { - [TestMethod] - public void UnusedPrivateMember_Types_Accessibility() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Types_Accessibility() => + builder.AddSnippet(@" public class PrivateTypes { private class InnerPrivateClass // Noncompliant {{Remove the unused private class 'InnerPrivateClass'.}} @@ -54,9 +54,9 @@ public struct PublicStruct { } } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_Types_InternalsVisibleTo() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Types_InternalsVisibleTo() => + builder.AddSnippet(@" [assembly:System.Runtime.CompilerServices.InternalsVisibleTo("""")] public class PrivateTypes { @@ -65,32 +65,32 @@ internal class InternalClass { } // Compliant, internal types are not reported w } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_Types_Internals() => - builder.AddSnippet(""" - // https://github.com/SonarSource/sonar-dotnet/issues/1225 - // https://github.com/SonarSource/sonar-dotnet/issues/904 - using System; - public class Class1 + [TestMethod] + public void UnusedPrivateMember_Types_Internals() => + builder.AddSnippet(""" + // https://github.com/SonarSource/sonar-dotnet/issues/1225 + // https://github.com/SonarSource/sonar-dotnet/issues/904 + using System; + public class Class1 + { + public void Method1() { - public void Method1() - { - var x = Sample.Constants.X; - } + var x = Sample.Constants.X; } + } - public class Sample + public class Sample + { + internal class Constants { - internal class Constants - { - public const int X = 5; - } + public const int X = 5; } - """).VerifyNoIssues(); + } + """).VerifyNoIssues(); - [TestMethod] - public void UnusedPrivateMember_Types_DirectReferences() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_Types_DirectReferences() => + builder.AddSnippet(@" using System.Linq; public class PrivateTypes { @@ -117,9 +117,9 @@ public void Test1() } ").Verify(); - [TestMethod] - public void UnusedPrivateMember_SupportTypeKinds() => - builder.AddSnippet(@" + [TestMethod] + public void UnusedPrivateMember_SupportTypeKinds() => + builder.AddSnippet(@" public class PrivateTypes { private class MyPrivateClass { } // Noncompliant @@ -149,5 +149,4 @@ public void Foo() public static int PerformCalculation(int x, int y) => x + y; } ").Verify(); - } } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs index 1ccd42adf73..36b94dcc98a 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Tests.Diagnostics @@ -168,3 +169,146 @@ public class ClassWithPrintMembers } } } + +// https://github.com/SonarSource/sonar-dotnet/issues/9379 +namespace Repro_9379 +{ + public static class Program + { + public static void Method() + { + var instance = CreateInstance(); + var instance2 = CreateInstance(typeof(AnotherClassInstantiatedThroughReflection)); + + A classViaReflection = new(); + InitValue(classViaReflection); + } + + public static T CreateInstance<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() => + (T)Activator.CreateInstance(typeof(T), 42); + + public static object CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) => + Activator.CreateInstance(type, 42); + + public static void InitValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T value) { } + + private class A + { + private bool a = true; // Noncompliant FP: type argument is inferred and ArgumentList is not present on syntax level (see line 183) + } + + class C + { + private int usedByReflection; // Noncompliant - FP + C Create() => Program.CreateInstance>(); // Noncompliant - FP + } + + private class ClassInstantiatedThroughReflection + { + private const int PrivateConst = 42; // Noncompliant - FP + private int privateField; // Noncompliant - FP + private int PrivateProperty { get; set; } // Noncompliant - FP + private void PrivateMethod() { } // Noncompliant - FP + private ClassInstantiatedThroughReflection() { } + private event EventHandler PrivateEvent; // Noncompliant - FP + + public ClassInstantiatedThroughReflection(int arg) + { + } + + private class NestedType // Noncompliant - FP + { + private int privateField; // Noncompliant - FP + } + } + + private class AnotherClassInstantiatedThroughReflection + { + private int privateField; // Noncompliant - FP + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + private class TypeDecoratedWithDynamicallyAccessedMembers + { + private const int PrivateConst = 42; + private int privateField; + private int PrivateProperty { get; set; } + private void PrivateMethod() { } + public TypeDecoratedWithDynamicallyAccessedMembers() { } + private event EventHandler PrivateEvent; + + private class NestedType + { + private const int PrivateConst = 42; + private int privateField; + private int PrivateProperty { get; set; } + private void PrivateMethod() { } + private NestedType() { } + private event EventHandler PrivateEvent; + } + } + + private class MembersDecoratedWithAttribute // Noncompliant + { + private const int PrivateConst = 42; // Noncompliant + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] private const int PrivateConstWithAttribute = 42; + + private int privateField; // Noncompliant + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] private int privateFieldWithAttribute; + + private int PrivateProperty { get; set; } // Noncompliant + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicProperties)] private int PrivatePropertyWithAttribute { get; set; } + + private void PrivateMethod() { } // Noncompliant + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicMethods)] private void PrivateMethodWithAttribute() { } + + private class NestedType { } // Noncompliant + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] private class NestedTypeWithAttribute { } + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicMethods)] + private int PrivateMethodWithReturn() { return 0; } // Noncompliant + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicMethods)] + private PrivateClass PrivateMethodWithReturnCustomClass() { return null; } // Noncompliant FP + } + + private class PrivateClass { } + + private class ArgumentsDecoratedWithAttribute // Noncompliant + { + public void PublicMethod() => Method(null); // Noncompliant + + private void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] NestedType arg) { } + + private class NestedType + { + private NestedType(int arg) { } + } + + public record RecordWithAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] string NotUnused); // Noncompliant + } + + private class DerivedFromTypeDecoratedWithDynamicallyAccessedMembers : TypeDecoratedWithDynamicallyAccessedMembers // Noncompliant + { + private int privateField; // Noncompliant - [DynamicallyAccessedMembers] attribute is not inherited + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private class DynamicallyAccessedMembersOnlyForConstructors + { + private int privateField; // FN - only public constructors are used are indicated to be used through reflection, not fields + // The analyzer assumens when the [DynamicallyAccessedMembers] attribute is used, then all members are used through reflection + } + } +} + +namespace CustomDynamicallyAccessedMembersAttribute +{ + public class DynamicallyAccessedMembersAttribute : Attribute { } + + [DynamicallyAccessedMembers] + public class UnusedClassMarkedWithCustomAttribute + { + private int privateField; + } +}