Skip to main content

What’s new in DCM 1.15.0

· 10 min read

Cover

Today we’re excited to announce the release of DCM 1.15.0!

This release includes extension types support for all existing rules and features, 13 new rules (4 for extension types), 30 new quick fixes (some of them are available via dcm fix), Checkstyle format for all available commands, UX improvements and more. 🚀

Let's go through the highlights of this release!

Extension types support

Extension types are now fully supported by all existing rules and commands! 🔥

This includes supporting various new cases for 24 existing rule (e.g. avoid-passing-self-as-argument, avoid-future-tostring avoid-collection-methods-with-unrelated-types and other).

Configuration

To help you with setting DCM up correctly, this release includes another config validation improvement, but this time it's for the top-level DCM config entries (e.g. rules, rules-exclude, etc.).

Here is how it looks in the IDE:

Validation Example

General command improvements

Checkstyle

To level up integration with BitBucket, all existing commands (including dcm run) now support the Checkstyle output format.

Checkstyle Example

Previously, only dcm analyze and dcm calculate-metrics provided support for this format.

Relative path by default

All DCM commands now use relative paths for output (e.g. in warning locations) by default since all modern IDEs and terminals support relative paths just fine, but switching to relative paths makes the output easier to read for large projects with many folders.

Before:

Path Before

After:

Path After

You can switch back to absolute paths by passing --absolute-path (or -a) CLI option to any of the commands.

Unused code improvements

While we are still working on taking the unused code IDE integration out of the beta version, this release includes two small improvements to how unused code is calculated.

Now, assigning a value to a variable is not considered as usage in some contexts (e.g. it's still a usage if it's part of a return statement).

class WithMutableVariables {
int value = 0;

int? _another = 1;
}

int mutate(WithMutableVariables input) {
input.value += 1; // This assignment does not mean that the `value` is used

return input._another ??= 5; // This one caches `5` after the first invocation
}

Additionally, some collection methods are now also not considered as usage.

For example,


class WithCollection {
final localItems = <String>{};
}

void insertItem(WithCollection input) {
input.localItems.add(''); // Adding a string is not a sign of usage
}

here, the localItems field is never used to get its value, and therefore just adding new elements to it is not considered as usage.

Exports completeness improvements

With this release, all parameters marked @visibleForTesting are now excluded from the public API graph and are not required to be exported.

Additionally, all exported declarations marked @internal are now reported, since exporting them clearly violates the idea of the @internal annotation.

To fix this warning, you can either make those declarations private or hide them explicitly using the hide combinator of the export directive.


const topLevel = 'value';

class PublicDeclaration {
...
}
export 'other_file.dart' hide topLevel;

IDEs

To improve the user experience of the "New Version" notification in both VS Code and IntelliJ, there is now a new "Open Changelog" button to help you get to our changelog faster.

Notification

You can now also toggle the visibility of issues from baseline using two new IDE commands: Show/Hide Issues From Baseline.

It works similarly to the config option that was introduced in the previous release, but now you can assign a shortcut to it. Plus, these commands automatically restart the analysis server.

Rule updates

New config options for "member-ordering"

Before this release, the member-ordering rule worked only for class declarations, but now you can enable it for other declarations (extensions, mixins, enums and extension types).

dart_code_metrics:
...
rules:
...
- member-ordering:
ignore-mixins: true
ignore-extensions: true
ignore-extension-types: true
ignore-enums: true
info

Support for new declarations is disabled by default to avoid potentially overwhelming number of warnings after updating to this version.

New quick fixes

With this release, 30 existing rules now have a quick fix:

  • avoid-banned-imports
  • avoid-banned-annotations
  • avoid-duplicate-test-assertions
  • avoid-map-keys-contains
  • avoid-nullable-tostring
  • prefer-addition-subtraction-assignments
  • prefer-moving-to-variable
  • avoid-importing-entrypoint-exports
  • avoid-duplicate-map-keys
  • prefer-explicit-type-arguments
  • prefer-both-inlining-annotations
  • avoid-cascade-after-if-null
  • prefer-parentheses-with-if-null
  • prefer-returning-conditional-expressions
  • prefer-correct-error-name
  • move-variable-outside-iteration
  • avoid-negated-conditions
  • prefer-specific-cases-first
  • enum-constants-ordering
  • avoid-equal-expressions
  • avoid-duplicate-mixins
  • avoid-duplicate-cascades
  • move-records-to-typedefs
  • prefer-define-hero-tag
  • prefer-action-button-tooltip
  • prefer-widget-private-members
  • prefer-text-rich
  • avoid-async-callback-in-fake-async
  • avoid-bloc-public-methods
  • prefer-correct-bloc-provider

Not all new quick fixes are supported by dcm fix as some of them need your decision (e.g. whether a duplicate can be safely removed or is actually a bug).

You can find the updated list of quick fixes that are not supported by dcm fix here.

New rules

Flutter

avoid-unnecessary-gesture-detector. Warns when a GestureDetector widget has no event handlers.

Gesture detectors with no handlers have no additional functionality and can be safely removed (or updated to have at least one handler).

For example,

SomeWidget(
...
child: GestureDetector(
child: ChildWidget(
...
),
),
)

can be replaced with

SomeWidget(
...
child: ChildWidget(
...
),
)

avoid-missing-controller. Warns when a TextFormField, TextField or EditableText does not have at least one way to get the updated value.

Not providing a controller or not getting an updated value in a callback leads to the widget state not being updated when the user updates the field.

For example,

TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
initialValue: '',
)

has no way to get the updated value and should be rewritten to

TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
controller: _stateController,
)

// OR

TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
onChanged: (newValue) {
_value = newValue,
},
)

Common

avoid-returning-void. Suggests calling functions or methods with a void return type separately from the return statement.

Returning void invocations can be confusing for code reviewers and introduce unnecessary changes if the return type changes.

For example,

void someFunction() {
return someVoidFunction();
}

Future<void> asyncFn() async {
return someVoidFunction();
}

void someVoidFunction() {
...
}

here, both return statements will be highlighted by the rule.

prefer-private-extension-type-field. Warns when an extension type has a public representation field.

Public representation fields can be accessed directly, which may lead to potential mistakes. Consider exposing only the required properties or methods.

For example,

extension type ET(String value) {
bool get isNotEmpty => ... // custom implementation
}

here, calling et.isNotEmpty and et.value.isNotEmpty can lead to potentially different results.

To avoid that, prefer making the representation field private

extension type ET(String _value) {
bool get isNotEmpty => ...
}

avoid-empty-spread. Warns when a collection literal in spread (...[]) has no elements.

For example,

void fn(bool flag) {
final another = [
...<String>[],
if (flag) ...<String>[],
];
}

here, both these spreads are empty and can be safely removed.

avoid-renaming-representation-getters. Warns when an extension type exposes a member that renames an existing representation field member.

Renaming an existing member from the representation type can lead to confusing results.

For example,

extension type ET(String _value) implements String {
bool get isEmpty => _value.isNotEmpty;

bool get isNotEmpty => _value.isEmpty;
}

here, isEmpty and isNotEmpty are inverted which is not something an external user of the extension type expects. But this also can a typo.

So the correct version is

extension type ET(String _value) implements String {
bool get isEmpty => _value.isEmpty;

bool get isNotEmpty => _value.isNotEmpty;
}

Additionally, consider adding the @redeclare annotation as it helps showing that the redeclaration is intentional.

avoid-missing-completer-stack-trace. Warns when Completer.completeError is called without providing a stack trace.

By default, completeError gets an empty stack trace. Consider passing StackTrace.current or Error(...).stackTrace to the invocation.

import 'dart:async';

void fn() {
final completer = Completer();
completer.completeError('foo');

Completer().completeError('foo');
}

should be rewritten to

import 'dart:async';

void fn() {
final completer = Completer();
completer.completeError('foo', StackTrace.current);

// or if the Error object is present
completer.completeError('foo', error.stackTrace);
}

avoid-casting-to-extension-type. Warns when an expression is cast to an extension type.

Casting to an extension type can throw at runtime, whereas instantiating it is safe and you'll get a hint about the representation field type mismatch.

For example,

void fn(String input) {
final value1 = input as ET;
final value2 = input as ET1;
}

extension type const ET(String s) {}
extension type const ET1(int v) {}

here, even though the first cast is correct, the second one throws at runtime because of mismatching types of representation fields.

So the correct approach is to instantiate the extension type instead

void fn(String input) {
final value = ET(input);
}

extension type const ET(String s) {}

which instantly shows any potential type mismatch.

avoid-nested-extension-types. Warns when the representation field of an extension type is also an extension type.

As there is no limit on extension types created from other extension types, this rules helps with avoiding deep nesting.

avoid-slow-collection-methods. Warns when an invocation is a slow sync* invocation.

Using sync* invocations can result in a ~2 times slower code compared to other approaches.

Additionally, this rule highlights .expand() usages as replacing them with a spread gives ~3 times performance difference.

void fn() {
const iterable = [null, 2, 3, 4, 5, 6, 7, 8, 9];

iterable.whereNotNull();
}

here, whereNotNull from package:collection has sync* implementation and can be replaced with whereType

void fn() {
const iterable = [null, 2, 3, 4, 5, 6, 7, 8, 9];

iterable.whereType<int>();
}

prefer-overriding-parent-equality. Warns when a parent class implements hashCode and ==, but the child class does not implement them.

Not providing an implementation in the child class can lead to tricky bugs where equality checks don't work as expected.

For example,

class SomeClass {

int get hashCode => 1;


bool operator ==(Object other) {
return super == other;
}
}

class AnotherClass extends SomeClass {
final String value;

AnotherClass(this.value);
}

here, using instances of AnotherClass in contexts that rely on hashCode (e.g. hash maps) can give unexpected results.

avoid-unnecessary-collections. Warns when a collection literal can be replaced by its first and only element.

For example,

Future<void> fn() async {
await Future.wait([future]);
await Future.any([future]);

_array.addAll([value]);
_array.addAll({value});

Stream.fromFutures([future]);
Stream.fromIterable([value]);

set.addAll([value]);
set.containsAll([value]);
}

can be simplified to

Future<void> fn() async {
await future;

_array.add(value);

Stream.fromFuture(future);
Stream.value(value);

set.add(value);
set.contains(value);
}

avoid-unknown-pragma. Warns when the @pragma annotation has an unknown value.

Given that pragmas accept a string value, it's relatively easy to make a typo. This rule guards against that.

('vm:prefer-inlined')
Future<String> fn() {
...
}

here, vm:prefer-inlined is a non-existing annotation and the correct version is vm:prefer-inline.

What’s next

Documentation improvements. We plan to improve the overall state of the documentation, including a rewrite of the Getting Started section, introducing several user guides, as well as updating the example rules and adding more information about the problem behind each rule. We hope these changes will help us better explain DCM's features and help you get started with the product faster.

New command to analyze project structure. This command will help you visualize and analyze how different files / folders / packages depend on each other. Such analysis can help you create better boundaries between different modules of your apps and make the code more maintainable. On top of that, the command will allow validation of the project structure including cases when a file is expected to be present, but it as actually missing.

Sharing your feedback

If there is something you miss from DCM right now or want us to make something better or have any general feedback - join our Discord server!