diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1648UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1648UnitTests.cs index 9443e7a83..68e36c227 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1648UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1648UnitTests.cs @@ -1,14 +1,13 @@ // Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -#nullable disable - namespace StyleCop.Analyzers.Test.DocumentationRules { using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Testing; using StyleCop.Analyzers.DocumentationRules; + using StyleCop.Analyzers.Test.Helpers; using StyleCop.Analyzers.Test.Verifiers; using Xunit; using static StyleCop.Analyzers.Test.Verifiers.CustomDiagnosticVerifier; @@ -18,6 +17,131 @@ namespace StyleCop.Analyzers.Test.DocumentationRules /// public class SA1648UnitTests { + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorWithNoParametersInheritsFromParentAsync(string keyword) + { + var testCode = @"$KEYWORD$ Base +{ + /// Base constructor. + public Base() { } +} + +$KEYWORD$ Test : Base +{ + /// + public Test() { } +}"; + + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorWithParametersInheritsFromParentAsync(string keyword) + { + var testCode = @"$KEYWORD$ Base +{ + /// Base constructor. + public Base(string s, int a) { } +} + +$KEYWORD$ Test : Base +{ + /// + public Test(string s, int b) + : base(s, b) { } +} +"; + + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorInheritsImplicitlyFromSystemObjectAsync(string keyword) + { + var testCode = @"$KEYWORD$ Test +{ + /// + public Test() { } +}"; + + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorInheritsExplicitlyFromSystemObjectAsync(string keyword) + { + var testCode = @"$KEYWORD$ Test : System.Object +{ + /// + public Test() { } +}"; + + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestConstructorInheritsExplicitlyFromTypeInDifferentAssemblyAsync() + { + var testCode = @"class MyArgumentException : System.ArgumentException +{ + /// + public MyArgumentException() { } + + /// + public MyArgumentException(string message) : base(message) { } +}"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorInheritsButBaseCtorHasTheSameNumberOfParametersButNotMatchingSignaturesAsync(string keyword) + { + var testCode = @"$KEYWORD$ Base +{ + /// Base constructor. + public Base(string s, string a) { } +} + +$KEYWORD$ Test : Base +{ + /// + public Test(string s, int b) + : base(s, b.ToString()) { } +} +"; + + var expected = Diagnostic().WithLocation(9, 9); + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), expected, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))] + public async Task TestConstructorInheritsButBaseCtorHasDifferentNumberOfParametersAsync(string keyword) + { + var testCode = @"$KEYWORD$ Base +{ + /// Base constructor. + public Base(string s) { } +} + +$KEYWORD$ Test : Base +{ + /// + public Test(string s, int b) + : base(s) { } +} +"; + + var expected = Diagnostic().WithLocation(9, 9); + await VerifyCSharpDiagnosticAsync(testCode.Replace("$KEYWORD$", keyword), expected, CancellationToken.None).ConfigureAwait(false); + } + [Fact] public async Task TestClassOverridesClassAsync() { @@ -90,7 +214,7 @@ public async Task TestTypeWithEmptyBaseListAndCrefAttributeAsync(string declarat } [Theory] - [InlineData("Test() { }")] + [InlineData("Test(int ignored) { }")] [InlineData("void Foo() { }")] [InlineData("string foo;")] [InlineData("string Foo { get; set; }")] diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1648InheritDocMustBeUsedWithInheritingClass.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1648InheritDocMustBeUsedWithInheritingClass.cs index 339508867..8c6747100 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1648InheritDocMustBeUsedWithInheritingClass.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1648InheritDocMustBeUsedWithInheritingClass.cs @@ -1,8 +1,6 @@ // Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -#nullable disable - namespace StyleCop.Analyzers.DocumentationRules { using System; @@ -70,7 +68,7 @@ public override void Initialize(AnalysisContext context) private static void HandleBaseTypeLikeDeclaration(SyntaxNodeAnalysisContext context) { - BaseTypeDeclarationSyntax baseType = context.Node as BaseTypeDeclarationSyntax; + BaseTypeDeclarationSyntax? baseType = context.Node as BaseTypeDeclarationSyntax; // baseType can be null here if we are looking at a delegate declaration if (baseType != null && baseType.BaseList != null && baseType.BaseList.Types.Any()) @@ -149,10 +147,24 @@ private static void HandleMemberDeclaration(SyntaxNodeAnalysisContext context) Location location; ISymbol declaredSymbol = context.SemanticModel.GetDeclaredSymbol(memberSyntax, context.CancellationToken); + + if (memberSyntax is ConstructorDeclarationSyntax constructorDeclarationSyntax && declaredSymbol is IMethodSymbol constructorMethodSymbol) + { + if (constructorMethodSymbol.ContainingType != null) + { + INamedTypeSymbol baseType = constructorMethodSymbol.ContainingType.BaseType; + + if (HasMatchingSignature(baseType.Constructors, constructorMethodSymbol)) + { + return; + } + } + } + if (declaredSymbol == null && memberSyntax.IsKind(SyntaxKind.EventFieldDeclaration)) { var eventFieldDeclarationSyntax = (EventFieldDeclarationSyntax)memberSyntax; - VariableDeclaratorSyntax firstVariable = eventFieldDeclarationSyntax.Declaration?.Variables.FirstOrDefault(); + VariableDeclaratorSyntax? firstVariable = eventFieldDeclarationSyntax.Declaration?.Variables.FirstOrDefault(); if (firstVariable != null) { declaredSymbol = context.SemanticModel.GetDeclaredSymbol(firstVariable, context.CancellationToken); @@ -206,15 +218,54 @@ private static void HandleMemberDeclaration(SyntaxNodeAnalysisContext context) } } + /// + /// Method compares a constructor method signature against its + /// base type constructors to find if there is a method signature match. + /// + /// if any base type constructor's signature matches the signature of , otherwise. + private static bool HasMatchingSignature(ImmutableArray baseConstructorSymbols, IMethodSymbol constructorMethodSymbol) + { + foreach (IMethodSymbol baseConstructorMethod in baseConstructorSymbols) + { + // Constructors must have the same number of parameters. + if (constructorMethodSymbol.Parameters.Length != baseConstructorMethod.Parameters.Length) + { + continue; + } + + // Our constructor and the base constructor must have the same signature. But variable names can be different. + bool success = true; + + for (int i = 0; i < constructorMethodSymbol.Parameters.Length; i++) + { + IParameterSymbol constructorParameter = constructorMethodSymbol.Parameters[i]; + IParameterSymbol baseParameter = baseConstructorMethod.Parameters[i]; + + if (!constructorParameter.Type.Equals(baseParameter.Type)) + { + success = false; + break; + } + } + + if (success) + { + return true; + } + } + + return false; + } + private static bool HasXmlCrefAttribute(XmlNodeSyntax inheritDocElement) { - XmlElementSyntax xmlElementSyntax = inheritDocElement as XmlElementSyntax; + XmlElementSyntax? xmlElementSyntax = inheritDocElement as XmlElementSyntax; if (xmlElementSyntax?.StartTag?.Attributes.Any(SyntaxKind.XmlCrefAttribute) ?? false) { return true; } - XmlEmptyElementSyntax xmlEmptyElementSyntax = inheritDocElement as XmlEmptyElementSyntax; + XmlEmptyElementSyntax? xmlEmptyElementSyntax = inheritDocElement as XmlEmptyElementSyntax; if (xmlEmptyElementSyntax?.Attributes.Any(SyntaxKind.XmlCrefAttribute) ?? false) { return true;