Dart Code Metrics is a static code analysis tool that allows you to collect code metrics and provide additional rules for the analyzer. The tool helps developers monitor the quality of code and improve it. In this article, we'd like to share the capabilities of Dart Code Metrics with the community. This tool helped us at Wrike, and we hope it'll help you, too.
It can be launched via the command line, which is connected as a plugin to the Dart Analysis Server, or as a library. Launching via the command line allows you to easily integrate the tool into the CI/CD process, and you can get results in Сonsole, HTML, JSON, CodeClimate, or GitHub. Connecting the tool as a plugin to the Analysis Server allows you to receive real-time feedback directly from the IDE.
Why did we develop a tool like this? Wrike already has about 2.5 million lines of code written in Dart. A code base like this comes with the price of maintenance - so how do you know when it's time to refactor your code, and what do you start with?
Dart SDK is distributed with an Analyzer, which has a built-in linter with a set of rules. In the official pub, you can also find recommended rulesets: pedantic, effective_dart, etc. This helps us avoid making mistakes and maintain the codebase as suggested by the author of the language. However, we didn't have enough analytical data on the code, and that started it all.
Metrics
After researching materials on evaluating and designing program code, we realized the first step was to implement a collection of software metrics from the code.
Now the analyzer collects the following metrics:
- Cyclomatic complexity
- Lines of executable code
- Lines of code
- Number of parameters
- Number of methods
- Maximum nesting
- Weight of class
We set a base threshold value for each metric, after which code refactoring is recommended. At the same time, threshold values can be easily predefined and passed to the tool when either called from the console, or during the plugin configuration for the analyzer via analysis_options.yaml. You can read more about this here.
In the next step the tool finds anti-patterns in the codebase, relying on data from several metrics. Only two anti-patterns are currently implemented - long-method and long-parameter-list - but we'll expand this list in the future.
While metrics like number of parameters or number of methods are easy enough to understand, what about cyclomatic complexity?
The cyclomatic complexity of a piece of code is the number of linearly independent routes in the code. For example, if the source code doesn't contain any branch points or loops, then its complexity is one, since there's only one route through the code. If the code has a single if-statement containing one simple condition, we have two routes through the code: the if-statement is true or false.
Many independent routes in the function body impact the code's readability and maintenance. So it's better to split the function in such cases.
The tool provides various report formats to visualize the metrics. We'll get back to this later in the "Reports" section.
Linting
Initially, the tool only collected metrics, but then we added linting.
Other ecosystems have useful rules like unused arguments check, class member ordering check, etc. They're not available in the built-in Dart SDK linter, so we made our own linter.
Why did we decide to add linting to a separate package instead of making PR in the built-in analyzer? Because we decided to implement extended configuration capabilities. For example, we can configure the exact ordered list of class members that should be checked.
The current list of rules:
General rules
- avoid-unused-parameters
- binary-expression-operand-order
- double-literal-format
- member-ordering
- member-ordering-extended
- newline-before-return
- no-boolean-literal-compare
- no-empty-block
- no-equal-arguments
- no-equal-then-else
- no-magic-number
- no-object-declaration
- prefer-conditional-expressions
- prefer-trailing-comma
For Intl library
- prefer-intl-name
- provide-correct-intl-args
For Dart Angular
- avoid-preserve-whitespace-false
- component-annotation-arguments-ordering
- prefer-on-push-cd-strategy
You can find the most up-to-date list in the documentation.
Stylistic rules aren't the only important things to consider; we also want to highlight potential errors like no-equal-then-else, no-equal-arguments, and more.
Our rules were partially based on issues that we'd encounter during our reviews. Ideally, we want the issues to be covered automatically so we can focus on the work that matters. Another part of our rules emerged during the process of studying other tools' rules. (Shoutout to PVS-Studio, TSLint, and ESLint for inspiration!)
Let's take a closer look at some of those rules.
Avoid unused parameters. Checks for unused parameters for functions or methods. An unused parameter can indicate that it's no longer needed during refactoring, or that a variable is somewhere in the body of a function (or method) instead of that parameter. In the first case the rule helps remove unused code; the second indicates a possible error.
Here's a simple example:
String method(String value) => "";
The value parameter isn't used here, and the analyzer will display the message "Parameter is unused."
Since Dart allows you to inherit from any class and requires that the parameter be included in the descendants, it can be renamed to _ in the base class, and the analyzer will skip it.
For example:
String method(String _) => "";
Prefer trailing comma. Checks for the trailing comma for arguments, parameters, enumerations, and collections, provided they span multiple lines.
For example:
void firstFunction (String firstArgument, String secondArgument, String thirdArgument) {
...
}
For a function like this, the rule will suggest adding a comma at the end so that once formatted it turns into:
void firstFunction(
String firstArgument,
String secondArgument,
String thirdArgument,
) {
...
}
If the parameters were initially placed on one line, the analyzer won't consider it an error:
void secondFunction(String arg1, String arg2, String arg3) {
...
}
You can also specify a break-on parameter for the rule, which enables additional checking for the specified number of elements. For example, without break-on, the example above is considered correct. But if you configure the rule for break-on: 2
, the analyzer will display an error for this function and suggest adding a comma.
No equal arguments. Checks whether the same argument is passed more than once when instantiating a class or calling a method/function.
Suppose there's a certain user class and a separate function that creates this user:
class User {
final String firstName;
final String lastName;
const User(this.firstName, this.lastName);
}
User createUser(String lastName) {
String firstName = getFirstName();
return User(
firstName,
firstName,
);
}
Both User class fields are strings, and it's easy to miss that the same variable is passed when you create it. In such cases, the rule will indicate the variable firstName is passed more than once, which may be an error.
Member ordering extended. The rule checks the class member order. The rule received an extended postfix since we already had a member ordering rule, but it wasn't as flexible.
The rule accepts an ordering configuration with quite a flexible template. It allows you to specify not just the type of a class member (field, method, constructor), but also keywords such as late, const, final, or, for example, nullable.
The configuration can look like this:
- public-late-final-fields
- private-late-final-fields
- public-nullable-fields
- private-nullable-fields
- named-constructors
- factory-constructors
- getters
- setters
- public-static-methods
- private-static-methods
- protected-methods
- etc.
Or be simplified to:
- fields
- methods
- setters
- getters (or, if there's no need to separate, just getters-setters)
- constructors
Read more about the rule's capabilities in the documentation.
Additionally, the rule may require alphabetical sorting. To do this, you need to pass alphabetize: true
to its configuration.
Reports
To visualize all the collected metrics and linting results, the tool conveniently provides reports in one of the following formats:
- Console
- HTML
- JSON
- CodeClimate
- GitHub
Console format is the default, and can be transferred to the console utility via the --reporter flag.
For example:
$ dart pub run dart_code_metrics:metrics lib
# or for a Flutter package
$ flutter pub run dart_code_metrics:metrics lib
When executing the command on the Dart Code Metrics codebase, we'll receive the following report in the console:
If you want to choose a different type reporter (e.g., HTML), you need to run the following command:
$ dart pub run dart_code_metrics:metrics lib --reporter=html
# or for a Flutter package
$ flutter pub run dart_code_metrics:metrics lib --reporter=html
The report will be generated in the metrics folder. The resulting folder can also be passed using the --output-directory or -o flag:
In the report, you can view each file separately:
To view detailed information on metrics, you need to hover over the icons to the left of the code:
If you're using GitHub Workflows and want to get a report immediately in the created PRs, you need to add a new step to the pipeline:
jobs:
your_job_name:
...
steps:
...
- name: Run Code Metrics
run: flutter pub run dart_code_metrics:metrics --reporter=github lib
This will allow you to get reports in this format:
Detailed information about all types of metrics and configuration methods can be found in the documentation.
Installing
If you want to try the package for yourself, here's a short guide on connecting it as a plug-in for Dart analyzer.
Step 1: install the package as a dev dependency.
$ dart pub add --dev dart_code_metrics
# or for a Flutter package
$ flutter pub add --dev dart_code_metrics
Or, add the package manually to pubspec.yaml.
Important: If your package hasn't been migrated to null safety yet, use version 2.5.0.
dev_dependencies:
dart_code_metrics: ^3.3.3
Run the command to install dependencies.
$ dart pub get
# or for a Flutter package
$ flutter pub get
Step 2: Add configuration to analysis_options.yaml.
This configuration specifies the long-method and long-parameter-list anti-patterns, metrics with their threshold values, and rules. The entire list of available metrics can be found here, and the list of rules here.
Step 3: Reload the IDE for the analyzer to detect the plugin.
If you want to use the package as a CLI, check out the documentation here. To use it as a Library, take a look at the documentation here.
Conclusion
Given the popularity of Flutter, we're considering what metrics and rules could help developers with this framework. We're open to suggestions; if you have ideas for rules or metrics that could be useful to Dart or Flutter developers, feel free to write to us or report an issue via GitHub. We'll be happy to make developers' lives better.
Our plans for the future can always be tracked in the public project repository in the issues list or in projects, where you can also leave feedback on the tool.
Feel free to join our community on Telegram!
Top comments (0)