Skip to content

Commit 891802f

Browse files
Adding coupling metrics (#8)
1 parent 947f64a commit 891802f

14 files changed

Lines changed: 631 additions & 27 deletions

File tree

CognitiveCodeAnalysis.Tests/src/CognitiveAnalysis/CognitiveAnalysisFacadeTests.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using CognitiveCodeAnalysis.CodeCoverage;
66
using CognitiveCodeAnalysis.CognitiveAnalysis;
77
using CognitiveCodeAnalysis.Configuration;
8+
using CognitiveCodeAnalysis.CouplingAnalysis;
89

910
namespace CognitiveCodeAnalysis.Tests.CognitiveAnalysis;
1011

@@ -23,7 +24,8 @@ public void SetUp()
2324
new CognitiveCodeAnalyser(),
2425
new CognitiveConfiguration(),
2526
new ScoreCalculator(),
26-
new CoberturaReader()
27+
new CoberturaReader(),
28+
new ClassCouplingAnalyser()
2729
);
2830
}
2931

@@ -131,7 +133,8 @@ public void LoadCoverageData_WithMatchingCoverage_UpdatesMetricsAndReturnsSucces
131133
new CognitiveCodeAnalyser(),
132134
new CognitiveConfiguration(),
133135
new ScoreCalculator(),
134-
new FakeCoverageReaderReturn( new[] { coverage } )
136+
new FakeCoverageReaderReturn( new[] { coverage } ),
137+
new ClassCouplingAnalyser()
135138
);
136139

137140
// Act
@@ -161,7 +164,8 @@ public void LoadCoverageData_WithNoCoverageEntries_ReturnsNoDataResult()
161164
new CognitiveCodeAnalyser(),
162165
new CognitiveConfiguration(),
163166
new ScoreCalculator(),
164-
new FakeCoverageReaderReturn(Enumerable.Empty<Coverage>())
167+
new FakeCoverageReaderReturn(Enumerable.Empty<Coverage>()),
168+
new ClassCouplingAnalyser()
165169
);
166170

167171
// Act
@@ -195,7 +199,8 @@ public void LoadCoverageData_WithCoverageButNoMatches_ReturnsSuccess()
195199
new CognitiveCodeAnalyser(),
196200
new CognitiveConfiguration(),
197201
new ScoreCalculator(),
198-
new FakeCoverageReaderReturn( new[] { coverage } )
202+
new FakeCoverageReaderReturn( new[] { coverage } ),
203+
new ClassCouplingAnalyser()
199204
);
200205

201206
// Act
@@ -219,7 +224,8 @@ public void LoadCoverageData_FileNotFoundException_ReturnsErrorMessage()
219224
new CognitiveCodeAnalyser(),
220225
new CognitiveConfiguration(),
221226
new ScoreCalculator(),
222-
new FakeCoverageReaderThrowFileNotFound()
227+
new FakeCoverageReaderThrowFileNotFound(),
228+
new ClassCouplingAnalyser()
223229
);
224230

225231
// Act

CognitiveCodeAnalysis.Tests/src/CognitiveAnalysis/Reports/HtmlReportGroupedTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using CognitiveCodeAnalysis.CognitiveAnalysis;
66
using CognitiveCodeAnalysis.CognitiveAnalysis.Reports;
77
using CognitiveCodeAnalysis.Configuration;
8+
using CognitiveCodeAnalysis.CouplingAnalysis;
89

910
namespace CognitiveCodeAnalysis.Tests.CognitiveAnalysis.Reports;
1011

@@ -55,6 +56,64 @@ public void HtmlReport_GroupByClass_RendersClassSection()
5556
}
5657
}
5758

59+
[Test]
60+
public void HtmlReport_GroupByClass_ShowCouplingMetrics_RendersCouplingLine()
61+
{
62+
var m = new CognitiveMetrics(
63+
methodName: "One",
64+
className: "Box",
65+
filePath: "src/Box.cs",
66+
methodSignature: "void One()",
67+
methodLineNumber: 3,
68+
ifCount: 0,
69+
elseCount: 0,
70+
loopCount: 0,
71+
switchCount: 0,
72+
tryCatchCount: 0,
73+
returnCount: 0,
74+
argumentCount: 0,
75+
linesOfCode: 2,
76+
nestingLevels: 0,
77+
cyclomaticComplexity: 1,
78+
localVariableCount: 0,
79+
fieldAccessCount: 0,
80+
propertyAccessCount: 0
81+
);
82+
m.totalScore = 1.1;
83+
var coll = new CognitiveMetricsCollection { m };
84+
coll.SetClassCouplingMetrics(
85+
[
86+
new ClassCouplingMetrics
87+
{
88+
ClassName = "Box",
89+
IncomingCoupling = 2,
90+
OutgoingCoupling = 5,
91+
Stability = 2.0 / 7.0,
92+
},
93+
]);
94+
var cfg = new CognitiveConfiguration
95+
{
96+
GroupByClass = true,
97+
ShowOnlyMethodsExceedingThreshold = false,
98+
ShowCouplingMetrics = true,
99+
};
100+
101+
var path = Path.Combine(Path.GetTempPath(), "html-coupling-" + Guid.NewGuid() + ".html");
102+
try
103+
{
104+
new HtmlReport().RenderMetrics(path, coll, cfg);
105+
var html = File.ReadAllText(path);
106+
Assert.That(html, Does.Contain("Coupling: In=2, Out=5, Stability=0.286"));
107+
}
108+
finally
109+
{
110+
if (File.Exists(path))
111+
{
112+
File.Delete(path);
113+
}
114+
}
115+
}
116+
58117
[Test]
59118
public void HtmlReport_GroupByClass_SortsByMaxClassScoreThenMethodScoreDescending()
60119
{
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
using CognitiveCodeAnalysis.CouplingAnalysis;
6+
7+
namespace CognitiveCodeAnalysis.Tests.CouplingAnalysis;
8+
9+
public class ClassCouplingAnalyserTests
10+
{
11+
private TempFiles _tempFiles = null!;
12+
private ClassCouplingAnalyser _analyser = null!;
13+
14+
[SetUp]
15+
public void SetUp()
16+
{
17+
_tempFiles = new TempFiles();
18+
_analyser = new ClassCouplingAnalyser();
19+
}
20+
21+
[TearDown]
22+
public void TearDown()
23+
{
24+
_tempFiles.CleanUp();
25+
}
26+
27+
[Test]
28+
public void Analyse_ChainOfDependencies_ComputesIncomingOutgoingAndStability()
29+
{
30+
_tempFiles.CreateFileWithContent(
31+
"A.cs",
32+
"""
33+
namespace Chain;
34+
public class A
35+
{
36+
private readonly B _b = new B();
37+
}
38+
"""
39+
);
40+
_tempFiles.CreateFileWithContent(
41+
"B.cs",
42+
"""
43+
namespace Chain;
44+
public class B
45+
{
46+
private readonly C _c = new C();
47+
}
48+
"""
49+
);
50+
_tempFiles.CreateFileWithContent(
51+
"C.cs",
52+
"""
53+
namespace Chain;
54+
public class C
55+
{
56+
public int Value;
57+
}
58+
"""
59+
);
60+
61+
var files = Directory.GetFiles(_tempFiles.tmpDirectory, "*.cs").OrderBy(f => f).ToList();
62+
var metrics = _analyser.Analyse(files).ToDictionary(m => m.ClassName);
63+
64+
Assert.That(metrics["Chain.A"].OutgoingCoupling, Is.EqualTo(1));
65+
Assert.That(metrics["Chain.A"].IncomingCoupling, Is.EqualTo(0));
66+
Assert.That(metrics["Chain.A"].Stability, Is.EqualTo(0.0));
67+
68+
Assert.That(metrics["Chain.B"].OutgoingCoupling, Is.EqualTo(1));
69+
Assert.That(metrics["Chain.B"].IncomingCoupling, Is.EqualTo(1));
70+
Assert.That(metrics["Chain.B"].Stability, Is.EqualTo(0.5).Within(0.001));
71+
72+
Assert.That(metrics["Chain.C"].OutgoingCoupling, Is.EqualTo(0));
73+
Assert.That(metrics["Chain.C"].IncomingCoupling, Is.EqualTo(1));
74+
Assert.That(metrics["Chain.C"].Stability, Is.EqualTo(1.0));
75+
}
76+
77+
[Test]
78+
public void Analyse_NoCrossTypeReferences_HasZeroCouplingAndStability()
79+
{
80+
_tempFiles.CreateFileWithContent(
81+
"Isolated.cs",
82+
"""
83+
namespace Solo;
84+
public class One
85+
{
86+
public int Add(int a, int b) => a + b;
87+
}
88+
"""
89+
);
90+
91+
var file = Directory.GetFiles(_tempFiles.tmpDirectory, "*.cs").Single();
92+
var metric = _analyser.Analyse([file]).Single();
93+
94+
Assert.That(metric.ClassName, Is.EqualTo("Solo.One"));
95+
Assert.That(metric.IncomingCoupling, Is.EqualTo(0));
96+
Assert.That(metric.OutgoingCoupling, Is.EqualTo(0));
97+
Assert.That(metric.Stability, Is.EqualTo(0.0));
98+
}
99+
100+
[Test]
101+
public void Analyse_PartialClassAcrossFiles_UsesSameCouplingForFqcn()
102+
{
103+
_tempFiles.CreateFileWithContent(
104+
"Partial1.cs",
105+
"""
106+
namespace Parts;
107+
public partial class Widget
108+
{
109+
private readonly Helper _helper = new Helper();
110+
}
111+
"""
112+
);
113+
_tempFiles.CreateFileWithContent(
114+
"Partial2.cs",
115+
"""
116+
namespace Parts;
117+
public partial class Widget
118+
{
119+
public int Id;
120+
}
121+
"""
122+
);
123+
_tempFiles.CreateFileWithContent(
124+
"Helper.cs",
125+
"""
126+
namespace Parts;
127+
public class Helper
128+
{
129+
public string Name = "x";
130+
}
131+
"""
132+
);
133+
134+
var files = Directory.GetFiles(_tempFiles.tmpDirectory, "*.cs").ToList();
135+
var metrics = _analyser.Analyse(files).ToDictionary(m => m.ClassName);
136+
137+
Assert.That(metrics["Parts.Widget"].OutgoingCoupling, Is.EqualTo(1));
138+
Assert.That(metrics["Parts.Widget"].IncomingCoupling, Is.EqualTo(0));
139+
Assert.That(metrics["Parts.Helper"].IncomingCoupling, Is.EqualTo(1));
140+
}
141+
142+
[Test]
143+
public void Analyse_EmptyFileList_ReturnsEmpty()
144+
{
145+
Assert.That(_analyser.Analyse([]), Is.Empty);
146+
}
147+
}

CognitiveCodeAnalysis/cognitive-metrics-settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"showOnlyMethodsExceedingThreshold": true,
77
"showHalsteadComplexity": false,
88
"showCyclomaticComplexity": false,
9+
"showCouplingMetrics": false,
910
"showDetailedCognitiveMetrics": true,
1011
"groupByClass": true,
1112
"countElseAsNesting": false,

CognitiveCodeAnalysis/src/CognitiveAnalysis/CognitiveAnalysisFacade.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using CognitiveCodeAnalysis.CodeCoverage;
66
using CognitiveCodeAnalysis.Configuration;
7+
using CognitiveCodeAnalysis.CouplingAnalysis;
78

89
namespace CognitiveCodeAnalysis.CognitiveAnalysis;
910

@@ -12,7 +13,8 @@ public class CognitiveAnalysisFacade(
1213
CognitiveCodeAnalyser analyser,
1314
CognitiveConfiguration cognitiveConfiguration,
1415
ScoreCalculator calculator,
15-
ICoverageReader coverageReader
16+
ICoverageReader coverageReader,
17+
ClassCouplingAnalyser classCouplingAnalyser
1618
) {
1719
public List<string> FindSourceFiles(string[] sourcePaths)
1820
{
@@ -41,6 +43,8 @@ CognitiveConfiguration configuration
4143
calculator.CalculateScores(metrics, configuration);
4244
}
4345

46+
metricsCollection.SetClassCouplingMetrics(classCouplingAnalyser.Analyse(files));
47+
4448
return metricsCollection;
4549
}
4650

CognitiveCodeAnalysis/src/CognitiveAnalysis/CognitiveMetricsCollection.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,38 @@
44

55
using System.Collections.ObjectModel;
66

7+
using CognitiveCodeAnalysis.CouplingAnalysis;
8+
79
using Microsoft.Extensions.Configuration;
810

911
namespace CognitiveCodeAnalysis.CognitiveAnalysis;
1012

1113
public class CognitiveMetricsCollection: Collection<CognitiveMetrics>
1214
{
15+
private readonly Dictionary<string, ClassCouplingMetrics> _classCouplingByName =
16+
new(StringComparer.Ordinal);
17+
18+
public void SetClassCouplingMetrics(IEnumerable<ClassCouplingMetrics> metrics)
19+
{
20+
_classCouplingByName.Clear();
21+
foreach (ClassCouplingMetrics metric in metrics)
22+
{
23+
_classCouplingByName[metric.ClassName] = metric;
24+
}
25+
}
26+
27+
public bool TryGetClassCoupling(string className, out ClassCouplingMetrics? coupling)
28+
{
29+
if (_classCouplingByName.TryGetValue(className, out ClassCouplingMetrics? value))
30+
{
31+
coupling = value;
32+
return true;
33+
}
34+
35+
coupling = null;
36+
return false;
37+
}
38+
1339
public CognitiveMetricsCollection OnlyMetricsExceedingScoreThreshold(double scoreThreshold)
1440
{
1541
CognitiveMetricsCollection filtered = [];

0 commit comments

Comments
 (0)