Skip to content

Commit 4ce8874

Browse files
Refactor ScoreCalculator to streamline metric calculations and improve score mapping
- Enhanced the `ScoreCalculator` class by introducing dedicated methods for retrieving count values and setting score values based on metric fields, improving code clarity and maintainability. - Implemented a switch expression for `GetCountValue` to simplify the retrieval of metric counts, and added reflection-based methods for dynamic property access. - Updated unit tests to ensure comprehensive coverage of the new score calculation logic, validating the correct mapping of known count metrics. - Aimed to optimize the cognitive analysis process and enhance the accuracy of score calculations.
1 parent e0a6200 commit 4ce8874

4 files changed

Lines changed: 115 additions & 27 deletions

File tree

CognitiveCodeAnalysis.Tests/src/CognitiveAnalysis/ScoreCalculatorTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,46 @@ public void CalculateScoresWithValidMetric()
146146
}
147147
}
148148

149+
[Test]
150+
public void CalculateScores_MapsAllRemainingKnownCountMetrics()
151+
{
152+
var metrics = new CognitiveMetrics(
153+
methodName: "TestMethod",
154+
className: "TestClass",
155+
filePath: "TestFile.cs",
156+
methodSignature: "TestMethod()",
157+
methodLineNumber: 1,
158+
loopCount: 5,
159+
switchCount: 3,
160+
tryCatchCount: 2,
161+
nestingLevels: 4,
162+
fieldAccessCount: 6
163+
);
164+
165+
var configuration = new CognitiveConfiguration
166+
{
167+
Metrics = new Dictionary<string, MetricConfiguration>
168+
{
169+
{ "loopCount", new MetricConfiguration { Scale = 1.0, Threshold = 2, Enabled = true } },
170+
{ "switchCount", new MetricConfiguration { Scale = 1.0, Threshold = 1, Enabled = true } },
171+
{ "tryCatchCount", new MetricConfiguration { Scale = 1.0, Threshold = 1, Enabled = true } },
172+
{ "nestingLevels", new MetricConfiguration { Scale = 1.0, Threshold = 2, Enabled = true } },
173+
{ "fieldAccessCount", new MetricConfiguration { Scale = 5.0, Threshold = 2, Enabled = true } },
174+
}
175+
};
176+
177+
new ScoreCalculator().CalculateScores(metrics, configuration);
178+
179+
using (Assert.EnterMultipleScope())
180+
{
181+
Assert.That(metrics.loopScore, Is.EqualTo(Math.Log(1 + (5.0 - 2.0) / 1.0)));
182+
Assert.That(metrics.switchScore, Is.EqualTo(Math.Log(1 + (3.0 - 1.0) / 1.0)));
183+
Assert.That(metrics.tryCatchScore, Is.EqualTo(Math.Log(1 + (2.0 - 1.0) / 1.0)));
184+
Assert.That(metrics.nestingScore, Is.EqualTo(Math.Log(1 + (4.0 - 2.0) / 1.0)));
185+
Assert.That(metrics.fieldAccessScore, Is.EqualTo(Math.Log(1 + (6.0 - 2.0) / 5.0)));
186+
}
187+
}
188+
149189
private static CognitiveConfiguration GetConfiguration()
150190
{
151191
CognitiveConfiguration configuration = new()

CognitiveCodeAnalysis/src/CognitiveAnalysis/ScoreCalculator.cs

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,87 @@ public CognitiveMetrics CalculateScores(CognitiveMetrics metrics, CognitiveConfi
2323
private static CognitiveMetrics CalculateMetric(
2424
CognitiveMetrics metrics,
2525
KeyValuePair<string, MetricConfiguration> keyValuePair
26-
)
27-
{
26+
) {
2827
if (!keyValuePair.Value.Enabled)
2928
{
3029
return metrics;
3130
}
3231

3332
string metricField = keyValuePair.Key;
33+
double count = GetCountValue(metrics, metricField);
34+
double score = CalculateLogWeight(
35+
value: count,
36+
threshold: keyValuePair.Value.Threshold,
37+
scale: keyValuePair.Value.Scale
38+
);
39+
SetScoreValue(metrics, metricField, score);
3440

35-
// Convert metric key to PascalCase property name (simple conversion)
36-
string countPropertyName = ToPascalCase(metricField);
41+
return metrics;
42+
}
43+
44+
private static double GetCountValue(CognitiveMetrics metrics, string metricField) => metricField switch
45+
{
46+
"ifCount" => metrics.ifCount,
47+
"elseCount" => metrics.elseCount,
48+
"loopCount" => metrics.loopCount,
49+
"switchCount" => metrics.switchCount,
50+
"tryCatchCount" => metrics.tryCatchCount,
51+
"returnCount" => metrics.returnCount,
52+
"argumentCount" => metrics.argumentCount,
53+
"nestingLevels" => metrics.nestingLevels,
54+
"linesOfCode" => metrics.linesOfCode,
55+
"localVariableCount" => metrics.localVariableCount,
56+
"fieldAccessCount" => metrics.fieldAccessCount,
57+
"propertyAccessCount" => metrics.propertyAccessCount,
58+
"cyclomaticComplexity" => metrics.cyclomaticComplexity,
59+
_ => GetCountViaReflection(metrics, metricField)
60+
};
61+
62+
private static void SetScoreValue(CognitiveMetrics metrics, string metricField, double score)
63+
{
64+
switch (metricField)
65+
{
66+
case "ifCount": metrics.ifScore = score; break;
67+
case "elseCount": metrics.elseScore = score; break;
68+
case "loopCount": metrics.loopScore = score; break;
69+
case "switchCount": metrics.switchScore = score; break;
70+
case "tryCatchCount": metrics.tryCatchScore = score; break;
71+
case "returnCount": metrics.returnScore = score; break;
72+
case "argumentCount": metrics.argumentScore = score; break;
73+
case "nestingLevels": metrics.nestingScore = score; break;
74+
case "linesOfCode": metrics.linesOfCodeScore = score; break;
75+
case "localVariableCount": metrics.localVariableScore = score; break;
76+
case "fieldAccessCount": metrics.fieldAccessScore = score; break;
77+
case "propertyAccessCount": metrics.propertyAccessScore = score; break;
78+
default: SetScoreViaReflection(metrics, metricField, score); break;
79+
}
80+
}
3781

82+
private static double GetCountViaReflection(CognitiveMetrics metrics, string metricField)
83+
{
84+
string countPropertyName = ToPascalCase(metricField);
3885
Type metricsType = metrics.GetType();
3986

40-
// Try property first (PascalCase), otherwise try field (original key, camelCase)
4187
PropertyInfo? countProperty = metricsType.GetProperty(countPropertyName, BindingFlags.Public | BindingFlags.Instance);
4288
FieldInfo? countField = metricsType.GetField(metricField, BindingFlags.Public | BindingFlags.Instance);
4389

4490
if (countProperty == null && countField == null)
4591
{
46-
return metrics;
92+
return 0.0;
4793
}
4894

4995
object? countValue = countProperty != null
5096
? countProperty.GetValue(metrics)
5197
: countField?.GetValue(metrics);
5298

53-
if (countValue == null)
54-
{
55-
return metrics;
56-
}
57-
58-
double count = Convert.ToDouble(countValue);
59-
double score = CalculateLogWeight(
60-
value: count,
61-
threshold: keyValuePair.Value.Threshold,
62-
scale: keyValuePair.Value.Scale
63-
);
99+
return countValue == null ? 0.0 : Convert.ToDouble(countValue);
100+
}
64101

65-
// Set score to property/field with same name but "Score" suffix (e.g., "ifCount" -> "ifScore" or "IfScore")
102+
private static void SetScoreViaReflection(CognitiveMetrics metrics, string metricField, double score)
103+
{
66104
string scoreFieldName = GetScoreFieldName(metricField);
67105
string scorePropertyName = ToPascalCase(scoreFieldName);
106+
Type metricsType = metrics.GetType();
68107

69108
PropertyInfo? scoreProperty = metricsType.GetProperty(scorePropertyName, BindingFlags.Public | BindingFlags.Instance);
70109
FieldInfo? scoreField = metricsType.GetField(scoreFieldName, BindingFlags.Public | BindingFlags.Instance);
@@ -77,15 +116,9 @@ KeyValuePair<string, MetricConfiguration> keyValuePair
77116
{
78117
scoreField.SetValue(metrics, score);
79118
}
80-
81-
return metrics;
82119
}
83120

84-
private static double CalculateLogWeight(
85-
double value,
86-
double threshold,
87-
double scale = 1.0
88-
)
121+
private static double CalculateLogWeight(double value, double threshold, double scale = 1.0)
89122
{
90123
if (value <= threshold) return 0.0;
91124

CognitiveCodeAnalysisConsoleApp/src/Progress/SpectreAnalysisProgressReporter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ private void UpdateSpectreTasks(AnalysisProgress update)
229229
State.ReportDescription ?? "Writing report",
230230
maxValue: State.ReportMaxValue
231231
);
232+
// Expire the throttle so ShouldRefresh returns true immediately,
233+
// ensuring the progress bar appears at 0% before items process.
234+
_lastRefreshTick = 0;
232235
}
233236

234237
if (_reportTask != null)

CognitiveCodeAnalysisConsoleApp/src/Progress/SpectreProgressSession.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,24 @@ namespace CognitiveCodeAnalysisConsoleApp.Progress;
1010

1111
internal static class SpectreProgressSession
1212
{
13+
/// <summary>
14+
/// Synchronous IProgress adapter that invokes the reporter directly on the calling thread,
15+
/// avoiding the async thread-pool dispatch of <see cref="Progress{T}"/>. This ensures the
16+
/// Spectre progress state is always up-to-date before <see cref="SpectreAnalysisProgressReporter.FinalizeSession"/>
17+
/// is called, and prevents progress events from being lost when work completes faster than
18+
/// the thread pool can drain its queue.
19+
/// </summary>
20+
private sealed class SynchronousProgress(SpectreAnalysisProgressReporter reporter) : IProgress<AnalysisProgress>
21+
{
22+
public void Report(AnalysisProgress value) => reporter.Report(value);
23+
}
24+
1325
public static void Run(Action<SpectreAnalysisProgressReporter, IProgress<AnalysisProgress>> action)
1426
{
1527
if (!AnsiConsole.Profile.Capabilities.Interactive)
1628
{
1729
var silentReporter = new SpectreAnalysisProgressReporter();
18-
var silentProgress = new Progress<AnalysisProgress>(silentReporter.Report);
30+
var silentProgress = new SynchronousProgress(silentReporter);
1931
action(silentReporter, silentProgress);
2032
silentReporter.FinalizeSession();
2133
silentReporter.FlushPendingMessages();
@@ -37,7 +49,7 @@ public static void Run(Action<SpectreAnalysisProgressReporter, IProgress<Analysi
3749
{
3850
reporter = new SpectreAnalysisProgressReporter();
3951
reporter.Attach(ctx);
40-
var progress = new Progress<AnalysisProgress>(reporter.Report);
52+
var progress = new SynchronousProgress(reporter);
4153
action(reporter, progress);
4254
reporter.FinalizeSession();
4355
});

0 commit comments

Comments
 (0)