diff --git a/bin/metrics.dart b/bin/metrics.dart index ccb0a958a2..c26eaf2b74 100644 --- a/bin/metrics.dart +++ b/bin/metrics.dart @@ -30,6 +30,7 @@ Future main(List args) async { int.tryParse(arguments[metrics.numberOfArgumentsKey] as String ?? ''), int.tryParse(arguments[metrics.numberOfMethodsKey] as String ?? ''), int.tryParse(arguments[metrics.maximumNestingKey] as String ?? ''), + double.tryParse(arguments[metrics.weightOfClassKey] as String ?? ''), arguments[reporterName] as String, arguments[verboseName] as bool, arguments[gitlabCompatibilityName] as bool, @@ -59,6 +60,7 @@ Future _runAnalysis( int numberOfArgumentsWarningLevel, int numberOfMethodsWarningLevel, int maximumNestingWarningLevel, + double weightOfClassWarningLevel, String reporterType, bool verbose, bool gitlab, @@ -99,6 +101,8 @@ Future _runAnalysis( options.metricsConfig.numberOfMethodsWarningLevel, maximumNestingWarningLevel: maximumNestingWarningLevel ?? options.metricsConfig.maximumNestingWarningLevel, + weightOfClassWarningLevel: weightOfClassWarningLevel ?? + options.metricsConfig.weightOfClassWarningLevel, ); Reporter reporter; diff --git a/doc/metrics/weight-of-class.md b/doc/metrics/weight-of-class.md new file mode 100644 index 0000000000..8e3f5df9ee --- /dev/null +++ b/doc/metrics/weight-of-class.md @@ -0,0 +1,7 @@ +# Weight Of Class + +Number of **functional** public methods divided by the total number of public methods. + +This metric tries to quantify whether the measured class' interface reveals more data than behaviour. Low values (less than 33%) indicate that the class reveals much more data than behaviour, which is a sign of poor encapsulation. + +Our definition of **functional method** excludes getters and setters. diff --git a/lib/src/config/analysis_options.dart b/lib/src/config/analysis_options.dart index d3228084d7..c359c64457 100644 --- a/lib/src/config/analysis_options.dart +++ b/lib/src/config/analysis_options.dart @@ -85,6 +85,8 @@ metrics.Config _readMetricsConfig(Map configMap) { .as(metrics.numberOfMethodsDefaultWarningLevel), maximumNestingWarningLevel: configMap[metrics.maximumNestingKey] .as(metrics.maximumNestingDefaultWarningLevel), + weightOfClassWarningLevel: configMap[metrics.weightOfClassKey] + .as(metrics.weightOfClassDefaultWarningLevel), ); } } diff --git a/lib/src/config/cli/arguments_parser.dart b/lib/src/config/cli/arguments_parser.dart index ad69919122..4a738120c1 100644 --- a/lib/src/config/cli/arguments_parser.dart +++ b/lib/src/config/cli/arguments_parser.dart @@ -64,10 +64,10 @@ void _appendReporterOption(ArgParser parser) { } @immutable -class _MetricOption { +class _MetricOption { final String name; final String help; - final int defaultValue; + final T defaultValue; const _MetricOption(this.name, this.help, this.defaultValue); } @@ -99,6 +99,11 @@ void _appendMetricsThresholdOptions(ArgParser parser) { 'Maximum nesting threshold', maximumNestingDefaultWarningLevel, ), + _MetricOption( + weightOfClassKey, + 'Weight Of Class threshold', + weightOfClassDefaultWarningLevel, + ), ]; for (final metric in metrics) { @@ -107,9 +112,16 @@ void _appendMetricsThresholdOptions(ArgParser parser) { help: metric.help, valueHelp: '${metric.defaultValue}', // ignore: avoid_types_on_closure_parameters - callback: (String i) { - if (i != null && int.tryParse(i) == null) { - print("'$i' invalid value for argument ${metric.name}"); + callback: (String value) { + var invalid = true; + if (metric.defaultValue is int) { + invalid = value != null && int.tryParse(value) == null; + } else if (metric.defaultValue is double) { + invalid = value != null && double.tryParse(value) == null; + } + + if (invalid) { + print("'$value' invalid value for argument ${metric.name}"); } }, ); diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart index 9ba605ddeb..e3337478a3 100644 --- a/lib/src/config/config.dart +++ b/lib/src/config/config.dart @@ -5,12 +5,14 @@ const linesOfExecutableCodeKey = 'lines-of-executable-code'; const numberOfArgumentsKey = 'number-of-arguments'; const numberOfMethodsKey = 'number-of-methods'; const maximumNestingKey = 'maximum-nesting'; +const weightOfClassKey = 'weight-of-class'; const cyclomaticComplexityDefaultWarningLevel = 20; const linesOfExecutableCodeDefaultWarningLevel = 50; const numberOfArgumentsDefaultWarningLevel = 4; const numberOfMethodsDefaultWarningLevel = 10; const maximumNestingDefaultWarningLevel = 5; +const weightOfClassDefaultWarningLevel = 0.33; /// Reporter config to use with various [Reporter]s @immutable @@ -20,6 +22,7 @@ class Config { final int numberOfArgumentsWarningLevel; final int numberOfMethodsWarningLevel; final int maximumNestingWarningLevel; + final double weightOfClassWarningLevel; const Config({ this.cyclomaticComplexityWarningLevel = @@ -29,5 +32,6 @@ class Config { this.numberOfArgumentsWarningLevel = numberOfArgumentsDefaultWarningLevel, this.numberOfMethodsWarningLevel = numberOfMethodsDefaultWarningLevel, this.maximumNestingWarningLevel = maximumNestingDefaultWarningLevel, + this.weightOfClassWarningLevel = weightOfClassDefaultWarningLevel, }); } diff --git a/lib/src/metrics_analyzer.dart b/lib/src/metrics_analyzer.dart index 14dc085ea1..4a43237287 100644 --- a/lib/src/metrics_analyzer.dart +++ b/lib/src/metrics_analyzer.dart @@ -78,6 +78,8 @@ class MetricsAnalyzer { .map((entity) => entity.path)) .toList(); + final woc = WeightOfClassMetric(); + for (final filePath in filePaths) { final normalized = p.normalize(p.absolute(filePath)); @@ -137,6 +139,8 @@ class MetricsAnalyzer { methodsCount: NumberOfMethodsMetric() .compute(classDeclaration, functions) .value, + weightOfClass: + woc.compute(classDeclaration, visitor.functions).value, ), ); } diff --git a/lib/src/models/component_record.dart b/lib/src/models/component_record.dart index d46a7b9208..080d090aed 100644 --- a/lib/src/models/component_record.dart +++ b/lib/src/models/component_record.dart @@ -6,9 +6,12 @@ class ComponentRecord { final int lastLine; final int methodsCount; + final double weightOfClass; - const ComponentRecord( - {@required this.firstLine, - @required this.lastLine, - @required this.methodsCount}); + const ComponentRecord({ + @required this.firstLine, + @required this.lastLine, + @required this.methodsCount, + @required this.weightOfClass, + }); } diff --git a/lib/src/models/component_report.dart b/lib/src/models/component_report.dart index e2df573bf5..cf65ddc2b4 100644 --- a/lib/src/models/component_report.dart +++ b/lib/src/models/component_report.dart @@ -4,6 +4,10 @@ import 'package:meta/meta.dart'; @immutable class ComponentReport { final MetricValue methodsCount; + final MetricValue weightOfClass; - const ComponentReport({@required this.methodsCount}); + const ComponentReport({ + @required this.methodsCount, + @required this.weightOfClass, + }); } diff --git a/lib/src/reporters/utility_selector.dart b/lib/src/reporters/utility_selector.dart index 46561a269f..f510daf376 100644 --- a/lib/src/reporters/utility_selector.dart +++ b/lib/src/reporters/utility_selector.dart @@ -90,6 +90,13 @@ class UtilitySelector { component.methodsCount, config.numberOfMethodsWarningLevel), comment: '', ), + weightOfClass: MetricValue( + metricsId: '', + value: component.weightOfClass, + level: _violationLevelPercentInvert( + component.weightOfClass, config.weightOfClassWarningLevel), + comment: '', + ), ); static FunctionReport functionReport(FunctionRecord function, Config config) { @@ -227,6 +234,25 @@ class UtilitySelector { rhs.maximumNestingLevelViolations, ); + static MetricValueLevel _violationLevelPercentInvert( + double value, + double warningLevel, + ) { + if (warningLevel == null) { + return MetricValueLevel.none; + } + + if (value < warningLevel * 0.5) { + return MetricValueLevel.alarm; + } else if (value < warningLevel) { + return MetricValueLevel.warning; + } else if (value < (warningLevel * 2)) { + return MetricValueLevel.noted; + } + + return MetricValueLevel.none; + } + static MetricValueLevel _maintainabilityIndexViolationLevel(double index) { if (index < 10) { return MetricValueLevel.alarm; diff --git a/test/stubs_builders.dart b/test/stubs_builders.dart index e5cccf4ec0..f9abfd38f1 100644 --- a/test/stubs_builders.dart +++ b/test/stubs_builders.dart @@ -8,11 +8,13 @@ ComponentRecord buildComponentRecordStub({ int firstLine = 0, int lastLine = 0, int methodsCount = 0, + double weightOfClass = 1, }) => ComponentRecord( firstLine: firstLine, lastLine: lastLine, methodsCount: methodsCount, + weightOfClass: weightOfClass, ); FunctionRecord buildFunctionRecordStub({ @@ -39,6 +41,8 @@ FunctionRecord buildFunctionRecordStub({ ComponentReport buildComponentReportStub({ int methodsCount = 0, MetricValueLevel methodsCountViolationLevel = MetricValueLevel.none, + double weightOfClass = 1, + MetricValueLevel weightOfClassViolationLevel = MetricValueLevel.none, }) => ComponentReport( methodsCount: MetricValue( @@ -47,6 +51,12 @@ ComponentReport buildComponentReportStub({ level: methodsCountViolationLevel, comment: '', ), + weightOfClass: MetricValue( + metricsId: '', + value: weightOfClass, + level: weightOfClassViolationLevel, + comment: '', + ), ); FunctionReport buildFunctionReportStub({