Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit accdb04

Browse files
authored
feat: introduce Halsted Volume metric (#434)
* refactor: remove unused metric documentation filed * refactor: improve maintainability-index calculation * refactor: improve HalsteadVolumeAstVisitor * refactor: remove custom implementation of `sum` * feature: introduce Halstead Volume metric * refactor: improve maintainability-index calculation * update changelog
1 parent 11b4ced commit accdb04

24 files changed

+280
-133
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"cSpell.words": [
55
"Backport",
66
"Gitlab",
7+
"Halstead",
78
"McCabe's",
89
"SLOC",
910
"ansicolor",

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
* Add support for global rules-exclude
6+
* Add `Halstead Volume` metric.
67

78
## 4.2.1
89

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Usage: metrics [arguments...] <directories>
150150
151151
152152
--cyclomatic-complexity=<20> Cyclomatic Complexity threshold
153+
--halstead-volume=<150> Halstead Volume threshold
153154
--lines-of-code=<100> Lines of Code threshold
154155
--maximum-nesting-level=<5> Maximum Nesting Level threshold
155156
--number-of-methods=<10> Number of Methods threshold

lib/src/analyzers/lint_analyzer/lint_analyzer.dart

Lines changed: 42 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:math';
33

44
import 'package:analyzer/dart/analysis/results.dart';
55
import 'package:analyzer/dart/ast/ast.dart';
6+
import 'package:collection/collection.dart';
67
import 'package:path/path.dart';
78

89
import '../../config_builder/config_builder.dart';
@@ -14,10 +15,8 @@ import '../../utils/file_utils.dart';
1415
import '../../utils/node_utils.dart';
1516
import 'lint_analysis_config.dart';
1617
import 'lint_config.dart';
17-
import 'metrics/halstead_volume_ast_visitor.dart';
18-
import 'metrics/metric_utils.dart';
1918
import 'metrics/metrics_list/cyclomatic_complexity/cyclomatic_complexity_metric.dart';
20-
import 'metrics/metrics_list/source_lines_of_code/source_code_visitor.dart';
19+
import 'metrics/metrics_list/halstead_volume/halstead_volume_metric.dart';
2120
import 'metrics/metrics_list/source_lines_of_code/source_lines_of_code_metric.dart';
2221
import 'metrics/models/metric_documentation.dart';
2322
import 'metrics/models/metric_value.dart';
@@ -32,7 +31,6 @@ import 'models/scoped_class_declaration.dart';
3231
import 'models/scoped_function_declaration.dart';
3332
import 'models/suppression.dart';
3433
import 'reporters/reporter_factory.dart';
35-
import 'reporters/utility_selector.dart';
3634

3735
class LintAnalyzer {
3836
const LintAnalyzer();
@@ -298,104 +296,48 @@ class LintAnalyzer {
298296
final functionRecords = <ScopedFunctionDeclaration, Report>{};
299297

300298
for (final function in visitor.functions) {
301-
final cyclomatic = config.methodsMetrics
302-
.firstWhere(
303-
(metric) => metric.id == CyclomaticComplexityMetric.metricId,
304-
)
305-
.compute(
299+
final metrics = [
300+
for (final metric in config.methodsMetrics)
301+
if (metric.supports(
306302
function.declaration,
307303
visitor.classes,
308304
visitor.functions,
309305
source,
310-
);
311-
312-
final sourceLinesOfCodeVisitor = SourceCodeVisitor(source.lineInfo);
313-
314-
function.declaration.visitChildren(sourceLinesOfCodeVisitor);
306+
))
307+
metric.compute(
308+
function.declaration,
309+
visitor.classes,
310+
visitor.functions,
311+
source,
312+
),
313+
];
315314

316-
final sourceLinesOfCode = MetricValue<int>(
317-
metricsId: SourceLinesOfCodeMetric.metricId,
318-
documentation: const MetricDocumentation(
319-
name: '',
320-
shortName: '',
321-
brief: '',
322-
measuredType: EntityType.methodEntity,
323-
recomendedThreshold: 0,
324-
examples: [],
325-
),
326-
value: sourceLinesOfCodeVisitor.linesWithCode.length,
327-
level: valueLevel(
328-
sourceLinesOfCodeVisitor.linesWithCode.length,
329-
readNullableThreshold<int>(
330-
config.metricsConfig,
331-
SourceLinesOfCodeMetric.metricId,
332-
),
333-
),
334-
comment: '',
315+
final cyclomatic = metrics.firstWhereOrNull(
316+
(value) => value.metricsId == CyclomaticComplexityMetric.metricId,
335317
);
336318

337-
final halsteadVolumeAstVisitor = HalsteadVolumeAstVisitor();
338-
339-
function.declaration.visitChildren(halsteadVolumeAstVisitor);
340-
341-
// Total number of occurrences of operators.
342-
final totalNumberOfOccurrencesOfOperators =
343-
sum(halsteadVolumeAstVisitor.operators.values);
344-
345-
// Total number of occurrences of operands
346-
final totalNumberOfOccurrencesOfOperands =
347-
sum(halsteadVolumeAstVisitor.operands.values);
348-
349-
// Number of distinct operators.
350-
final numberOfDistinctOperators =
351-
halsteadVolumeAstVisitor.operators.keys.length;
352-
353-
// Number of distinct operands.
354-
final numberOfDistinctOperands =
355-
halsteadVolumeAstVisitor.operands.keys.length;
356-
357-
// Halstead Program Length – The total number of operator occurrences and the total number of operand occurrences.
358-
final halsteadProgramLength = totalNumberOfOccurrencesOfOperators +
359-
totalNumberOfOccurrencesOfOperands;
360-
361-
// Halstead Vocabulary – The total number of unique operator and unique operand occurrences.
362-
final halsteadVocabulary =
363-
numberOfDistinctOperators + numberOfDistinctOperands;
364-
365-
// Program Volume – Proportional to program size, represents the size, in bits, of space necessary for storing the program. This parameter is dependent on specific algorithm implementation.
366-
final halsteadVolume =
367-
halsteadProgramLength * log2(max(1, halsteadVocabulary));
319+
final halsteadVolume = metrics.firstWhereOrNull(
320+
(value) => value.metricsId == HalsteadVolumeMetric.metricId,
321+
);
368322

369-
final maintainabilityIndex = max(
370-
0,
371-
(171 -
372-
5.2 * log(max(1, halsteadVolume)) -
373-
cyclomatic.value * 0.23 -
374-
16.2 * log(max(1, sourceLinesOfCode.value))) *
375-
100 /
376-
171,
377-
).toDouble();
323+
final sourceLinesOfCode = metrics.firstWhereOrNull(
324+
(value) => value.metricsId == SourceLinesOfCodeMetric.metricId,
325+
);
378326

379-
final report = Report(
380-
location: nodeLocation(
381-
node: function.declaration,
382-
source: source,
383-
),
384-
declaration: function.declaration,
385-
metrics: [
386-
for (final metric in config.methodsMetrics)
387-
if (metric.supports(
388-
function.declaration,
389-
visitor.classes,
390-
visitor.functions,
391-
source,
392-
))
393-
metric.compute(
394-
function.declaration,
395-
visitor.classes,
396-
visitor.functions,
397-
source,
398-
),
327+
if (cyclomatic != null &&
328+
halsteadVolume != null &&
329+
sourceLinesOfCode != null) {
330+
final maintainabilityIndex = max(
331+
0,
332+
(171 -
333+
5.2 * log(max(1, halsteadVolume.value)) -
334+
cyclomatic.value * 0.23 -
335+
16.2 * log(max(1, sourceLinesOfCode.value))) *
336+
100 /
337+
171,
338+
).toDouble();
339+
340+
metrics.add(
399341
MetricValue<double>(
400342
metricsId: 'maintainability-index',
401343
documentation: const MetricDocumentation(
@@ -404,18 +346,21 @@ class LintAnalyzer {
404346
brief: '',
405347
measuredType: EntityType.classEntity,
406348
recomendedThreshold: 0,
407-
examples: [],
408349
),
409350
value: maintainabilityIndex,
410351
level: _maintainabilityIndexViolationLevel(
411352
maintainabilityIndex,
412353
),
413354
comment: '',
414355
),
415-
],
416-
);
356+
);
357+
}
417358

418-
functionRecords[function] = report;
359+
functionRecords[function] = Report(
360+
location: nodeLocation(node: function.declaration, source: source),
361+
declaration: function.declaration,
362+
metrics: metrics,
363+
);
419364
}
420365

421366
return functionRecords;

lib/src/analyzers/lint_analyzer/metrics/metrics_factory.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import '../models/entity_type.dart';
22
import 'metrics_list/cyclomatic_complexity/cyclomatic_complexity_metric.dart';
3+
import 'metrics_list/halstead_volume/halstead_volume_metric.dart';
34
import 'metrics_list/lines_of_code_metric.dart';
45
import 'metrics_list/maximum_nesting_level/maximum_nesting_level_metric.dart';
56
import 'metrics_list/number_of_methods_metric.dart';
@@ -11,6 +12,8 @@ import 'models/metric.dart';
1112
final _implementedMetrics = <String, Metric Function(Map<String, Object>)>{
1213
CyclomaticComplexityMetric.metricId: (config) =>
1314
CyclomaticComplexityMetric(config: config),
15+
HalsteadVolumeMetric.metricId: (config) =>
16+
HalsteadVolumeMetric(config: config),
1417
LinesOfCodeMetric.metricId: (config) => LinesOfCodeMetric(config: config),
1518
MaximumNestingLevelMetric.metricId: (config) =>
1619
MaximumNestingLevelMetric(config: config),

lib/src/analyzers/lint_analyzer/metrics/metrics_list/cyclomatic_complexity/cyclomatic_complexity_metric.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const _documentation = MetricDocumentation(
2121
brief: 'The number of linearly-independent paths through a code block',
2222
measuredType: EntityType.methodEntity,
2323
recomendedThreshold: 20,
24-
examples: [],
2524
);
2625

2726
/// Cyclomatic Complexity (CYCLO)

lib/src/analyzers/lint_analyzer/metrics/halstead_volume_ast_visitor.dart renamed to lib/src/analyzers/lint_analyzer/metrics/metrics_list/halstead_volume/halstead_volume_ast_visitor.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
import 'package:analyzer/dart/ast/ast.dart';
22
import 'package:analyzer/dart/ast/token.dart';
33
import 'package:analyzer/dart/ast/visitor.dart';
4+
import 'package:collection/collection.dart';
45

56
class HalsteadVolumeAstVisitor extends RecursiveAstVisitor<void> {
67
final _operators = <String, int>{};
78
final _operands = <String, int>{};
89

9-
Map<String, int> get operators => _operators;
10-
Map<String, int> get operands => _operands;
10+
/// the number of operators
11+
int get operators => _operators.values.sum;
12+
13+
/// the number of unique operators
14+
int get uniqueOperators => _operators.keys.length;
15+
16+
/// the number of operands
17+
int get operands => _operands.values.sum;
18+
19+
/// the number of unique operands
20+
int get uniqueOperands => _operands.keys.length;
1121

1222
@override
1323
void visitBlockFunctionBody(BlockFunctionBody node) {
1424
_analyzeFunctionBodyData(
1525
node.block.leftBracket.next,
1626
node.block.rightBracket,
1727
);
28+
1829
super.visitBlockFunctionBody(node);
1930
}
2031

@@ -24,7 +35,8 @@ class HalsteadVolumeAstVisitor extends RecursiveAstVisitor<void> {
2435
node.expression.beginToken.previous,
2536
node.expression.endToken.next,
2637
);
27-
node.visitChildren(this);
38+
39+
super.visitExpressionFunctionBody(node);
2840
}
2941

3042
void _analyzeFunctionBodyData(Token? firstToken, Token? lastToken) {
@@ -33,9 +45,11 @@ class HalsteadVolumeAstVisitor extends RecursiveAstVisitor<void> {
3345
if (token.isOperator) {
3446
_operators[token.type.name] = (_operators[token.type.name] ?? 0) + 1;
3547
}
48+
3649
if (token.isIdentifier) {
3750
_operands[token.lexeme] = (_operands[token.lexeme] ?? 0) + 1;
3851
}
52+
3953
token = token.next;
4054
}
4155
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'dart:math';
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
5+
import '../../../models/entity_type.dart';
6+
import '../../../models/internal_resolved_unit_result.dart';
7+
import '../../../models/scoped_class_declaration.dart';
8+
import '../../../models/scoped_function_declaration.dart';
9+
import '../../metric_utils.dart';
10+
import '../../models/function_metric.dart';
11+
import '../../models/metric_computation_result.dart';
12+
import '../../models/metric_documentation.dart';
13+
import 'halstead_volume_ast_visitor.dart';
14+
15+
const _documentation = MetricDocumentation(
16+
name: 'Halstead Volume',
17+
shortName: 'HALVOL',
18+
brief:
19+
'The Halstead Volume is based on the Length and the Vocabulary. You can view this as the "bulk" of the code – how much information does the reader of the code have to absorb to understand its meaning. The biggest influence on the Volume metric is the Halstead length which causes a linear increase in the Volume i.e doubling the Length will double the Volume. In the case of the Vocabulary the increase is logarithmic.',
20+
measuredType: EntityType.methodEntity,
21+
recomendedThreshold: 150,
22+
);
23+
24+
/// Halstead Volume (HALVOL)
25+
///
26+
/// The Halstead Volume is based on the Length and the Vocabulary. You can view
27+
/// this as the ‘bulk’ of the code – how much information does the reader of the
28+
/// code have to absorb to understand its meaning. The biggest influence on the
29+
/// Volume metric is the Halstead length which causes a linear increase in the
30+
/// Volume i.e doubling the Length will double the Volume. In the case of the
31+
/// Vocabulary the increase is logarithmic.
32+
class HalsteadVolumeMetric extends FunctionMetric<double> {
33+
static const String metricId = 'halstead-volume';
34+
35+
HalsteadVolumeMetric({Map<String, Object> config = const {}})
36+
: super(
37+
id: metricId,
38+
documentation: _documentation,
39+
threshold: readNullableThreshold<double>(config, metricId),
40+
levelComputer: valueLevel,
41+
);
42+
43+
@override
44+
MetricComputationResult<double> computeImplementation(
45+
Declaration node,
46+
Iterable<ScopedClassDeclaration> classDeclarations,
47+
Iterable<ScopedFunctionDeclaration> functionDeclarations,
48+
InternalResolvedUnitResult source,
49+
) {
50+
final visitor = HalsteadVolumeAstVisitor();
51+
node.visitChildren(visitor);
52+
53+
// LTH (length) - the sum of the number of operators and the number of operands
54+
final lth = visitor.operators + visitor.operands;
55+
56+
// VOC (vocabulary) – the the number of unique operators and the number of unique operands
57+
final voc = visitor.uniqueOperators + visitor.uniqueOperands;
58+
59+
// VOL (volume) – based on the length and the vocabulary
60+
final vol = voc != 0 ? lth * _log2(voc) : 0.0;
61+
62+
return MetricComputationResult<double>(value: vol);
63+
}
64+
65+
@override
66+
String commentMessage(String nodeType, double value, double? threshold) {
67+
final exceeds = threshold != null && value > threshold
68+
? ', which exceeds the maximum of $threshold allowed'
69+
: '';
70+
71+
return 'This $nodeType has a halstead volume of $value$exceeds.';
72+
}
73+
74+
double _log2(int a) => log(max(1, a)) / ln2;
75+
}

lib/src/analyzers/lint_analyzer/metrics/metrics_list/lines_of_code_metric.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const _documentation = MetricDocumentation(
1616
'The number of physical lines of code of a method, including blank lines and comments',
1717
measuredType: EntityType.methodEntity,
1818
recomendedThreshold: 100,
19-
examples: [],
2019
);
2120

2221
/// Lines of Code (LOC)

lib/src/analyzers/lint_analyzer/metrics/metrics_list/maximum_nesting_level/maximum_nesting_level_metric.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const _documentation = MetricDocumentation(
1919
brief: 'The maximum nesting level of control structures within a method',
2020
measuredType: EntityType.methodEntity,
2121
recomendedThreshold: 5,
22-
examples: [],
2322
);
2423

2524
/// Maximum Nesting Level (MAXNESTING)

0 commit comments

Comments
 (0)