Skip to content
New issue

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

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

Already on GitHub? # to your account

Initialize fields when possible when adding parameters to constructors. #76352

Merged

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Dec 10, 2024

The bulk of this PR is oriented around extracting needed functionality from several existing fixers, so it could be used by 'add parameter'.

Specifically, 'generate constructor' already had the logic to determine which fields/properties a constructor parameter could be assigned to. And 'initialize parameter' already had the logic to add such an assignment statement in a way that matched the existing code that was already in a constructor.

When extracting this logic, i used the following rules that we already follow for several similar scenarios.

  1. if the code doesn't need any VB/C# specific behavior, it goes in a static class in the corresponding feature area called FeatureNameHelpers.
  2. if the code does need VB/C# specific behavior, we define an IFeatureNameSerivice : ILanguageService and expose in that fashion.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Dec 10, 2024
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review December 10, 2024 08:01
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner December 10, 2024 08:01
@@ -45,7 +43,7 @@ protected override bool TryInitializeConstructorInitializerGeneration(
SyntaxNode node,
CancellationToken cancellationToken,
out SyntaxToken token,
out ImmutableArray<Argument> arguments,
out ImmutableArray<Argument<ExpressionSyntax>> arguments,
Copy link
Member Author

Choose a reason for hiding this comment

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

the 'Argument' type was moved out of a specific feature and placed in the 'CodeGeneration' namespace. It was previously using the TExpressionSyntax type from its container. But as a new top level type, it had to become generic, and this is the fallout.

invocationDocument,
method,
argumentType,
refKind,
argumentNameSuggestion,
new ParameterName(argumentNameSuggestion, isNamedArgument, tryMakeCamelCase: !method.ContainingType.IsRecord),
GetArgument(argument),
Copy link
Member Author

Choose a reason for hiding this comment

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

passing more information along to the 'Add Parameter' service so it can attempt to add an appropriate assignment if desired.

insertionIndex++;

var memberToAssignTo = await GetMemberToAssignToAsync(semanticDocument).ConfigureAwait(false);
Copy link
Member Author

Choose a reason for hiding this comment

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

this ends up calling into one service to see if it can find a field/prop to assign to.

if (semanticDocument.SemanticModel.GetOperation(body, cancellationToken) is IBlockOperation blockOperation)
{
initializeParameterService.AddAssignment(
methodNode, blockOperation, parameterSymbol, memberToAssignTo, editor);
Copy link
Member Author

Choose a reason for hiding this comment

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

and, if so, calls into the InitializeParameterService to actually add the assignment.

By reusing these services, we share as much code logic as possible, so the features don't have to dupe them.

if (argument is null)
return null;

var (_, parameterToExistingMember, _, _) = await GenerateConstructorHelpers.GetParametersAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

we don't need the other values returned here. just the computation about which existing fields we could map to.

@@ -405,161 +409,6 @@ private async Task<bool> TryDetermineTypeToGenerateInAsync(
return TypeToGenerateIn?.TypeKind is (TypeKind?)TypeKind.Class or (TypeKind?)TypeKind.Struct;
}

private void GetParameters(
Copy link
Member Author

Choose a reason for hiding this comment

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

this helper moved to a shared static class location. We want to be able to compute things like which existing fields/props can be initialized with the parameters we're adding in a common way across several features.

@@ -117,4 +123,222 @@ private static bool IsCompatible<TExpressionSyntax>(

return true;
}

public static async Task<
Copy link
Member Author

Choose a reason for hiding this comment

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

this is just a move of the shared logic. It stayed the same except that it returns the values as it cannot directly as#to the destination fields of a type as it used to.

Copy link
Member Author

Choose a reason for hiding this comment

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

all the logic to the end of the file is just a move and is unchange.d

protected override SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement)
=> InitializeParameterHelpers.TryGetLastStatement(blockStatement);

protected override void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, StatementSyntax statement)
Copy link
Member Author

Choose a reason for hiding this comment

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

all these methods moved out of the refactoring provider into the lang service instead. that way other refactorings providers can access them.

{
this.s = s;
this.t = t;
this.i = i;
Copy link
Member Author

Choose a reason for hiding this comment

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

here we get the logic for free that the assignment is placed correctly between the two existing assignments for the parameters that come before/after the added parameter.

{
this.s = s;
this.t = t;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

here we get the logic for free that the expression body is rewritten into a block body, with the assignment correctly placed.


public C(string s, string t, string i)
{
(this.s, this.t, this.i) = (s, t, i);
Copy link
Member Author

Choose a reason for hiding this comment

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

here we get the logic for free that we recognize and updated an existing tuple assignment with the new field assignment.

@CyrusNajmabadi
Copy link
Member Author

Moving to draft whiel #76359 is being reviewed.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as draft December 10, 2024 21:42
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review December 10, 2024 23:21
@CyrusNajmabadi
Copy link
Member Author

@JoeRobich @ToddGrun ptal. this is now out of draft.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants