From 5567ed12d3fc760f2b32e941f17609befe2cd344 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Sun, 9 Aug 2026 17:43:20 +0200 Subject: [PATCH 1/4] fix(analyzers): resolve RCS1263 primary constructor param refs Fixes #1730 Resolve the declaring syntax for documentation comments by walking ancestor nodes instead of using parent.Parent, so primary constructor parameters are matched correctly. Co-authored-by: Cursor --- ChangeLog.md | 1 + ...eLineDocumentationCommentTriviaAnalyzer.cs | 63 +++++++++++-------- .../CSharp/Extensions/SyntaxExtensions.cs | 20 ++++++ ...lidReferenceInDocumentationCommentTests.cs | 30 +++++++++ 4 files changed, 88 insertions(+), 26 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 6bea031912..bbcb5b489c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix analyzer [RCS1263](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1263) for primary constructor parameters in documentation comments - Fix analyzer [RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231) to not suggest `in` for `ref struct` parameters ([#1725](https://github.com/dotnet/roslynator/issues/1725)) ([PR](https://github.com/dotnet/roslynator/pull/1807)) - Fix analyzer [RCS1260](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1260) false positive for `omit_when_single_line` on multi-line object/collection initializers ([#1439](https://github.com/dotnet/roslynator/issues/1439)) ([PR](https://github.com/dotnet/roslynator/pull/1808)) - Fix analyzer [RCS0036](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0036) to report blank lines between single-line declarations in records ([PR](https://github.com/dotnet/roslynator/pull/1813)) diff --git a/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs b/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs index f2272c54f5..5c171cd7ad 100644 --- a/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs @@ -198,29 +198,38 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys bool addParam = DiagnosticRules.AddParamElementToDocumentationComment.IsEffective(context); bool addTypeParam = DiagnosticRules.AddTypeParamElementToDocumentationComment.IsEffective(context); + SyntaxNode declaration = (addParam + || orderParams + || invalidReference + || addTypeParam) + ? documentationComment.GetDeclaringSyntaxForDocumentationComment() + : null; + if (addParam || orderParams || invalidReference) { - SeparatedSyntaxList parameters = CSharpUtility.GetParameters( - (parent is MemberDeclarationSyntax) ? parent : parent.Parent); - - if (addParam - && parameters.Any()) + if (declaration is not null) { - foreach (ParameterSyntax parameter in parameters) + SeparatedSyntaxList parameters = CSharpUtility.GetParameters(declaration); + + if (addParam + && parameters.Any()) { - if (IsMissing(documentationComment, parameter)) + foreach (ParameterSyntax parameter in parameters) { - ReportDiagnostic(context, DiagnosticRules.AddParamElementToDocumentationComment, documentationComment); - break; + if (IsMissing(documentationComment, parameter)) + { + ReportDiagnostic(context, DiagnosticRules.AddParamElementToDocumentationComment, documentationComment); + break; + } } } - } - if (orderParams || invalidReference) - { - Analyze(context, documentationComment.Content, parameters, XmlTag.Param, (nodes, name) => nodes.IndexOf(name)); + if (orderParams || invalidReference) + { + Analyze(context, documentationComment.Content, parameters, XmlTag.Param, (nodes, name) => nodes.IndexOf(name)); + } } } @@ -228,25 +237,27 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys || orderParams || invalidReference) { - SeparatedSyntaxList typeParameters = CSharpUtility.GetTypeParameters( - (parent is MemberDeclarationSyntax) ? parent : parent.Parent); - - if (addTypeParam - && typeParameters.Any()) + if (declaration is not null) { - foreach (TypeParameterSyntax typeParameter in typeParameters) + SeparatedSyntaxList typeParameters = CSharpUtility.GetTypeParameters(declaration); + + if (addTypeParam + && typeParameters.Any()) { - if (IsMissing(documentationComment, typeParameter)) + foreach (TypeParameterSyntax typeParameter in typeParameters) { - ReportDiagnostic(context, DiagnosticRules.AddTypeParamElementToDocumentationComment, documentationComment); - break; + if (IsMissing(documentationComment, typeParameter)) + { + ReportDiagnostic(context, DiagnosticRules.AddTypeParamElementToDocumentationComment, documentationComment); + break; + } } } - } - if (orderParams || invalidReference) - { - Analyze(context, documentationComment.Content, typeParameters, XmlTag.TypeParam, (nodes, name) => nodes.IndexOf(name)); + if (orderParams || invalidReference) + { + Analyze(context, documentationComment.Content, typeParameters, XmlTag.TypeParam, (nodes, name) => nodes.IndexOf(name)); + } } } } diff --git a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs index e5e21b0378..c24ed35eab 100644 --- a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs +++ b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs @@ -627,6 +627,26 @@ internal static bool IsPartOfDeclaration(this DocumentationCommentTriviaSyntax d return node is MemberDeclarationSyntax or LocalFunctionStatementSyntax || node?.Parent is MemberDeclarationSyntax or LocalFunctionStatementSyntax; } + + internal static SyntaxNode? GetDeclaringSyntaxForDocumentationComment(this DocumentationCommentTriviaSyntax documentationComment) + { + SyntaxNode? node = documentationComment.ParentTrivia.Token.Parent; + + while (node is not null) + { + if (node is MemberDeclarationSyntax or LocalFunctionStatementSyntax) + return node; + +#if ROSLYN_5_0 + if (node.IsKind(SyntaxKind.ExtensionBlockDeclaration)) + return node; +#endif + + node = node.Parent; + } + + return null; + } #endregion DocumentationCommentTriviaSyntax #region DoStatementSyntax diff --git a/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs b/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs index 8d980db15c..5b23366097 100644 --- a/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs @@ -205,6 +205,36 @@ public struct Foo(string value) { public string Value { get; } = value; } +"""); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.InvalidReferenceInDocumentationComment)] + public async Task TestNoDiagnostic_PrimaryConstructorWithSpacedParameterList() + { + await VerifyNoDiagnosticAsync(""" +/// +/// Text +/// +/// Text +record struct RS ( int par ); + +/// +/// Text +/// +/// Text +internal class C ( int prop ) +{ + int Prop { get; init; } = prop; +} +"""); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.InvalidReferenceInDocumentationComment)] + public async Task TestNoDiagnostic_PrimaryConstructorParameterDocumentation() + { + await VerifyNoDiagnosticAsync(""" +record struct RS( +/// Text +int par); """); } } From 24d06c2dd039a72a7c5cabbb3601704226e8a551 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Sun, 9 Aug 2026 17:44:23 +0200 Subject: [PATCH 2/4] chore: add PR link to changelog entry Co-authored-by: Cursor --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index bbcb5b489c..b22dde52f7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix analyzer [RCS1263](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1263) for primary constructor parameters in documentation comments +- Fix analyzer [RCS1263](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1263) for primary constructor parameters in documentation comments ([#1730](https://github.com/dotnet/roslynator/issues/1730)) ([PR](https://github.com/dotnet/roslynator/pull/1803)) - Fix analyzer [RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231) to not suggest `in` for `ref struct` parameters ([#1725](https://github.com/dotnet/roslynator/issues/1725)) ([PR](https://github.com/dotnet/roslynator/pull/1807)) - Fix analyzer [RCS1260](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1260) false positive for `omit_when_single_line` on multi-line object/collection initializers ([#1439](https://github.com/dotnet/roslynator/issues/1439)) ([PR](https://github.com/dotnet/roslynator/pull/1808)) - Fix analyzer [RCS0036](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0036) to report blank lines between single-line declarations in records ([PR](https://github.com/dotnet/roslynator/pull/1813)) From 89d09b795c3730cd094ac1f491615ae0df5ca0a5 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Fri, 14 Aug 2026 19:36:04 +0200 Subject: [PATCH 3/4] refactor(analyzers): drop redundant extension-block XmlDoc walk ExtensionBlockDeclarationSyntax already matches MemberDeclarationSyntax. Co-authored-by: Cursor --- src/CSharp/CSharp/Extensions/SyntaxExtensions.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs index c24ed35eab..e37b51612f 100644 --- a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs +++ b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs @@ -637,11 +637,6 @@ internal static bool IsPartOfDeclaration(this DocumentationCommentTriviaSyntax d if (node is MemberDeclarationSyntax or LocalFunctionStatementSyntax) return node; -#if ROSLYN_5_0 - if (node.IsKind(SyntaxKind.ExtensionBlockDeclaration)) - return node; -#endif - node = node.Parent; } From 143ada0f7cebda9393535c8c88ab3b2fcd583ee7 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Fri, 14 Aug 2026 23:48:23 +0200 Subject: [PATCH 4/4] refactor(analyzers): drop RCS1263 ancestor walk, keep regression tests Primary constructor param refs already resolve via GetParameterList. Keep the spaced-parameter-list coverage from #1730. Co-authored-by: Cursor --- ...eLineDocumentationCommentTriviaAnalyzer.cs | 63 ++++++++----------- .../CSharp/Extensions/SyntaxExtensions.cs | 15 ----- ...lidReferenceInDocumentationCommentTests.cs | 11 +--- 3 files changed, 27 insertions(+), 62 deletions(-) diff --git a/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs b/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs index 5c171cd7ad..f2272c54f5 100644 --- a/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/SingleLineDocumentationCommentTriviaAnalyzer.cs @@ -198,38 +198,29 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys bool addParam = DiagnosticRules.AddParamElementToDocumentationComment.IsEffective(context); bool addTypeParam = DiagnosticRules.AddTypeParamElementToDocumentationComment.IsEffective(context); - SyntaxNode declaration = (addParam - || orderParams - || invalidReference - || addTypeParam) - ? documentationComment.GetDeclaringSyntaxForDocumentationComment() - : null; - if (addParam || orderParams || invalidReference) { - if (declaration is not null) - { - SeparatedSyntaxList parameters = CSharpUtility.GetParameters(declaration); + SeparatedSyntaxList parameters = CSharpUtility.GetParameters( + (parent is MemberDeclarationSyntax) ? parent : parent.Parent); - if (addParam - && parameters.Any()) + if (addParam + && parameters.Any()) + { + foreach (ParameterSyntax parameter in parameters) { - foreach (ParameterSyntax parameter in parameters) + if (IsMissing(documentationComment, parameter)) { - if (IsMissing(documentationComment, parameter)) - { - ReportDiagnostic(context, DiagnosticRules.AddParamElementToDocumentationComment, documentationComment); - break; - } + ReportDiagnostic(context, DiagnosticRules.AddParamElementToDocumentationComment, documentationComment); + break; } } + } - if (orderParams || invalidReference) - { - Analyze(context, documentationComment.Content, parameters, XmlTag.Param, (nodes, name) => nodes.IndexOf(name)); - } + if (orderParams || invalidReference) + { + Analyze(context, documentationComment.Content, parameters, XmlTag.Param, (nodes, name) => nodes.IndexOf(name)); } } @@ -237,27 +228,25 @@ private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalys || orderParams || invalidReference) { - if (declaration is not null) - { - SeparatedSyntaxList typeParameters = CSharpUtility.GetTypeParameters(declaration); + SeparatedSyntaxList typeParameters = CSharpUtility.GetTypeParameters( + (parent is MemberDeclarationSyntax) ? parent : parent.Parent); - if (addTypeParam - && typeParameters.Any()) + if (addTypeParam + && typeParameters.Any()) + { + foreach (TypeParameterSyntax typeParameter in typeParameters) { - foreach (TypeParameterSyntax typeParameter in typeParameters) + if (IsMissing(documentationComment, typeParameter)) { - if (IsMissing(documentationComment, typeParameter)) - { - ReportDiagnostic(context, DiagnosticRules.AddTypeParamElementToDocumentationComment, documentationComment); - break; - } + ReportDiagnostic(context, DiagnosticRules.AddTypeParamElementToDocumentationComment, documentationComment); + break; } } + } - if (orderParams || invalidReference) - { - Analyze(context, documentationComment.Content, typeParameters, XmlTag.TypeParam, (nodes, name) => nodes.IndexOf(name)); - } + if (orderParams || invalidReference) + { + Analyze(context, documentationComment.Content, typeParameters, XmlTag.TypeParam, (nodes, name) => nodes.IndexOf(name)); } } } diff --git a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs index e37b51612f..e5e21b0378 100644 --- a/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs +++ b/src/CSharp/CSharp/Extensions/SyntaxExtensions.cs @@ -627,21 +627,6 @@ internal static bool IsPartOfDeclaration(this DocumentationCommentTriviaSyntax d return node is MemberDeclarationSyntax or LocalFunctionStatementSyntax || node?.Parent is MemberDeclarationSyntax or LocalFunctionStatementSyntax; } - - internal static SyntaxNode? GetDeclaringSyntaxForDocumentationComment(this DocumentationCommentTriviaSyntax documentationComment) - { - SyntaxNode? node = documentationComment.ParentTrivia.Token.Parent; - - while (node is not null) - { - if (node is MemberDeclarationSyntax or LocalFunctionStatementSyntax) - return node; - - node = node.Parent; - } - - return null; - } #endregion DocumentationCommentTriviaSyntax #region DoStatementSyntax diff --git a/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs b/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs index 5b23366097..d4f2f47cfd 100644 --- a/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1263InvalidReferenceInDocumentationCommentTests.cs @@ -207,6 +207,7 @@ public struct Foo(string value) } """); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.InvalidReferenceInDocumentationComment)] public async Task TestNoDiagnostic_PrimaryConstructorWithSpacedParameterList() { @@ -225,16 +226,6 @@ internal class C ( int prop ) { int Prop { get; init; } = prop; } -"""); - } - - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.InvalidReferenceInDocumentationComment)] - public async Task TestNoDiagnostic_PrimaryConstructorParameterDocumentation() - { - await VerifyNoDiagnosticAsync(""" -record struct RS( -/// Text -int par); """); } }