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

Remove Regex-Replaces/Raw String ops from #2660 part 2 #2733

Merged
merged 17 commits into from Mar 1, 2017

Conversation

BZngr
Copy link
Contributor

@BZngr BZngr commented Feb 23, 2017

Follow up to rejected PR to #2714. Updates AssignedByValParameterQuickFixes to use ParseTree results. Incorporates comments from PR #2174. Adds an ICodeModuleExtensions class per PR #2714 comments.


namespace Rubberduck.Common
{
public static class ICodeModuleExtensions
Copy link
Member

Choose a reason for hiding this comment

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

The I prefix indicates an interface; I'd rename this one to CodeModuleExtensions; this is similar to LINQ's Enumerable static class, which extends the IEnumerable interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will make the change.

var result = ReplaceStringAtIndex(original, token.Text, replacement, token.Column);
module.ReplaceLine(token.Line, result);
}
public static void ReplaceIdentifierReferenceName(this ICodeModule module, IdentifierReference identifierReference, string replacement)
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: I like to have an empty line between members of a class - that would be consistent with the rest of the code base.

Copy link
Member

Choose a reason for hiding this comment

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

That said the class looks pretty neat otherwise 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will make the change.

public override bool CanFixInProject { get { return false; } }

//This function exists solely to support unit testing
public void TESTONLY_FixUsingAutoGeneratedName(string suggestedName = "")
Copy link
Member

Choose a reason for hiding this comment

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

I don't like this... a class shouldn't have the knowledge of whether or not it's being executed in a test context. There has to be a better way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Take a look at the RenameDeclarationQuickFix and associated tests for an example of how to approach this. The "testing only" part is basically just to suppress the dialog - if you make the dialog a dependency, you can mock it in the tests.

{
view.NewName = _localCopyVariableName;
view.IdentifierNamesAlreadyDeclared = _variableNamesAccessibleToProcedureContext;
if (!_isQuickFixUnitTest)
Copy link
Member

Choose a reason for hiding this comment

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

The problem is that there's this AssignedByValParameterQuickFixDialog dependency that needs to be tamed.

You could constructor-inject an abstract factory here - and Ninject will do all the hard work!

public interface IAssignedByValParameterQuickFixDialogFactory
{
    IAssignedByValParameterQuickFixDialog Create(Declaration target, Selection selection, string[] accessibleNames);
}

Just make the dialog implement an interface that's named after it (with the I prefix) - that interface could look like this:

public interface IAssignedByValParameterQuickFixDialog : IDisposable
{
    void ShowDialog();
    DialogResult DialogResult { get; }
    string NewName { get; }
}

Make the dialog implement that interface (just tacking : IAssignedByValParameterQuickFixDialog to its definition should be enough, from what I'm seeing)

And then the AssignedByValParameterMakeLocalCopyQuickFix can take an IAssignedByValParameterQuickFixDialogFactory constructor parameter and store that abstract factory in a private readonly field:

private readonly IAssignedByValParameterQuickFixDialogFactory _dialogFactory;

Now, instead of doing using (var view = new AssignedByValParameterQuickFixDialogFactory(_target, Selection)), you can do this:

using (var view = _dialogFactory.Create(_target, Selection, _variableNamesAccessibleToProcedureContext))

And now whoever creates a AssignedByValParameterMakeLocalCopyQuickFix is responsible for passing the dependencies to the constructor - I think that would be the inspection result class... so for now let's make that inspection result's constructor intake the IAssignedByValParameterQuickFixDialogFactory dependency through its constructor, and then since the inspection result is new'd up by the inspection itself, the inspection's constructor for now will have to intake the IAssignedByValParameterQuickFixDialogFactory dependency through its constructor as well.

That's dirty dependency injection, but it should "just work". We'll revisit the injecting depedencies of dependencies of dependencies issue when the inspections feature is re-architected.

Copy link
Member

Choose a reason for hiding this comment

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

Forgot to mention: the abstract factory doesn't need a concrete implementation - Ninject will deal with that automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for providing all this detail. I'll look at the examples and get rid of the dependency (I was a little surprised this topic didn't come up in the very first PR!).

var firstBlockLineNumber = blocks.FirstOrDefault().Start.Line;

var module = Selection.QualifiedName.Component.CodeModule;
module.InsertLines(firstBlockLineNumber++, BuildLocalCopyDeclaration());
Copy link
Member

Choose a reason for hiding this comment

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

why the ++ here? it might be clearer to extract that into a separate statement.

Alternatively you could join the two declarations and insert them together

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 can extract it. Originally, I had a CodeModuleExtension function InsertLinesAfter( int lineNumber, string[] lines) but thought it might be overkill for inserting just two lines. Would putting a function like this in the CodeModule extension class make sense in general?

@@ -647,7 +647,7 @@ If the parameter can be null, ignore this inspection result; passing a null valu
<data name="ApplicationWorksheetFunctionQuickFix" xml:space="preserve">
<value>Use Application.WorksheetFunction explicitly.</value>
</data>
<data name="AssignedByValParameterQuickFix" xml:space="preserve">
<data name="AssignedByValParameterMakeLocalCopyQuickFix" xml:space="preserve">
Copy link
Member

Choose a reason for hiding this comment

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

Note: needs to also be adjusted in the other languages

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There were not pre-existing entries in fr.resx and de.resx to replace for 'AssignedByValParameterQuickFix'. So, I'm planning to add the entry with the english description unless someone can provide translations...Or, if it is better to leave them missing than entered in english. Thanks.

Copy link
Member

Choose a reason for hiding this comment

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

If they don't exist yet, you may as well leave them missing ... Because then the translators for the respective languages (me and retailcoder) need to get to those either way. On that note: @retailcoder we should probably do a i18n check for the next release either way ...

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.

👍

@BZngr
Copy link
Contributor Author

BZngr commented Feb 24, 2017

This version removes the QuickFix's dependency on the dialog class. Used two implementations of a factory interface to accomplish the result. Could not get figure out how to get there with Mock so created a simple Mock class of my own. At the bottom of AssignedByValParameterMakeLocalCopyQuickFixTests.cs you'll find the test-related factory implementation and Mocked class.

[TestClass]
public class AssignedByValParameterMakeLocalCopyQuickFixTests
public class AssignedByValParameterMakeLocalCopyQuickFixTests : IAssignedByValParameterQuickFixDialogFactory
Copy link
Member

Choose a reason for hiding this comment

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

The test class implements the factory interface... why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The QuickFix calls "Create(...)" on the factory interface passed into the constructor. During unit testing, the Testing class passes in a factory interface that points to itself so that it can construct a and return an interface to the MockDialog in order to support the tests.


namespace Rubberduck.UI.Refactorings
{
public interface IAssignedByValParameterQuickFixDialog : IDialogView
Copy link
Member

Choose a reason for hiding this comment

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

👍 extends IDialogView

@@ -25,9 +26,10 @@ public override IEnumerable<QuickFixBase> QuickFixes
{
get
{
IAssignedByValParameterQuickFixDialogFactory factory = new AssignedByValParameterQuickFixDialogFactory();
Copy link
Member

Choose a reason for hiding this comment

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

The factory is a dependency, it should be constructor-injected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, do you mean moving this (or a similar) line of code to the constructor of the Inspection class? I'm OK with moving it....I'm wondering if the constructor of the inspection should carry the factory interface dependency for a dialog component of one of the three quickfixes - it seems a little 'high up' - but it's likely I'm not understanding what you are advocating. Could you comment a bit more? Thanks.

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 went back and reviewed your earlier comments on this topic...I can see that they are consistent with the above comment - I think I just lost track of where to bring in the dependency once I started coding. Soooo...I'll have another update coming soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This latest update to the PR (2/25) is much closer (I'm hoping ) to what you expected to see. The couple lines of code I added to RubberDuckModule.cs look out of place to me, so I suspect there is a better way. As I'm sure you've already figured out, using Ninject is new to me...just another one of many things here at RD!.

@@ -425,6 +429,10 @@ private IEnumerable<ICommandMenuItem> GetRubberduckCommandBarItems()

private IEnumerable<IMenuItem> GetRubberduckMenuItems()
{
//This bind needs to occur before the the new array is built by the function calls
Copy link
Member

Choose a reason for hiding this comment

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

That's because the factory interface naming convention is kicking in and binding to an automatic factory implementation; if you want to override that binding you can use Rebind - please keep GetRubberduckMenuItems dedicated to returning the RD menu items.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback on this particular change. Using Rebind did not succeed...I'm thinking it is because the Inspection objects are constructed just once (when the menu items are created) and there is never another opportunity to inject the redefined dependency with Rebind(). (True?) I've moved the Bind definition to a sequentially (and contextually) correct place in the body of the "Load()' function.


namespace Rubberduck.UI.Refactorings
{
public class AssignedByValParameterQuickFixMockDialogFactory : IAssignedByValParameterQuickFixDialogFactory
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to make actual mock implementations to inject in the tests - we use a mocking framework for that (Moq).

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'll figure out how to use the Moq framework.

@@ -49,7 +49,7 @@ public override void Fix()

private void RequestLocalCopyVariableName()
{
using( var view = _factory.Create(_target.IdentifierName, _target.DeclarationType.ToString()))
using( var view = _dialogFactory.Create(_target.IdentifierName, _target.DeclarationType.ToString()))
Copy link
Member

Choose a reason for hiding this comment

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

isn't this blowing up with a null reference exception in the tests when you pass a null factory to the inspection? You'll want to give it a Mock<IAssignedByValParameterQuickFixDialogFactory> object, something like this:

var mock = new Mock<IAssignedByValParameterQuickFixDialogFactory>();
mock.SetupGet(m => m.NewName).Returns(() => WhateverTheTestExpectsTheNewNameToBe);
mock.SetupGet(m => m.IdentifierNamesAlreadyDeclared).Returns(() => WhateverTheTestNeedsToWorkWith);
mock.SetupGet(m => m.IsCancelled).Returns(() => false); // or true, to test cancellation behavior

var inspection = new AssignedByValParameterInspection(mockParser.State, mock.Object);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All the tests pass free of any exceptions - The factory reference is not de-referenced unless the test invokes "Fix()" on the MakeLocalCopyQuickFix. Once I sort out to use the Moq framework for the factory - I'll pass it to the constructor.

Copy link
Member

Choose a reason for hiding this comment

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

@BZngr ah, sorry I missed that! don't bother setting up a mock for a test that doesn't need it ;-)

I guess a mock will be useful for a test that tests the quickfix itself.

@retailcoder
Copy link
Member

I don't have push access to your fork so I can't fix the merge conflict from here, but it should be a pretty simple one to fix 😃

Let me know when you're ready to merge the PR!

@BZngr
Copy link
Contributor Author

BZngr commented Feb 27, 2017

Yes - the conflicts are straight forward - locally I've already resolved them. Yet to add the code to use the Moq framework. Hoping to have time to do it later today. Thanks.

@BZngr
Copy link
Contributor Author

BZngr commented Feb 28, 2017

All comments addressed - I'm thinking it's finally ready to go.

@Vogel612
Copy link
Member

Currently failing tests on AppVeyor:

  • EnclosingProjectComesBeforeOtherProceduralModule
  • EnclosingProjectComesBeforeOtherModuleInEnclosingProject
  • GivenLocalDeclarationAsQualifiedClassName_ResolvesFirstPartToProject

I don't know in which way that was caused by this PR ...

@BZngr
Copy link
Contributor Author

BZngr commented Feb 28, 2017

When I look at the content of these tests, any relationship to this PR is not obvious to me. The tests appear to be independent of code that was modified. I also ran these tests in loops. The results vary slightly with each run of the loops. However, the results below are typical:

EnclosingProjectComesBeforeOtherProceduralModule

  • Run Mode: succeeded 60 out of 100 tests
  • Debug Mode: succeeded 92 out of 100 tests

EnclosingProjectComesBeforeOtherModuleInEnclosingProject

  • Run Mode: succeeded 63 out of 100 tests
  • Debug Mode: succeeded 91 out of 100 tests

GivenLocalDeclarationAsQualifiedClassName_ResolvesFIrstPartToProject

  • Run Mode: succeeded 55 out of 100 tests
  • Debug Mode: succeeded 90 out of 100 tests

@MDoerner
Copy link
Contributor

@BZngr Do not worry about the failed tests. These are three of our beloved six tests that fail an pass randomly for about two months now. We still have not figured out what is causing it but it must be some concurrency issue in the tests.

@BZngr
Copy link
Contributor Author

BZngr commented Feb 28, 2017

FWIW - There is a simple way to mask the concurrency issue yet allow the tests to run and detect 'hard' failures. Just give the Parser several tries to get the right answer prior to evaluating the results within the Assert. The example below is for 'EnclosingProjectComesBeforeOtherProceduralModule':

for (int idx = 1; idx < 25 && declaration.References.Count() != 1; idx++)
{
     state = Parse(vbe);
     declaration = state.AllUserDeclarations
          .Single(d => d.DeclarationType == DeclarationType.Project 
              && d.IdentifierName == BINDING_TARGET_NAME);
}

This approach works for all three tests. The loop is different for GivenLocalDeclarationAsQualifiedClassName_ResolvesFIrstPartToProject, but the principle is the same.

@BZngr
Copy link
Contributor Author

BZngr commented Mar 1, 2017

Looking for some input. The PR as it stands is an improvement over the version of the QuickFix currently in 'next'. That said, there is still some work to do to expand the assistance to the user in choosing a new name. That is, to include module scope and global scope variable name checks. I am currently working on this. So...my question is regarding the preference at this point - and I am good with either. Do you want to hold off on this PR - and I'll continue to push improvements as I have them ready? Or, proceed as it is to get the current improvements and identify the additional checks as a bug. Just wanted to make sure it was clear what is missing (as far as I know) from the PR as it currently exits. Thanks.

@retailcoder
Copy link
Member

@BZngr sorry for the delay! no worries, I just hadn't had a chance to get to it yet. On it!

@retailcoder retailcoder merged commit 359cbcf into rubberduck-vba:next Mar 1, 2017
retailcoder added a commit that referenced this pull request Mar 1, 2017
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

5 participants