Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refactoring to convert a primary-constructor-parameter to a normal constructor. #70399

Merged
merged 41 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e8c123b
In progress
CyrusNajmabadi Oct 13, 2023
68398cc
In progress
CyrusNajmabadi Oct 13, 2023
0684d78
in progress
CyrusNajmabadi Oct 13, 2023
1f59609
First pass done
CyrusNajmabadi Oct 13, 2023
ea45ce5
Working test
CyrusNajmabadi Oct 13, 2023
0095685
porting tests
CyrusNajmabadi Oct 13, 2023
19cd15b
All tests
CyrusNajmabadi Oct 13, 2023
1c49061
In progress
CyrusNajmabadi Oct 13, 2023
1064923
Merge remote-tracking branch 'upstream/main' into convertToRegularCon…
CyrusNajmabadi Oct 14, 2023
e46646d
Narrow the search
CyrusNajmabadi Oct 14, 2023
9d3201f
Fixes
CyrusNajmabadi Oct 14, 2023
db3545f
Update nested type references
CyrusNajmabadi Oct 14, 2023
f736afa
in progress
CyrusNajmabadi Oct 14, 2023
48c0258
Fixes
CyrusNajmabadi Oct 14, 2023
5aeeb49
inner node
CyrusNajmabadi Oct 14, 2023
33d07b8
in progrss
CyrusNajmabadi Oct 14, 2023
7be704f
Update xml
CyrusNajmabadi Oct 14, 2023
2201c49
in proress
CyrusNajmabadi Oct 14, 2023
07e0b32
move all initializers
CyrusNajmabadi Oct 16, 2023
8f18337
Handle multiple initializers case
CyrusNajmabadi Oct 16, 2023
3f583e0
Cleaner doc code
CyrusNajmabadi Oct 16, 2023
0e7ef4d
Merge remote-tracking branch 'upstream/main' into convertToRegularCon…
CyrusNajmabadi Oct 16, 2023
eb60b9a
Doc comments
CyrusNajmabadi Oct 16, 2023
4823bc2
Fies
CyrusNajmabadi Oct 16, 2023
acca367
further along
CyrusNajmabadi Oct 16, 2023
9834647
Fixes
CyrusNajmabadi Oct 16, 2023
f3883a8
Workign docs
CyrusNajmabadi Oct 16, 2023
ff3b72d
More fixups
CyrusNajmabadi Oct 16, 2023
fca2ced
Do not know what's going on here
CyrusNajmabadi Oct 16, 2023
bab846b
Cleanup
CyrusNajmabadi Oct 16, 2023
2a51300
Move to new file
CyrusNajmabadi Oct 16, 2023
11fe527
Inlined
CyrusNajmabadi Oct 16, 2023
1cd956b
In progress
CyrusNajmabadi Oct 16, 2023
634b4c9
inline
CyrusNajmabadi Oct 16, 2023
23e2241
cleanup
CyrusNajmabadi Oct 16, 2023
700680b
simplify
CyrusNajmabadi Oct 16, 2023
806db14
Make readonly
CyrusNajmabadi Oct 16, 2023
74600a9
Fix indentation
CyrusNajmabadi Oct 16, 2023
eccf536
Rename tests
CyrusNajmabadi Oct 16, 2023
e533908
simplify
CyrusNajmabadi Oct 16, 2023
1b58f6c
indent
CyrusNajmabadi Oct 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ public static void AnalyzeNamedTypeStart(

for (var containingType = startSymbol.ContainingType; containingType != null; containingType = containingType.ContainingType)
{
var containgTypeAnalyzer = TryGetOrCreateAnalyzer(containingType);
RegisterFieldOrPropertyAnalysisIfNecessary(containgTypeAnalyzer);
var containingTypeAnalyzer = TryGetOrCreateAnalyzer(containingType);
RegisterFieldOrPropertyAnalysisIfNecessary(containingTypeAnalyzer);
Copy link
Member Author

Choose a reason for hiding this comment

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

mispeeling

}

// Now try to make the analyzer for this type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,12 @@ static TListSyntax RemoveElementIndentation<TListSyntax>(
getElements(list),
(p, _) =>
{
var parameterLeadingWhitespace = GetLeadingWhitespace(p);
if (parameterLeadingWhitespace.EndsWith(indentation))
var elementLeadingWhitespace = GetLeadingWhitespace(p);
if (elementLeadingWhitespace.EndsWith(indentation))
Copy link
Member Author

Choose a reason for hiding this comment

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

renamed to be more accurate. this helper was used for more htan just parameters.

{
var leadingTrivia = p.GetLeadingTrivia();
return p.WithLeadingTrivia(
leadingTrivia.Take(leadingTrivia.Count - 1).Concat(Whitespace(parameterLeadingWhitespace[..^indentation.Length])));
leadingTrivia.Take(leadingTrivia.Count - 1).Concat(Whitespace(elementLeadingWhitespace[..^indentation.Length])));
}

return p;
Expand Down
3 changes: 3 additions & 0 deletions src/Features/CSharp/Portable/CSharpFeaturesResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -609,4 +609,7 @@
<value>static int Main</value>
<comment>{Locked}</comment>
</data>
<data name="Convert_to_regular_constructor" xml:space="preserve">
<value>Convert to regular constructor</value>
</data>
</root>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.ConvertPrimaryToRegularConstructor;

using static SyntaxFactory;

internal sealed partial class ConvertPrimaryToRegularConstructorCodeRefactoringProvider
{
private static SyntaxTrivia GetDocComment(SyntaxTriviaList trivia)
=> trivia.LastOrDefault(t => t.IsSingleLineDocComment());

private static DocumentationCommentTriviaSyntax? GetDocCommentStructure(SyntaxTrivia trivia)
=> (DocumentationCommentTriviaSyntax?)trivia.GetStructure();

private static bool IsXmlElement(XmlNodeSyntax node, string name, [NotNullWhen(true)] out XmlElementSyntax? element)
{
element = node is XmlElementSyntax { StartTag.Name.LocalName.ValueText: var elementName } xmlElement && elementName == name
? xmlElement
: null;
return element != null;
}

private static TypeDeclarationSyntax RemoveParamXmlElements(TypeDeclarationSyntax typeDeclaration)
{
var triviaList = typeDeclaration.GetLeadingTrivia();
var trivia = GetDocComment(triviaList);
var docComment = GetDocCommentStructure(trivia);
if (docComment == null)
return typeDeclaration;

using var _ = ArrayBuilder<XmlNodeSyntax>.GetInstance(out var content);

foreach (var node in docComment.Content)
{
if (IsXmlElement(node, "param", out var paramElement))
{
// We're skipping a param node. Fixup any preceding text node we may have before it.
FixupLastTextNode();
}
else
{
content.Add(node);
}
}

if (content.All(c => c is XmlTextSyntax xmlText && xmlText.TextTokens.All(
t => t.Kind() == SyntaxKind.XmlTextLiteralNewLineToken || string.IsNullOrWhiteSpace(t.Text))))
{
// Nothing but param nodes. Just remove all the doc comments entirely.
var triviaIndex = triviaList.IndexOf(trivia);

// remove the doc comment itself
var updatedTriviaList = triviaList.RemoveAt(triviaIndex);

// If the comment was on a line that started with whitespace, remove that whitespce too.
if (triviaIndex > 0 && triviaList[triviaIndex - 1].IsWhitespace())
updatedTriviaList = updatedTriviaList.RemoveAt(triviaIndex - 1);

return typeDeclaration.WithLeadingTrivia(updatedTriviaList);
}
else
{
var updatedTrivia = Trivia(docComment.WithContent(List(content)));
return typeDeclaration.WithLeadingTrivia(triviaList.Replace(trivia, updatedTrivia));
}

void FixupLastTextNode()
{
var node = content.LastOrDefault();
if (node is not XmlTextSyntax xmlText)
return;

var tokens = xmlText.TextTokens;
var lastIndex = tokens.Count;
if (lastIndex - 1 >= 0 && tokens[lastIndex - 1].Kind() == SyntaxKind.XmlTextLiteralToken && string.IsNullOrWhiteSpace(tokens[lastIndex - 1].Text))
lastIndex--;

if (lastIndex - 1 >= 0 && tokens[lastIndex - 1].Kind() == SyntaxKind.XmlTextLiteralNewLineToken)
lastIndex--;

if (lastIndex == tokens.Count)
{
// no change necessary.
return;
}
else if (lastIndex == 0)
{
// Removed all tokens from the text node. So remove the text node entirely.
content.RemoveLast();
}
else
{
// Otherwise, replace with newlines stripped.
content[^1] = xmlText.WithTextTokens(TokenList(tokens.Take(lastIndex)));
}
}
}

private static ConstructorDeclarationSyntax WithTypeDeclarationParamDocComments(TypeDeclarationSyntax typeDeclaration, ConstructorDeclarationSyntax constructor)
{
// Now move the param tags on the type decl over to the constructor.
var triviaList = typeDeclaration.GetLeadingTrivia();
var trivia = GetDocComment(triviaList);
var docComment = GetDocCommentStructure(trivia);
if (docComment is not null)
{
using var _2 = ArrayBuilder<XmlNodeSyntax>.GetInstance(out var content);

for (int i = 0, n = docComment.Content.Count; i < n; i++)
{
var node = docComment.Content[i];
if (IsXmlElement(node, "param", out _))
{
content.Add(node);

// if the param tag is followed with a newline, then preserve that when transferring over.
if (i + 1 < docComment.Content.Count && IsDocCommentNewLine(docComment.Content[i + 1]))
content.Add(docComment.Content[i + 1]);
}
}

if (content.Count > 0)
{
if (!content[0].GetLeadingTrivia().Any(SyntaxKind.DocumentationCommentExteriorTrivia))
content[0] = content[0].WithLeadingTrivia(DocumentationCommentExterior("/// "));

content[^1] = content[^1].WithTrailingTrivia(EndOfLine(""));

var finalTrivia = DocumentationCommentTrivia(SyntaxKind.SingleLineDocumentationCommentTrivia, List(content));
return constructor.WithLeadingTrivia(Trivia(finalTrivia));
}
}

return constructor;
}

private static bool IsDocCommentNewLine(XmlNodeSyntax node)
{
if (node is not XmlTextSyntax xmlText)
return false;

foreach (var textToken in xmlText.TextTokens)
{
if (textToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken)
continue;

if (textToken.Kind() == SyntaxKind.XmlTextLiteralToken && string.IsNullOrWhiteSpace(textToken.Text))
continue;

return false;
}

return true;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading