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

Clean-up blank lines left behind by EncapsulateFieldRefactoring and MoveCloserToUsage #5555

Merged

Conversation

BZngr
Copy link
Contributor

@BZngr BZngr commented Aug 13, 2020

The EncapsulateFieldRefactoring potentially leaves large blocks of blank lines in the scenarios when the user elects to encapsulate numerous fields by wrapping them in a UDT.

Residual newlines also occur in the MoveCloserToUsage refactoring and quickfix. It is less noticeable since these actions delete declarations one at a time. But, running the refactoring/quickfix against many declarations in the same module can potentially result in a large block of residual blank lines.

To accomplish the newline removals, this PR introduces an IModuleRewriterExtensions class in Refactorings.Common. The purpose of the extension methods are to provide a convenient mechanism to clear residual newlines left behind by the removal of variable and member declarations.

IModuleRewrite.Remove does not remove the EndOfStmtContext which can result in orphaned newlines. The extension method related to variable declarations also correctly handles removal of a variable declaration list in the scenario where are all variables in the list are identified for removal. (calling IModuleRewrite.Remove on each variable in a list leaves the VisibilityContext behind)

BZngr added 7 commits June 4, 2020 09:09
Extends the IModuleRewriter.Remove() method to clear residual blank
lines between the removed declaration and the next declaration.
Supports VariableDeclarations and ModuleBodyElementDeclarations
removals.  Also improves Variable declaration list handling.
Incorporates IModuleRewriter extension method to clean up newlines left
by declaration removals.  Removes EncapsulateFieldRewriteSession wrapper
class that previously handled declaration removals.
Incorporates IModuleRewriter extension method to clean up newlines left
by declaration removals.
Copy link
Contributor

@MDoerner MDoerner left a comment

Choose a reason for hiding this comment

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

LGTM
Just a few comments on style and reenumeration safety and, well, my typical concerns about using extension methods.

/// </remarks>
public static void RemoveVariables(this IModuleRewriter rewriter, IEnumerable<VariableDeclaration> toRemove, bool removeEndOfStmtContext = true)
{
if (!toRemove.Any()) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

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

To follow the style conventions in this project, please put the return; on its own line.

if (!toRemove.Any()) { return; }

var fieldsByListContext = toRemove.Distinct()
.GroupBy(f => f.Context.GetAncestor<VBAParser.VariableListStmtContext>());
Copy link
Contributor

Choose a reason for hiding this comment

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

The general style used in the project is to indent method calls on new lines one further than the previous line.


namespace Rubberduck.Refactorings.Common
{
public static class IModuleRewriterExtensions
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not a fan of extension methods since they come with a few drawbacks.

  • You cannot swap them out via a different implementation.
  • You cannot mock them in tests.

However, using them here might be OK. There really is no sense in providing an alternate implementation here and we never assert on the calls to the rewriters in tests; we assert on the result, instead. To me the question remains whether the new methods should actually be part of the IModuleRewriter interface in the first place. Opinions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since RD 'owns' ModuleRewriter, adding the methods to IModuleRewriter is an available choice and certainly avoids the drawbacks you mention. Even though I used an extension class, I'm not advocating that extension methods are the best choice here. For a PR submission, I am very reluctant to modify such a ubiquitous interface as IModuleRewriter without specific direction...So, extension methods provide a less impactful way to initially add/propose new behavior. I'm good with making the extension methods part of the IModuleRewriter if that is preferred.

/// </remarks>
public static void RemoveMembers(this IModuleRewriter rewriter, IEnumerable<ModuleBodyElementDeclaration> toRemove, bool removeEndOfStmtContext = true)
{
if (!toRemove.Any()) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

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

style


foreach (var fieldsGroup in fieldsByListContext)
{
var variables = fieldsGroup.Key.children.Where(ch => ch is VBAParser.VariableSubStmtContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

This would probably look cleaner as OfType<VBAParser.VariableSubStmtContext>().

var variables = fieldsGroup.Key.children.Where(ch => ch is VBAParser.VariableSubStmtContext);
if (variables.Count() == fieldsGroup.Count())
{
if (fieldsGroup.First().ParentDeclaration.DeclarationType.HasFlag(DeclarationType.Module))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a second iteration of fieldsGroup. Accordingly, it should be materialized via ToList where it is defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand the comment..though I am curious as how to best materialize the List since fieldsGroup is defined within a foreach (var fieldsGroup in fieldsByListContext) statement. Currently, I've added var fieldsToDelete = fieldsGroup.ToList(); and use fieldsToDelete for the Count() and First() calls which (I believe) resolves the comment.

Is there another more preferred way in this scenario?...perhaps using ToLookup to instantiate fieldsByListContext instead of GroupBy resolves this as well without calling ToList on fieldsGroup?

Copy link
Contributor

Choose a reason for hiding this comment

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

That resolves the issue. You could alternatively use ToDictionary on fieldsByListContext. There should be an extension method turning it into a Dictionary<TKey, List<TValue>>.


refactorRewriteSession = RefactorRewrite(model, refactorRewriteSession);
RefactorRewrite(model, refactorRewriteSession as IRewriteSession);
Copy link
Contributor

Choose a reason for hiding this comment

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

The cast seems to be redundant; the return type of CheckOutCodePaneSession extends IRewrtiteSession.
Or is there another overload taking an IExecutableRewriteSession?

{
refactorRewriteSession.Remove(field.Declaration, rewriter);
}
rewriter.RemoveVariables(SelectedFields.Select(f => f.Declaration).Cast<VariableDeclaration>());
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add a line break for each LINQ call. However, that is a question of style.

InsertNewDeclaration(target, rewriteSession);
RemoveOldDeclaration(target, rewriteSession);
UpdateQualifiedCalls(target, rewriteSession);
if (target is VariableDeclaration variable)
Copy link
Contributor

Choose a reason for hiding this comment

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

That should probably be inverted. The result is a guard clause and no need to use return.

Retains use of IModuleRewriterExtensions class.
@BZngr
Copy link
Contributor Author

BZngr commented Aug 15, 2020

Code is updated per the comments. Retained the IModuleRewriterExtensions class rather than modify IModuleRewriter.

Copy link
Member

@retailcoder retailcoder left a comment

Choose a reason for hiding this comment

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

Not a huge fan of extension methods against our own types, but as Max explained this particular case does not interfere with testing, ...and I'm thinking this kind of methods could ultimately live in a set of extension namespaces or something, kind of like LINQ or Dapper, but to provide such specialized methods to RD's IModuleRewriter; we would have to think of a way to regroup them all so that they're easily reusable and we don't end up reinventing that wheel - I love all the extensive xmldoc going on here, great work as always!

@retailcoder retailcoder merged commit df01e8a into rubberduck-vba:next Aug 15, 2020
@BZngr BZngr deleted the EncapsulateField_ClearBlanklines branch September 25, 2020 00:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants