Перейти к основному содержимому

DCM: When the Built-In Analyzer Isn't Enough

· 8 мин. чтения

This is the text version of my presentation at Flutter Czechia Meetup, where we delved into how Dart Code Metrics elevates your development game, making your coding process more efficient.

Let's start with a simple question: "Why do we need an analyzer at all?"

One significant benefit is its ability to help us enforce code style within our team. Ask yourself, how often during code reviews do you encounter comments like these:

  • "A trailing comma is unnecessary when you have only one callback argument..."
  • "Please include a blank line before a return statement..."
  • "We typically avoid using the .call() method explicitly..."

If these situations occur frequently, especially when onboarding a new developer, then automation might be the solution. While this is most useful for team environments, it's essential to remember that even two developers can be considered a team (sometimes even one developer is a team).

Another area where an analyzer can be invaluable is in identifying potential errors. Have you ever received or left comments such as:

  • "Is this unused parameter intentional?"
  • "This variable will never be of that type..."
  • "This switch statement has the same code in another case; this could be a copy-paste error..."

A robust analyzer can detect such issues.

In general, the goal of this tool is to increase your efficiency in several ways:

  • You'll make fewer bugs because you won't overlook issues like unused parameters, duplicate if conditions, or incorrect type casting.
  • Your team won't have to debate code style – you can establish a single style and rely on the tool to ensure compliance.
  • During code reviews, you can focus on aspects that can't be automatically checked, such as architecture, readability, maintainability, and solving the actual business problem.
  • Onboarding new developers becomes faster and simpler, as they won't need to read a comprehensive code style guide (even if you have one). The analyzer will promptly flag any violations.

Now, you might be wondering, "Why do we need another analyzer, especially a paid one, when Dart already has a built-in one?"

Regarding Dart Code Metrics (DCM), it offers over 250 configurable rules. Some of them cater to commonly used libraries like provider, bloc, patrol, and others—features that the built-in analyzer can't provide.

The development cycle is much faster. While the built-in analyzer updates alongside the language, DCM releases updates approximately every month, consistently introducing new rules. For instance, in October, there were 14 new rules, 13 in September, 17 in August, and so on.

In essence, DCM aims to make you and your team even more efficient. If you're uncertain about the price-to-benefit ratio or need to convince stakeholders, refer to this article on the official blog.

...let's take a typical team of 5 people. Having introduced DCM, we can expect that, thanks to the saved time, the team will be able to perform useful work that will cost:

  • Skeptic: $24.000
  • Reality: $36.000

And the Teams version for 5 people costs $840.

I wholeheartedly endorse the calculations presented in the article and would even argue that the author might be underestimating the benefits DCM can bring to a team.

Let's now discuss the specific rules we've enabled in our configuration. Reading through all the rules doesn't make sense within this article (I'll give you a link to the full list at the end), so I'll touch on a few of them and provide examples.

When it comes to rules, I typically categorize them into several groups.

The first category, "code style lints," as I mentioned earlier, simplifies your life more than it enhances your safety. The exact style guide you follow doesn't matter as long as you have one.

For instance, in our team, we've decided that an explicit call method for callable objects slightly reduces code readability. Therefore, we've enabled a rule that checks if we can use a shorter form:

// BAD

return _getPaymentsForCheckIn
.call(
reservation: reservation,
session: _session,
allProducts: allProducts,
allServices: allServices,
)
.letAsync(bind);

// GOOD

return _getPaymentsForCheckIn(
reservation: reservation,
session: _session,
allProducts: allProducts,
allServices: allServices,
).letAsync(bind);

The next, more critical category involves rules that enforce "best practices." While the boundary may sometimes be blurry, these rules typically have more or less formal justifications for their inclusion.

For example, one of these "best practice" rules requires commenting on all analyzer ignores:

if (resource.hasParent) {
// ignore: avoid-recursive-calls, base case is handled when the resource has no parent.
addNode(filteredResources[resource.parentId]!, node);
}

child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 40),
// TODO(KB): Check whether Row is needed here
// ignore: avoid-single-child-column-or-row
child: widget.isExpanded ? Row(children: [Expanded(child: content)]) : content,
),

No matter how reliable your analyzer is, there will be moments when you disagree with a rule. If these cases are infrequent, it's better to locally ignore the rule while providing a rationale. This is crucial because sometimes the analyzer may lack the necessary context or even contain a bug (yeah, that happens as well, life hurts).

The next, even more critical category involves potential errors. These could be copy-paste errors, unused parameters, or obscure syntax or language behavior.

For instance, can you spot the error in the following code block?

// BAD

if (accessInfo != null) {
await _processAccessInfo(accessInfo, emit);
} else if (accessInfo != null) {
emit(const AuthState.pinRequired());
}

// GOOD

if (accessInfo != null) {
await _processAccessInfo(accessInfo, emit);
} else if (authenticator != null) {
emit(const AuthState.pinRequired());
}

Would you still spot this error if there were hundreds of other changes in the pull request? DCM does, and with the no-equal-conditions rule enabled, it warns you that the second branch doesn't make sense because it has the same condition.

Another example of a potential error is the unnecessary use of the late keyword:

// BAD

late final String x;

switch (someCondition) {
case Some.option1:
x = 'a';
break;
case Some.option2:
x = 'b';
break;
}

// GOOD

final String x;

switch (someCondition) {
case Some.option1:
x = 'a';
break;
case Some.option2:
x = 'b';
break;
}

I've even written an article explaining why this is undesirable. Fortunately, DCM can now detect it.

The last category, which I refer to as "template rules," doesn't necessarily make sense on its own. However, with custom parameters, it becomes quite powerful.

avoid-banned-imports:
severity: error
entries:
- paths: [".*/models/.+\\.dart"]
deny: [".*/(data|services|widgets|screens|ui|l10n)/.*"]
message: "Do not import data|services|widgets|screens|ui|l10n from models"

- paths: [".*/data/.+\\.dart"]
deny: [".*/(services|widgets|screens|ui|l10n)/.*"]
message: "Do not import services|widgets|screens|ui|l10n from data"

- paths: [".*/services/.+\\.dart"]
deny: [".*/(widgets|screens|ui|l10n)/.*"]
message: "Do not import widgets|screens|ui|l10n from services"

For instance, you can use the avoid-banned-imports rule to enforce layered architecture. With this setup, you can only allow importing dependencies from the "upper" layers, and the tool will immediately flag any attempts to import from the wrong layer.

Analyzer feedback

In another project, I used this rule to emulate modules. A folder/module could have an internal src folder, and other modules couldn't import anything from this src folder. This approach resembled the package conventional structure, where packages cannot import anything from the src folders of other packages.

If you'd like to review the configuration we use in all our projects, please check our package, mews_pedantic. You don't necessarily have to adopt this or any other package verbatim for your projects. Analyzer configuration is highly team-specific, particularly when it comes to code style or best practices rules, so what works for us might not be suitable for you.

However, I recommend going through all the rules and considering their relevance to your project. We follow the "enabled by default" approach, implying that there should be a compelling reason to disable a rule rather than enable it.

Also, for your convenience, we maintain a list of all the rules in the configuration, with disabled ones merely commented out. This helps keep the configuration in sync with new rules.

So, what else does DCM offer? If you remain unconvinced, consider exploring other functionality that DCM provides:

  • Code metrics: While we don't use them, some teams find them valuable.
  • Custom formatter: Although we don't use it, if you're dissatisfied with the default formatter, DCM offers a more configurable alternative.
  • Detection of unused files, code, and dependencies: This is a great utility for larger codebases, allowing you to eliminate dead code.
  • Various code assists in IDEs.
  • Improved performance: DCM was initially developed as a plugin for the built-in analyzer, and its performance was subpar, especially for large codebases. However, the current standalone version is significantly faster.
  • Strong support from the author is available in the Discord channel.

Lastly, but certainly not least, DCM has recently introduced a new free license for open-source repositories:

Free for OSS

DCM proves to be an invaluable companion, helping developers and teams achieve new heights of efficiency and code quality. It's high time to embrace advanced code analysis and style enforcement, and Dart Code Metrics, especially with a new licensing policy, stands as a standout choice to achieve these objectives.