Skip to content

Commit

Permalink
Merge pull request #3719 from MartyIX/issue2575
Browse files Browse the repository at this point in the history
Allow inheritdoc for class constructors with base types
  • Loading branch information
sharwell authored Dec 19, 2023
2 parents 0fc865d + 6e752bb commit 69d477f
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -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<StyleCop.Analyzers.DocumentationRules.SA1648InheritDocMustBeUsedWithInheritingClass>;
Expand All @@ -18,6 +17,131 @@ namespace StyleCop.Analyzers.Test.DocumentationRules
/// </summary>
public class SA1648UnitTests
{
[Theory]
[MemberData(nameof(CommonMemberData.ReferenceTypeDeclarationKeywords), MemberType = typeof(CommonMemberData))]
public async Task TestConstructorWithNoParametersInheritsFromParentAsync(string keyword)
{
var testCode = @"$KEYWORD$ Base
{
/// <summary>Base constructor.</summary>
public Base() { }
}
$KEYWORD$ Test : Base
{
/// <inheritdoc/>
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
{
/// <summary>Base constructor.</summary>
public Base(string s, int a) { }
}
$KEYWORD$ Test : Base
{
/// <inheritdoc/>
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
{
/// <inheritdoc/>
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
{
/// <inheritdoc/>
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
{
/// <inheritdoc/>
public MyArgumentException() { }
/// <inheritdoc/>
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
{
/// <summary>Base constructor.</summary>
public Base(string s, string a) { }
}
$KEYWORD$ Test : Base
{
/// <inheritdoc/>
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
{
/// <summary>Base constructor.</summary>
public Base(string s) { }
}
$KEYWORD$ Test : Base
{
/// <inheritdoc/>
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()
{
Expand Down Expand Up @@ -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; }")]
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -206,15 +218,54 @@ private static void HandleMemberDeclaration(SyntaxNodeAnalysisContext context)
}
}

/// <summary>
/// Method compares a <paramref name="constructorMethodSymbol">constructor method</paramref> signature against its
/// <paramref name="baseConstructorSymbols">base type constructors</paramref> to find if there is a method signature match.
/// </summary>
/// <returns><see langword="true"/> if any base type constructor's signature matches the signature of <paramref name="constructorMethodSymbol"/>, <see langword="false"/> otherwise.</returns>
private static bool HasMatchingSignature(ImmutableArray<IMethodSymbol> 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;
Expand Down

0 comments on commit 69d477f

Please sign in to comment.