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

Refactor parse tree inspections #5404

Merged

Conversation

MDoerner
Copy link
Contributor

Closes #2719

This PR introduces a base class for the IInspectionListener implementations and extends the common code in the ParseTreeInspectionBase.

In addition, the InspectionBase gets modified in two ways. First, a new method is introduced to run inspections separately per module. Second, the State property gets removed, allowing the inspections to depend only on IDeclarationFinderProvider instead.

Furthermore, the three inspections ImplicitByRefModifierInspection, RedundantByRefModifierInspection and ProcedureCanBeWrittenAsFunctionInspection get turned into declaration inspections, since this very much simplifies them.

Since a new member RequiredArguments is added to IAnnotation, the MissingAnnotationArgumentInspection can be made more extensible and simple. It also only looks at annotations provided by the declaration finder instead of parse trees now.

There are also a few more small fixes and refactorings.

This PR is still WIP, since I have to clean up some FIXMEs in UnreacahbleCaseInspection.

Still, I wanted to get this PR out to get feedback and to pose the question whether we should always provide a declaration finder as argument to the abstract function DoGetInspectionResults in InspectionBase since most inspections need one and get it immediately in that method—with the exception of the parse tree inspections—and since, moreover, the enclosing method GetInspectionResults uses one anyway to determine whether results are ignored.

Also adds a base for inspection listeners.
The DeclarationFinderProvider remains protected for now.
Require an IDeclarationFinderProvider instead of a RubberduckParserState.
Also adds the missing configurationd for the references libraries in tests.
Also removes the use of a CodeModule.
Also refactors the class a bit and fixes several possible multiple enumeration issues.
It specifies the minimal number of arguments for the annotation.
Also adds an overload of DeclarationFinder.FindAnnotations providing all annotations in a module. IllegalAnnotationInspection now uses this in favor of RubberduckParserState.GetAnnotations, which allows to remove the dependency on the state.
@MDoerner MDoerner added the PR-Status: WIP Pull request is work-in-progress, more commits should be expected. label Feb 27, 2020
@codecov
Copy link

codecov bot commented Feb 27, 2020

Codecov Report

Merging #5404 into next will increase coverage by 0.6%.
The diff coverage is 84.91%.

@@            Coverage Diff            @@
##             next    #5404     +/-   ##
=========================================
+ Coverage   60.91%   61.51%   +0.6%     
=========================================
  Files        1175     1195     +20     
  Lines       40332    40570    +238     
  Branches     8536     8644    +108     
=========================================
+ Hits        24568    24955    +387     
+ Misses      13824    13788     -36     
+ Partials     1940     1827    -113
Impacted Files Coverage Δ
...rduck.Core/UI/Settings/GeneralSettingsViewModel.cs 57.47% <ø> (ø) ⬆️
...Concrete/UnreachableCaseInspection/FilterLimits.cs 67.39% <ø> (ø) ⬆️
...sis/QuickFixes/Concrete/RemoveAttributeQuickFix.cs 58.82% <ø> (ø)
...eAnalysis/Settings/CodeInspectionConfigProvider.cs 94.12% <ø> (ø) ⬆️
...ncrete/UnreachableCaseInspection/ParseTreeValue.cs 85.45% <ø> (+1.82%) ⬆️
...Parsing/Preprocessing/CompilationArgumentsCache.cs 100% <ø> (ø) ⬆️
...ctions/Results/QualifiedContextInspectionResult.cs 100% <ø> (ø) ⬆️
...QuickFixes/Concrete/ExpandDefaultMemberQuickFix.cs 78.26% <ø> (ø)
...ickFixes/Concrete/AdjustAttributeValuesQuickFix.cs 50% <ø> (ø)
...rters/CodeInspectionSeverityEnumToTextConverter.cs 0% <ø> (ø) ⬆️
... and 403 more

Also add allowed number of arguments to IAnnotation interface.
Also removes the special testing interface from ParseTreeValueVisitor.
The inspector swallows exceptions, which we really do not want in tests.
It now correctly depends on the module and passes along the found results instead of aggregating them in an instance variable.
This allows to remove the corresponding state from the ParseTreeValueVisitor.
Copy link
Contributor

@bclothier bclothier left a comment

Choose a reason for hiding this comment

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

Great work as always; I skimmed over minor changes but have the following questions.

public virtual CodeKind TargetKindOfCode => CodeKind.CodePaneCode;
}

public class InspectionListenerBase : VBAParserBaseListener, IInspectionListener
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we might want to consider sticking to the rule of one type per one file. Maybe even consider having all bases in a folder for easy grouping.

Copy link
Member

Choose a reason for hiding this comment

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

Except for generic abstract classes inherited from a non-generic abstract class? 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For parse tree inspections, we have always put the listeners into the same file.

I think, in many cases, we can actually make them private classes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I do agree with inlining the listeners in the implementation since most of the time they will be specific to only one inspection so it makes sense to make it a nested & private class. We do have to consider the discoverability aspect for the abstract classes, however. If I'm new to the code, I cannot see that there are both ParseTreeInspectionBase and InspectionListenerBase by viewing the filesystem alone. I'd have to use something like Find Implementation on the IInspectionListener to see that I need to create my listener based on the InspectionListenerBase. I am not sure off the cuff if the Find Implementation won't give cluttered results with all other concrete implementations.

string.Format(InspectionResults.BooleanAssignedInIfElseInspection,
(((VBAParser.IfStmtContext)result.Context).block().GetDescendent<VBAParser.LetStmtContext>()).lExpression().GetText().Trim()),
result));
var literalText = ((VBAParser.IfStmtContext) context.Context)
Copy link
Contributor

Choose a reason for hiding this comment

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

The way it is, it requires upcasting from general ParserContext to a specific context. Given that we had the specific context available to us when listening, are we better off constructing the strings and saving it in the contexts dictionary (as a tuple, perhaps), thus simplifying the ResultDescription and cut out potential runtime errors resulting from incorrect upcasting in all and future implementations of the parse tree inspections?

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 not a crazy idea at all. Actually there's something odd about losing all benefits of generics here... it's almost like.. like it wants to be string ResultDescription<TContext>(QualifiedContext<TContext> context) where TContext : ParserRuleContext? Or that's.. probably a rabbit hole in generics wonderland...

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 already have an idea how to wire up a generic version. However, this will need rework on all parse tree inspections and on the interface and its users as well.

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 the latest push, the parse tree inspection base classes and the listeners are generic with respect to the context type.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the latest changes is a vast improvement, nicely done! The only nitpick I have is that a select number of inspections use the general ParserRuleContext as the TContext due to the fact that they work with more than one contexts. Ideally, it would be possible to statically enumerate valid contexts they can have but I don't see a solution for that short of adding marker interfaces to various parser rule contexts. For this PR, I'm happy with the direction you have taken.

@@ -45,18 +43,16 @@ namespace Rubberduck.Inspections.Concrete
[Experimental(nameof(ExperimentalNames.EmptyBlockInspections))]
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated but is this still necessary? The same question applies to other empty whatever inspections.

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 remove the experimental attributes, if you want.

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 think we should change the severity to Hint, though.

Copy link
Member

Choose a reason for hiding this comment

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

@MDoerner agreed, Hint works. Were the attributes removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I remove them?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, now's a good time :)

@@ -13,15 +13,15 @@ public abstract class DeclarationInspectionBaseBase : InspectionBase
protected readonly DeclarationType[] RelevantDeclarationTypes;
protected readonly DeclarationType[] ExcludeDeclarationTypes;

protected DeclarationInspectionBaseBase(RubberduckParserState state, params DeclarationType[] relevantDeclarationTypes)
: base(state)
protected DeclarationInspectionBaseBase(IDeclarationFinderProvider declarationFinderProvider, params DeclarationType[] relevantDeclarationTypes)
Copy link
Member

Choose a reason for hiding this comment

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

This is why I'm less certain than before about the one class: one file rule/guideline, @bclothier: I'm thinking the only reason this class needs a silly name here is because of one class: one file, because DeclarationInspectionBase<T> wants to go into a DeclarationInspectionBase.cs file.
I'm pretty sure we could have DeclarationInspectionBase and DeclarationInspectionBase<T> in the same DeclarationInspectionBase.cs file, and avoid needing to name our abstract classes ThingBaseBase 😜

@MDoerner what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll admit that I am not consistent on that guideline myself. But FWIW, in this particular case, I had files RefactoringDialogBase and RefactoringDialogBaseGeneric. Note that in the generic file, the class is just RefactoringDialogBase<>; no need for BaseBase.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, but then you sacrifice the consistency of classname.cs! #NamingIsHard

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, in this particular example, this isn't generic. It's basically a InspectionBase implementing a particular implementation for declaration inspections. In which case, I'm not sure it should be DeclarationInspectionBaseBase; simply just DeclarationInspectionBase is sufficient. The fact that it's based on another abstract class (InspectionBase) is not really material and shouldn't factor into the naming, IMO.

Yes, agreed on #NamingIsHard.

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 have to admit that I don't really like the name myself. It is called that way because it is the base class for the other declaration inspection base classes. Apart from the ones you already mentioned there are also the two multi result base classes.
That it is based on InspectionBase is irrelevant for the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw, the generic and non-generic versions of the base classes are already in the same file for each kind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I do something about this class or not? Options are

  1. Rename DeclarationInspectionBase to DeclaratiinInspectionSingleResultBase and DeclarationInspectionBaseBase to DeclarationInspectionBase. That removes the BaseBase but opens room for confusion since this class should exclusively be implemented by base classes and not concrete inspection.
  2. Inline the class into all three implementation inspection base classes.
  3. Leave it as-is.

Copy link
Contributor

Choose a reason for hiding this comment

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

Your comment makes me realize that I had the BaseBase backward; I interpreted it as a Base class based on another Base class but you're meaning a Base class that needs to be implemented by other Base classes. I had to look at the DeclarationInspectionBase to see that it inherits DeclarationInspectionBaseBase. Obviously that ambiguity is not what we want.

The only suggestion I can think of is PartialBase, in similar fashion to the partial class to suggest that it's not a complete definition, but I am not sure that is any improvement. At very least, there should be a xml docs on the BaseBase explaining that it is meant to be implemented by other base classes and not directly.

@retailcoder
Copy link
Member

Still, I wanted to get this PR out to get feedback and to pose the question whether we should always provide a declaration finder as argument to the abstract function DoGetInspectionResults in InspectionBase since most inspections need one and get it immediately in that method—with the exception of the parse tree inspections—and since, moreover, the enclosing method GetInspectionResults uses one anyway to determine whether results are ignored.

Providing the finder as an argument of the templated method could eliminate the need to expose it in some protected manner, assuming it's only accessed in DoGetInspectionResults in derived types. That makes the encapsulation more complete, and the resulting API is much friendlier IMO; if I'm new to the code base and look at how inspections work, everything is easier to follow when you get parameters and use them to implement a procedure - it's almost like handling a Change event in a Worksheet module!

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.

The amount of tech debt being paid here is phenomenal - is the draft/WIP status still valid? Let's merge this!

@MDoerner
Copy link
Contributor Author

MDoerner commented Mar 4, 2020

The draft status is still valid. Based on comments, I will make both the listener base and the parse tree inspection base generic and wire them up correctly. Moreover, I am not done with UnreachableCaseInspection yet.

…ntations as method parameter

Before, the provider was protected and that was used, getting a new one in all base classes for the different kinds of inspections.
Also turns the ParseTreeValueVisitor in UnreachableCaseInspection into a constructor injected field.
Also turns the inspector in UnreachableCaseInspection into a constructor injected field.
@MDoerner MDoerner removed the PR-Status: WIP Pull request is work-in-progress, more commits should be expected. label Mar 6, 2020
@MDoerner MDoerner marked this pull request as ready for review March 6, 2020 23:54
@MDoerner
Copy link
Contributor Author

MDoerner commented Mar 6, 2020

I do not really like that ParseTreeValueVisitor still takes an optional constructor parameter solely for testing purposes, but I think I am done with what I wanted to do in this PR.

@@ -7,7 +7,7 @@

namespace Rubberduck.Inspections.Concrete
{
public class EmptyBlockInspectionListenerBase<TContext> : InspectionListenerBase<TContext>
internal class EmptyBlockInspectionListenerBase<TContext> : InspectionListenerBase<TContext>
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be cleaner to have an abstract EmptyBlockInspectionBase<TContext> with a protected EmptyBlockInspectionListenerBase<TContext> ContextListener { get; }? That way the listener becomes private to the EmptyBlockInspectionBase.

Copy link
Member

Choose a reason for hiding this comment

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

The Base suffix says it already, but having this an abstract class would make it compiler-enforced that it's not intended to be instantiated directly, too.

@MDoerner MDoerner force-pushed the RefactorParseTreeInspections branch from fb9b8bb to b527bb3 Compare March 7, 2020 19:49
@MDoerner
Copy link
Contributor Author

MDoerner commented Mar 8, 2020

PSA: once this PR gets merged, everybody might have to rebuild the RubberduckMeta solution because this PR fixes the inspection XML-doc analyzer. Otherwise, you might get false positives for missing name tags.
(Previously, the analyzer did not work at all, because the inspections were not in the expected namespace.)

Copy link
Contributor

@bclothier bclothier left a comment

Choose a reason for hiding this comment

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

I know the PR was merged already but few minor nitpicks maybe to address in future PRs.

<value>Duplicate value in 'name' attribute</value>
</data>
<data name="DuplicateNameAttributeDescription" xml:space="preserve">
<value>The 'name' attribute is used to uniquely identify an element. Consequently, it has to be unique.</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<value>The 'name' attribute is used to uniquely identify an element. Consequently, it has to be unique.</value>
<value>The 'name' attribute must uniquely identify an element.</value>

/// End Sub
/// ]]>
/// </example>
[RequiredHost(""NotExcel"")]
Copy link
Contributor

Choose a reason for hiding this comment

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

I have to point out that the way it is, there's coupling between the xml-docs and the attribute. It would be better for the markup generator to actually read the attributes instead of expecting a hostApp (or any elements), which means we only have one source of truth and no need to keep thing synchronized.

Copy link
Member

Choose a reason for hiding this comment

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

I believe MSBuild is the markup generator here... it reads XML-docs though, not attributes. That coupling is hopelessly necessary, unfortunately.

<value>Invalid value for 'type' attribute.</value>
</data>
<data name="InvalidTypeAttributeDescription" xml:space="preserve">
<value>The values of the 'type' attribute are restricted to the value 'Standard Module', 'Class Module', 'Document' and 'User Form'.</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: Isn't UserForm more typical?

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.

Procedure 'x' can be written as a function shouldn't fire if the parameter is an object
3 participants