Skip to content

Commit a4d6173

Browse files
authored
Merge pull request #3522 from Vogel612/code-metrics
Introduces Code Metrics feature
2 parents bc12cf4 + 9a46e6e commit a4d6173

28 files changed

+3473
-33
lines changed

RetailCoder.VBE/Extension.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ private void Startup()
221221
catch (Exception e)
222222
{
223223
_logger.Log(LogLevel.Fatal, e, "Startup sequence threw an unexpected exception.");
224-
//throw; // <<~ uncomment to crash the process
224+
#if DEBUG
225+
throw; // <<~ uncomment to crash the process
226+
#endif
225227
}
226228
}
227229

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Rubberduck.Parsing.VBA;
4+
using Antlr4.Runtime.Tree;
5+
using Rubberduck.Parsing.Grammar;
6+
using Rubberduck.VBEditor;
7+
using Antlr4.Runtime.Misc;
8+
using Rubberduck.Parsing.Symbols;
9+
using Rubberduck.SmartIndenter;
10+
11+
namespace Rubberduck.Navigation.CodeMetrics
12+
{
13+
public class CodeMetricsAnalyst : ICodeMetricsAnalyst
14+
{
15+
private readonly IIndenterSettings _indenterSettings;
16+
17+
public CodeMetricsAnalyst(IIndenterSettings indenterSettings)
18+
{
19+
_indenterSettings = indenterSettings;
20+
}
21+
22+
public IEnumerable<ModuleMetricsResult> ModuleMetrics(RubberduckParserState state)
23+
{
24+
if (state == null || !state.AllUserDeclarations.Any())
25+
{
26+
// must not return Enumerable.Empty
27+
yield break;
28+
}
29+
30+
var trees = state.ParseTrees;
31+
32+
foreach (var moduleTree in trees)
33+
{
34+
yield return GetModuleResult(moduleTree.Key, moduleTree.Value, state.DeclarationFinder);
35+
};
36+
}
37+
38+
public ModuleMetricsResult GetModuleResult(RubberduckParserState state, QualifiedModuleName qmn)
39+
{
40+
return GetModuleResult(qmn, state.GetParseTree(qmn), state.DeclarationFinder);
41+
}
42+
43+
private ModuleMetricsResult GetModuleResult(QualifiedModuleName qmn, IParseTree moduleTree, DeclarationFinder declarationFinder)
44+
{
45+
// Consider rewrite as visitor? That should make subtrees easier and allow us to expand metrics
46+
var cmListener = new CodeMetricsListener(declarationFinder, _indenterSettings);
47+
ParseTreeWalker.Default.Walk(cmListener, moduleTree);
48+
return cmListener.GetMetricsResult(qmn);
49+
}
50+
51+
52+
private class CodeMetricsListener : VBAParserBaseListener
53+
{
54+
private readonly DeclarationFinder _finder;
55+
private readonly IIndenterSettings _indenterSettings;
56+
57+
private Declaration _currentMember;
58+
private List<CodeMetricsResult> _results = new List<CodeMetricsResult>();
59+
private List<CodeMetricsResult> _moduleResults = new List<CodeMetricsResult>();
60+
61+
private List<MemberMetricsResult> _memberResults = new List<MemberMetricsResult>();
62+
63+
public CodeMetricsListener(DeclarationFinder finder, IIndenterSettings indenterSettings)
64+
{
65+
_finder = finder;
66+
_indenterSettings = indenterSettings;
67+
}
68+
69+
public override void EnterEndOfLine([NotNull] VBAParser.EndOfLineContext context)
70+
{
71+
int followingIndentationLevel = 0;
72+
// we have a proper newline
73+
if (context.NEWLINE() != null)
74+
{
75+
// the last whitespace, which is the one in front of the next line's contents
76+
var followingWhitespace = context.whiteSpace().LastOrDefault();
77+
followingIndentationLevel = IndentationLevelFromWhitespace(followingWhitespace);
78+
}
79+
(_currentMember == null ? _moduleResults : _results).Add(new CodeMetricsResult(1, 0, followingIndentationLevel));
80+
}
81+
82+
public override void EnterIfStmt([NotNull] VBAParser.IfStmtContext context)
83+
{
84+
_results.Add(new CodeMetricsResult(0, 1, 0));
85+
}
86+
87+
public override void EnterElseIfBlock([NotNull] VBAParser.ElseIfBlockContext context)
88+
{
89+
_results.Add(new CodeMetricsResult(0, 1, 0));
90+
}
91+
92+
// notably: NO additional complexity for an Else-Block
93+
94+
public override void EnterForEachStmt([NotNull] VBAParser.ForEachStmtContext context)
95+
{
96+
_results.Add(new CodeMetricsResult(0, 1, 0));
97+
}
98+
99+
public override void EnterForNextStmt([NotNull] VBAParser.ForNextStmtContext context)
100+
{
101+
_results.Add(new CodeMetricsResult(0, 1, 0));
102+
}
103+
104+
public override void EnterCaseClause([NotNull] VBAParser.CaseClauseContext context)
105+
{
106+
_results.Add(new CodeMetricsResult(0, 1, 0));
107+
}
108+
109+
public override void EnterSubStmt([NotNull] VBAParser.SubStmtContext context)
110+
{
111+
_results.Add(new CodeMetricsResult(0, 1, 0));
112+
_currentMember = _finder.UserDeclarations(DeclarationType.Procedure).Where(d => d.Context == context).First();
113+
}
114+
115+
public override void ExitSubStmt([NotNull] VBAParser.SubStmtContext context)
116+
{
117+
ExitMeasurableMember();
118+
}
119+
120+
public override void EnterFunctionStmt([NotNull] VBAParser.FunctionStmtContext context)
121+
{
122+
_results.Add(new CodeMetricsResult(0, 1, 0));
123+
_currentMember = _finder.UserDeclarations(DeclarationType.Function).Where(d => d.Context == context).First();
124+
}
125+
126+
public override void ExitFunctionStmt([NotNull] VBAParser.FunctionStmtContext context)
127+
{
128+
ExitMeasurableMember();
129+
}
130+
131+
public override void EnterPropertyGetStmt([NotNull] VBAParser.PropertyGetStmtContext context)
132+
{
133+
_results.Add(new CodeMetricsResult(0, 1, 0));
134+
_currentMember = _finder.UserDeclarations(DeclarationType.PropertyGet).Where(d => d.Context == context).First();
135+
}
136+
137+
public override void ExitPropertyGetStmt([NotNull] VBAParser.PropertyGetStmtContext context)
138+
{
139+
ExitMeasurableMember();
140+
}
141+
142+
public override void EnterPropertyLetStmt([NotNull] VBAParser.PropertyLetStmtContext context)
143+
{
144+
_results.Add(new CodeMetricsResult(0, 1, 0));
145+
_currentMember = _finder.UserDeclarations(DeclarationType.PropertyLet).Where(d => d.Context == context).First();
146+
}
147+
148+
public override void ExitPropertyLetStmt([NotNull] VBAParser.PropertyLetStmtContext context)
149+
{
150+
ExitMeasurableMember();
151+
}
152+
153+
public override void EnterPropertySetStmt([NotNull] VBAParser.PropertySetStmtContext context)
154+
{
155+
_results.Add(new CodeMetricsResult(0, 1, 0));
156+
_currentMember = _finder.UserDeclarations(DeclarationType.PropertySet).Where(d => d.Context == context).First();
157+
}
158+
159+
public override void ExitPropertySetStmt([NotNull] VBAParser.PropertySetStmtContext context)
160+
{
161+
ExitMeasurableMember();
162+
}
163+
164+
public override void EnterBlockStmt([NotNull] VBAParser.BlockStmtContext context)
165+
{
166+
// there is a whitespace context here after the option of a statementLabel.
167+
// we need to account for that
168+
_results.Add(new CodeMetricsResult(0, 0, IndentationLevelFromWhitespace(context.whiteSpace())));
169+
}
170+
171+
private int IndentationLevelFromWhitespace(VBAParser.WhiteSpaceContext wsContext)
172+
{
173+
if (wsContext == null) return 0;
174+
// the only thing that contains underscores is the line-continuation at this point
175+
var lineContinuation = wsContext.children.LastOrDefault((tree) => tree.GetText().Contains("_"));
176+
var index = lineContinuation != null ? wsContext.children.IndexOf(lineContinuation) : 0;
177+
return (wsContext?.ChildCount ?? 0 - index) / _indenterSettings.IndentSpaces;
178+
}
179+
180+
private void ExitMeasurableMember()
181+
{
182+
_memberResults.Add(new MemberMetricsResult(_currentMember, _results));
183+
_results = new List<CodeMetricsResult>(); // reinitialize to drop results
184+
_currentMember = null;
185+
}
186+
187+
internal ModuleMetricsResult GetMetricsResult(QualifiedModuleName qmn)
188+
{
189+
return new ModuleMetricsResult(qmn, _memberResults, _moduleResults);
190+
}
191+
}
192+
}
193+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Rubberduck.Parsing.Symbols;
2+
using Rubberduck.VBEditor;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace Rubberduck.Navigation.CodeMetrics
8+
{
9+
public struct CodeMetricsResult
10+
{
11+
public CodeMetricsResult(int lines, int cyclomaticComplexity, int nesting)
12+
: this(lines, cyclomaticComplexity, nesting, Enumerable.Empty<CodeMetricsResult>())
13+
{
14+
}
15+
16+
public CodeMetricsResult(int lines, int cyclomaticComplexity, int nesting, IEnumerable<CodeMetricsResult> childScopeResults)
17+
{
18+
var childScopeMetric =
19+
childScopeResults.Aggregate(new CodeMetricsResult(), (r1, r2) => new CodeMetricsResult(r1.Lines + r2.Lines, r1.CyclomaticComplexity + r2.CyclomaticComplexity, Math.Max(r1.MaximumNesting, r2.MaximumNesting)));
20+
Lines = lines + childScopeMetric.Lines;
21+
CyclomaticComplexity = cyclomaticComplexity + childScopeMetric.CyclomaticComplexity;
22+
MaximumNesting = Math.Max(nesting, childScopeMetric.MaximumNesting);
23+
}
24+
25+
public int Lines { get; private set; }
26+
public int CyclomaticComplexity { get; private set; }
27+
public int MaximumNesting { get; private set; }
28+
29+
}
30+
31+
public struct MemberMetricsResult
32+
{
33+
public Declaration Member { get; private set; }
34+
public CodeMetricsResult Result { get; private set; }
35+
36+
public MemberMetricsResult(Declaration member, IEnumerable<CodeMetricsResult> contextResults)
37+
{
38+
Member = member;
39+
Result = new CodeMetricsResult(0, 0, 0, contextResults);
40+
}
41+
}
42+
43+
public struct ModuleMetricsResult
44+
{
45+
public QualifiedModuleName ModuleName { get; private set; }
46+
public CodeMetricsResult Result { get; private set; }
47+
public IReadOnlyDictionary<Declaration, CodeMetricsResult> MemberResults { get; private set; }
48+
49+
public ModuleMetricsResult(QualifiedModuleName moduleName, IEnumerable<MemberMetricsResult> memberMetricsResult, IEnumerable<CodeMetricsResult> nonMemberResults)
50+
{
51+
ModuleName = moduleName;
52+
MemberResults = memberMetricsResult.ToDictionary(mmr => mmr.Member, mmr => mmr.Result);
53+
Result = new CodeMetricsResult(0, 0, 0, nonMemberResults.Concat(MemberResults.Values));
54+
}
55+
}
56+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using NLog;
2+
using Rubberduck.Parsing.VBA;
3+
using Rubberduck.UI;
4+
using Rubberduck.UI.Command;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
namespace Rubberduck.Navigation.CodeMetrics
10+
{
11+
public class CodeMetricsViewModel : ViewModelBase, IDisposable
12+
{
13+
private readonly RubberduckParserState _state;
14+
private readonly ICodeMetricsAnalyst _analyst;
15+
16+
public CodeMetricsViewModel(RubberduckParserState state, ICodeMetricsAnalyst analyst)
17+
{
18+
_state = state;
19+
_analyst = analyst;
20+
_state.StateChanged += OnStateChanged;
21+
}
22+
23+
private void OnStateChanged(object sender, ParserStateEventArgs e)
24+
{
25+
if (e.State == ParserState.Ready)
26+
{
27+
IsBusy = true;
28+
ModuleMetrics = _analyst.ModuleMetrics(_state);
29+
IsBusy = false;
30+
}
31+
}
32+
33+
public void Dispose()
34+
{
35+
_state.StateChanged -= OnStateChanged;
36+
}
37+
38+
private IEnumerable<ModuleMetricsResult> _moduleMetrics;
39+
public IEnumerable<ModuleMetricsResult> ModuleMetrics {
40+
get => _moduleMetrics;
41+
private set
42+
{
43+
_moduleMetrics = value;
44+
OnPropertyChanged();
45+
}
46+
}
47+
48+
private bool _isBusy;
49+
public bool IsBusy
50+
{
51+
get => _isBusy;
52+
set
53+
{
54+
_isBusy = value;
55+
OnPropertyChanged();
56+
// If the window is "busy" then hide the Refresh message
57+
OnPropertyChanged("EmptyUIRefreshMessageVisibility");
58+
}
59+
}
60+
}
61+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Rubberduck.Parsing.VBA;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Rubberduck.Navigation.CodeMetrics
10+
{
11+
public interface ICodeMetricsAnalyst
12+
{
13+
IEnumerable<ModuleMetricsResult> ModuleMetrics(RubberduckParserState state);
14+
}
15+
}

RetailCoder.VBE/Root/RubberduckIoCInstaller.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
using Rubberduck.VBEditor.SafeComWrappers.Abstract;
4444
using Rubberduck.VBEditor.SafeComWrappers.Office.Core.Abstract;
4545
using Component = Castle.MicroKernel.Registration.Component;
46+
using Rubberduck.UI.CodeMetrics;
47+
using Rubberduck.Navigation.CodeMetrics;
4648

4749
namespace Rubberduck.Root
4850
{
@@ -463,6 +465,7 @@ private static Type[] NavigateMenuItems()
463465
#if DEBUG
464466
typeof(RegexSearchReplaceCommandMenuItem),
465467
#endif
468+
466469
typeof(FindSymbolCommandMenuItem),
467470
typeof(FindAllReferencesCommandMenuItem),
468471
typeof(FindAllImplementationsCommandMenuItem)
@@ -486,6 +489,7 @@ private Type[] ToolsMenuItems()
486489
{
487490
typeof(RegexAssistantCommandMenuItem),
488491
typeof(ToDoExplorerCommandMenuItem),
492+
typeof(CodeMetricsCommandMenuItem),
489493
typeof(ExportAllCommandMenuItem)
490494
};
491495

@@ -598,6 +602,12 @@ private void RegisterCommandsWithPresenters(IWindsorContainer container)
598602
.LifestyleTransient()
599603
.Named(typeof(CodeExplorerCommand).Name));
600604

605+
container.Register(Component.For<CommandBase>()
606+
.ImplementedBy<CodeMetricsCommand>()
607+
.DependsOn(Dependency.OnComponent<IDockablePresenter, CodeMetricsDockablePresenter>())
608+
.LifestyleSingleton()
609+
.Named(typeof(CodeMetricsCommand).Name));
610+
601611
container.Register(Component.For<CommandBase>()
602612
.ImplementedBy<ToDoExplorerCommand>()
603613
.DependsOn(Dependency.OnComponent<IDockablePresenter, ToDoExplorerDockablePresenter>())

0 commit comments

Comments
 (0)