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

Commit 6722e3f

Browse files
authored
feat: add technical-debt metric (#575)
* chore: update changelog * feat: implement tech debt metric * chore: fix test * chore: add tests
1 parent 411c51b commit 6722e3f

28 files changed

+527
-20
lines changed

CHANGELOG.md

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

55
* feat: add alphabetical sorting by type for `member-ordering-extended` rule.
66
* feat: add support mixins, extensions and enums for `prefer-match-file-name` rule.
7+
* feat: add `technical-debt` metric.
78
* fix: prefer conditional expressions rule breaks code with increment / decrement operators.
89
* fix: improve file metrics.
910
* feat: add metric value unit type.

analysis_options.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ dart_code_metrics:
2121
maximum-nesting: 5
2222
number-of-parameters: 5
2323
source-lines-of-code: 50
24+
technical-debt:
25+
threshold: 1
26+
todo-cost: 161
27+
ignore-cost: 320
28+
ignore-for-file-cost: 396
29+
as-dynamic-cost: 322
30+
deprecated-annotations: 37
31+
file-nullsafety-migration: 41
32+
unit-type: "USD"
2433
metrics-exclude:
2534
- test/src/analyzer_plugin/**
2635
- test/src/analyzers/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ T? readConfigValue<T extends Object>(
3535
return int.tryParse(configValue) as T?;
3636
} else if (configValue != null && T == double) {
3737
return double.tryParse(configValue) as T?;
38+
} else if (configValue != null && T == String) {
39+
return configValue as T?;
3840
}
3941

4042
return null;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'metrics_list/maximum_nesting_level/maximum_nesting_level_metric.dart';
77
import 'metrics_list/number_of_methods_metric.dart';
88
import 'metrics_list/number_of_parameters_metric.dart';
99
import 'metrics_list/source_lines_of_code/source_lines_of_code_metric.dart';
10+
import 'metrics_list/technical_debt/technical_debt_metric.dart';
1011
import 'metrics_list/weight_of_class_metric.dart';
1112
import 'models/metric.dart';
1213

@@ -29,6 +30,8 @@ final _implementedMetrics = <String, Metric Function(Map<String, Object>)>{
2930
// Depend on CyclomaticComplexityMetric, HalsteadVolumeMetric and SourceLinesOfCodeMetric metrics
3031
MaintainabilityIndexMetric.metricId: (config) =>
3132
MaintainabilityIndexMetric(config: config),
33+
// Depend on all metrics
34+
TechnicalDebtMetric.metricId: (config) => TechnicalDebtMetric(config: config),
3235
};
3336

3437
Iterable<Metric> getMetrics({
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/syntactic_entity.dart';
3+
import 'package:analyzer/dart/ast/token.dart';
4+
import 'package:analyzer/dart/ast/visitor.dart';
5+
import 'package:analyzer/dart/element/type.dart';
6+
7+
import '../../../../../utils/node_utils.dart';
8+
import '../../../models/context_message.dart';
9+
import '../../../models/entity_type.dart';
10+
import '../../../models/internal_resolved_unit_result.dart';
11+
import '../../../models/scoped_class_declaration.dart';
12+
import '../../../models/scoped_function_declaration.dart';
13+
import '../../metric_utils.dart';
14+
import '../../models/file_metric.dart';
15+
import '../../models/metric_computation_result.dart';
16+
import '../../models/metric_documentation.dart';
17+
import '../../models/metric_value.dart';
18+
19+
part 'visitor.dart';
20+
21+
const _documentation = MetricDocumentation(
22+
name: 'Technical Debt',
23+
shortName: 'TECHDEBT',
24+
measuredType: EntityType.fileEntity,
25+
recomendedThreshold: 0,
26+
);
27+
28+
/// Technical Debt (TECHDEBT)
29+
///
30+
/// Technical debt is a concept in software development that reflects the
31+
/// implied cost of additional rework caused by choosing an easy solution now
32+
/// instead of using a better approach that would take longer.
33+
class TechnicalDebtMetric extends FileMetric<double> {
34+
static const String metricId = 'technical-debt';
35+
36+
final double _todoCommentCost;
37+
final double _ignoreCommentCost;
38+
final double _ignoreForFileCommentCost;
39+
final double _asDynamicExpressionCost;
40+
final double _deprecatedAnnotationsCost;
41+
final double _fileNullSafetyMigrationCost;
42+
final String? _unitType;
43+
44+
TechnicalDebtMetric({Map<String, Object> config = const {}})
45+
: _todoCommentCost =
46+
readConfigValue<double>(config, metricId, 'todo-cost') ?? 0.0,
47+
_ignoreCommentCost =
48+
readConfigValue<double>(config, metricId, 'ignore-cost') ?? 0.0,
49+
_ignoreForFileCommentCost =
50+
readConfigValue<double>(config, metricId, 'ignore-for-file-cost') ??
51+
0.0,
52+
_asDynamicExpressionCost =
53+
readConfigValue<double>(config, metricId, 'as-dynamic-cost') ?? 0.0,
54+
_deprecatedAnnotationsCost = readConfigValue<double>(
55+
config,
56+
metricId,
57+
'deprecated-annotations',
58+
) ??
59+
0.0,
60+
_fileNullSafetyMigrationCost = readConfigValue<double>(
61+
config,
62+
metricId,
63+
'file-nullsafety-migration',
64+
) ??
65+
0.0,
66+
_unitType = readConfigValue<String>(config, metricId, 'unit-type'),
67+
super(
68+
id: metricId,
69+
documentation: _documentation,
70+
threshold: readNullableThreshold<double>(config, metricId),
71+
levelComputer: valueLevel,
72+
);
73+
74+
@override
75+
MetricComputationResult<double> computeImplementation(
76+
AstNode node,
77+
Iterable<ScopedClassDeclaration> classDeclarations,
78+
Iterable<ScopedFunctionDeclaration> functionDeclarations,
79+
InternalResolvedUnitResult source,
80+
Iterable<MetricValue<num>> otherMetricsValues,
81+
) {
82+
var debt = 0.0;
83+
84+
final visitor = _Visitor()..visitComments(node);
85+
node.visitChildren(visitor);
86+
87+
debt += visitor.todos.length * _todoCommentCost;
88+
debt += visitor.ignore.length * _ignoreCommentCost;
89+
debt += visitor.ignoreForFile.length * _ignoreForFileCommentCost;
90+
debt += visitor.asDynamicExpressions.length * _asDynamicExpressionCost;
91+
debt += visitor.deprecatedAnnotations.length * _deprecatedAnnotationsCost;
92+
debt += visitor.nonNullSafetyLanguageComment.length *
93+
_fileNullSafetyMigrationCost;
94+
95+
return MetricComputationResult(
96+
value: debt,
97+
context: [
98+
if (_todoCommentCost > 0.0)
99+
..._context(visitor.todos, source, _todoCommentCost),
100+
if (_ignoreCommentCost > 0.0)
101+
..._context(visitor.ignore, source, _ignoreCommentCost),
102+
if (_ignoreForFileCommentCost > 0.0)
103+
..._context(visitor.ignoreForFile, source, _ignoreForFileCommentCost),
104+
if (_asDynamicExpressionCost > 0.0)
105+
..._context(
106+
visitor.asDynamicExpressions,
107+
source,
108+
_asDynamicExpressionCost,
109+
),
110+
if (_deprecatedAnnotationsCost > 0.0)
111+
..._context(
112+
visitor.deprecatedAnnotations,
113+
source,
114+
_deprecatedAnnotationsCost,
115+
),
116+
if (_fileNullSafetyMigrationCost > 0.0)
117+
..._context(
118+
visitor.nonNullSafetyLanguageComment,
119+
source,
120+
_fileNullSafetyMigrationCost,
121+
),
122+
],
123+
);
124+
}
125+
126+
@override
127+
String commentMessage(String nodeType, double value, double? threshold) {
128+
final exceeds = threshold != null && value > threshold
129+
? ', exceeds the maximum of $threshold allowed'
130+
: '';
131+
final debt = '$value swe ${value == 1 ? 'hour' : 'hours'} of debt';
132+
133+
return 'This $nodeType has $debt$exceeds.';
134+
}
135+
136+
@override
137+
String? unitType(double value) => _unitType;
138+
139+
Iterable<ContextMessage> _context(
140+
Iterable<SyntacticEntity> complexityEntities,
141+
InternalResolvedUnitResult source,
142+
double cost,
143+
) =>
144+
complexityEntities
145+
.map((entity) => ContextMessage(
146+
message: '+$cost ${_unitType ?? ''}'.trim(),
147+
location: nodeLocation(node: entity, source: source),
148+
))
149+
.toList()
150+
..sort((a, b) => a.location.start.compareTo(b.location.start));
151+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
part of 'technical_debt_metric.dart';
2+
3+
final _todoRegExp = RegExp(r'//+(.* )?TODO\b');
4+
final _hacksRegExp = RegExp(r'//+(.* )?HACK\b');
5+
final _fixmeRegExp = RegExp(r'//+(.* )?FIXME\b');
6+
final _undoneRegExp = RegExp(r'//+(.* )?UNDONE\b');
7+
8+
final _ignorePattern = RegExp('// *ignore:');
9+
10+
final _ignoreForFilePattern = RegExp('// *ignore_for_file:');
11+
12+
class _Visitor extends RecursiveAstVisitor<void> {
13+
final _todoComments = <Token>[];
14+
final _ignoreComments = <Token>[];
15+
final _ignoreForFileComments = <Token>[];
16+
final _nonNullSafetyLanguageComment = <Token>[];
17+
final _asDynamicExpressions = <AsExpression>[];
18+
final _deprecatedAnnotations = <Annotation>[];
19+
20+
Iterable<Token> get todos => _todoComments;
21+
Iterable<Token> get ignore => _ignoreComments;
22+
Iterable<Token> get ignoreForFile => _ignoreForFileComments;
23+
Iterable<Token> get nonNullSafetyLanguageComment =>
24+
_nonNullSafetyLanguageComment;
25+
Iterable<AsExpression> get asDynamicExpressions => _asDynamicExpressions;
26+
Iterable<Annotation> get deprecatedAnnotations => _deprecatedAnnotations;
27+
28+
void visitComments(AstNode node) {
29+
Token? token = node.beginToken;
30+
while (token != null) {
31+
Token? comment = token.precedingComments;
32+
while (comment != null) {
33+
final content = comment.lexeme;
34+
if (content.startsWith(_todoRegExp) ||
35+
content.startsWith(_hacksRegExp) ||
36+
content.startsWith(_fixmeRegExp) ||
37+
content.startsWith(_undoneRegExp)) {
38+
_todoComments.add(comment);
39+
} else if (content.startsWith(_ignorePattern)) {
40+
_ignoreComments.add(comment);
41+
} else if (content.startsWith(_ignoreForFilePattern)) {
42+
_ignoreForFileComments.add(comment);
43+
} else if (comment is LanguageVersionToken) {
44+
final nullSafetyFile =
45+
comment.major > 2 || (comment.major == 2 && comment.minor >= 12);
46+
if (!nullSafetyFile) {
47+
_nonNullSafetyLanguageComment.add(comment);
48+
}
49+
}
50+
51+
comment = comment.next;
52+
}
53+
54+
if (token == token.next) {
55+
break;
56+
}
57+
58+
token = token.next;
59+
}
60+
}
61+
62+
@override
63+
void visitAsExpression(AsExpression node) {
64+
super.visitAsExpression(node);
65+
66+
if (node.type.type is DynamicType) {
67+
_asDynamicExpressions.add(node);
68+
}
69+
}
70+
71+
@override
72+
void visitAnnotation(Annotation node) {
73+
super.visitAnnotation(node);
74+
75+
final elementAnnotation = node.elementAnnotation;
76+
if (elementAnnotation != null && elementAnnotation.isDeprecated) {
77+
_deprecatedAnnotations.add(node);
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)