Skip to main content

What’s new in DCM 1.8.0

· 12 min read

Cover

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

New command to simplify integrating DCM into existing projects, 17 new rules (2 for the Patrol testing library 🚀), support for dart_frog files for check-unused-code and check-unused-files, Gitlab reporter format for all existing commands and more 🔥.

And with this release, DCM crossed the 200 rule mark 🥳 (currently 215)!

Let’s go on a quick journey together to explore all updates!

Unused code in the IDE (Beta)

We are excited to announce that you can now enable the display of unused code in the IDE!

To do so, open the IDE extension settings and search for "Show unused code" option.

There are plenty areas of improvements (for example, this feature only supports the "monorepo" mode for now, can be a bit annoying when you just created a new class and so on). That's why we are releasing it as "Beta" (and disabled by default) and will work on improving it in the upcoming releases.

But we'd also love to hear your feedback!

Command updates

New command "dcm init preview"

This command allows you to run all available (or explicitly passed) DCM rules against your codebase and see which rules show violations, how many of these violations you have and whether these violations can be automatically fixed.

Here is the output example,

Output example

You can change the output format to one of the following: console (default), table or analysis_options. The latter can help you generate a list of rules that you can copy and paste into the analysis_options.yaml file you have.

Here is the analysis_options output example,

Analysis Options example

You can also change the list of the rules that DCM runs. To do so, you can pass --severity, --rule-types and --only-fixable options.

For example, to get only for Flutter and Bloc rule violations with warning and style severities, pass --severity=warning,style --rule-types=flutter,bloc.

Note: this command is a subcommand to the dcm init command, you can call it via dcm init preview. And to generate the baseline, call dcm init baseline.

Flutter plugins and dart_frog support for check-unused-code and check-unused-files commands

Check unused code and Check unused files commands rely on how files within the project are imported (and exported) to build the usage graph. But when it comes to specific conventions or packages, fully relying on this concept can produce unwanted false-positives.

That's why with this release to reduce this unwanted noise DCM supports Flutter federated plugins (the fileName field in the pubspec file is now used to correctly mark the give file as used) and dart_frog middlewares, requests and the entrypoint file.

GitLab format support

Initially, GitLab format was supported by only two commands, but with this release this format is supported by all other commands that are meant to be used on CI.

These commands are: check-dependencies, check-exports-completeness, check-unnecessary-nullable, check-unused-code, check-unused-files and check-unused-l10n.

Check dependencies

To reduce the need of passing excluded packages manually, with this release the "check-dependencies" command supports an internal list of packages that are used with other packages.

For example, if a package has a freezed_annotation or json_serializable then build_runner and freezed (only for freezed_annotation) will be automatically marked as used.

If you have other packages in mind, that can be automatically excluded based on the presence of other packages in pubspec, don't hesitate to share your feedback!

New rules

Patrol

prefer-custom-finder-over-find. Suggests using custom finders instead of find in Patrol tests.

Patrol provides its own way of finding widgets, which is the idiomatic way to write Patrol tests.

For example,

void main() {
patrolTest('widget test', () {
...
find.byType(SomeType);
find.text('SomeText');
});
}

should be rewritten to

void main() {
patrolTest('widget test', () {
...
$(SomeType);
$('SomeText');
});
}

You can read more about Patrol's custom finders here.

prefer-symbol-over-key. Suggests using symbols instead of string keys in Patrol tests.

When it comes to using Keys, Patrol has its own way as well. It's recommended to stick up to the Patrol's idiomatic way here and use symbols over string keys.

For example,

void main() {
patrolTest('widget test', () {
...
$(Key('button'));
});
}

should be rewritten to

void main() {
patrolTest('widget test', () {
...
$(#button);
});
}

Common

avoid-map-keys-contains. Warns when Map's .keys.contains is used instead of containsKey.

Although it might be convenient to use .keys.contains, it comes with a huge performance downside (it is 6000 times slower than containsKey) and is not recommended to be used.

For example,

void main() {
final map = {'hello': 'world'};

map.keys.contains('hello');
}

should be rewritten to

void main() {
final map = {'hello': 'world'};

map.containsKey('hello');
}

prefer-correct-json-casts. Warns when a JSON object type cast is done in an unsafe way that will throw at runtime.

Casting a JSON object to a map or list with generic types specified as anything except Object? / Object / dynamic can lead to a runtime exception. Instead, cast first to Object? and then to the desired type.

For example,

void main() {
final jsonRaw = '{"items": {"hello": 1}}';
final items = json['items'] as Map<String, int>;

final jsonRaw = '{"items": [1, 2, 3]}';
final items = json['items'] as List<int>;

if (json case Object() as List<String>) {}
}

should be rewritten to

import 'dart:convert';

void main() {
final jsonRaw = '{"items": {"hello": 1}}';
final json = jsonDecode(jsonRaw) as Map<String, Object?>;
final items = (json['items'] as Map<String, Object?>).cast<String, int>();

final jsonRaw = '{"items": [1, 2, 3]}';
final json = jsonDecode(jsonRaw) as Map<String, Object?>;
final items = (json['items'] as List<Object?>).cast<int>().toList();
}

avoid-async-call-in-sync-function. Warns when an async function is invoked in non-async blocks.

Making asynchronous calls in non-async functions is usually the sign of a programming error. In general these functions should be marked async and such futures should likely be awaited.

For example,

Future<void> asyncValue() async => 'value';

class SomeClass {
SomeClass() {
asyncValue();
}

Future<void> asyncMethod() => asyncValue();

Future<void> anotherAsyncMethod() async => asyncValue();

void syncFn() => asyncValue();
}

here, the constructor of SomeClass and the syncFn method invoke a function that is asynchronous and therefore will be executed later. If the code that calls them relies on this function result, it can lead to the wrong behavior.

It's recommended to await all async functions (and for the exceptional cases you can wrap them with unawaited or .ignore()).

So, the example above should be rewritten to

Future<void> asyncValue() async => 'value';

class SomeClass {
SomeClass() {
unawaited(asyncValue());
}

Future<void> asyncMethod() => asyncValue();

Future<void> anotherAsyncMethod() async => asyncValue();
}

This rule is an improved version of the standard rule discarded_futures.

avoid-duplicate-mixins. Warns when a class has a mixin that is already present in that class's hierarchy.

Duplicated mixins do not add any meaning and are usually a sign of a mistake, but they are also hard to spot.

For example,

mixin class MC {}
mixin M {}

class C1 = MC with MC;
class C2 = Object with M, M;
class C3 extends MC with MC {}
class C4 extends C1 with MC;
class C5 extends C6 with MC;
class C6 extends C1;

should be rewritten to

mixin class MC {}
mixin M {}

class C1 = MC;
class C2 = Object with M;
class C3 extends MC {}
class C4 extends C1;
class C5 extends C6;
class C6 extends C1;

avoid-nullable-interpolation. Warns when an interpolation string contains a nullable value.

Displaying null instead of the actual value is rarely a desired outcome. This rule helps you stop cases like that and add a null check before the value is used.

For example,

void function(String? value) {
print('$value hello');
}

should be rewritten to

void function(String? value) {
if (value != null) {
print('$value hello');
}

print('${value != null ? value : ''} hello');
}

avoid-unused-instances.Warns when a newly created object is not being used.

Sometimes due to a newly created object becomes unused (instead of being returned or thrown). And cases like that can be really hard to spot.

For example,

class SomeClass {
...
}

SomeClass function() {
if (someCondition) {
SomeClass();
}

return SomeClass();
}

here, SomeClass creation inside the if statement is unused and therefore is a bug. It's expected to be returned instead.

class SomeClass {
...
}

SomeClass function() {
if (someCondition) {
return SomeClass();
}

return SomeClass();
}

enum-constants-ordering. Ensures consistent alphabetical order of Enum constants.

Together with the map-keys-ordering this rule helps you delegate the ordering check to DCM and not think about it.

For example,

enum SomeEnum {
second,
first,
}

should be updated to

enum SomeEnum {
first,
second,
}

to keep the alphabetical order.

prefer-named-boolean-parameters. Warns when a declaration has a boolean positional parameter.

Converting positional boolean parameters to named parameters helps you avoid situations with a wrong value passed to the parameter.

For example,

void someFunction(String name, bool isExternal, bool isTemporary) {
...
}

someFunction('User', true, false);

should be rewritten to

void someFunction(
String name, {
required bool isExternal,
required bool isTemporary,
}) {
...
}

someFunction('User', isExternal: true, isTemporary: false);

prefer-correct-for-loop-increment. Warns when a for loop increments a wrong variable.

For example,

void main() {
var j = 1;

for (var i = 0; i < 10; j++) {
...
}
}

here, it's really hard to notice that it's j that is being incremented and not i therefore resulting in an endless loop.

prefer-public-exception-classes. Warns when an exception class declaration is not public.

If a private exception is thrown, the external code will have to catch instances of the nearest accessible parent class. In this case, it becomes more difficult to handle specific exceptions, because the external code will not be able to clearly identify the problem that has arisen.

Lack of clear identification of exceptions poses an additional security risk because some specific exceptions may require specific handling rather than general handling.

For example,

class _MyPrivateException implements Exception {}

class _MyPrivateException extends Exception {}

should be rewritten to

class MyException implements Exception {}

class MyException extends Exception {}

match-class-name-pattern. Configure class names to match the given pattern.

Helps you ensure that classes within a given directory or file are named in a specific way.

With the given config:

dart_code_metrics:
...
rules:
...
- match-class-name-pattern:
entries:
- path: '.*state.dart'
pattern: 'State$'
ignore-private: true

will trigger on

class SomeClass {
...
}

class MyState {}

since SomeClass is expected to end with State.

newline-before-case. Enforces a blank line between cases in a switch statement.

For example,

final value = 'some-value';

switch (value) {
case '1': {
...
}
case '2':
...
case '3':
}

should be rewritten to

final value = 'some-value';

switch (value) {
case '1': {
...
}

case '2':
case '3':
...
}

avoid-unnecessary-reassignment. Warns when a value is reassigned to a variable without using the initial value.

For example,

String alpha = '';
if (something) {
alpha = 'true value';
} else {
alpha = 'false value';
}

String beta = '';
beta = something ? 'true value' : 'false value';

SomeObject object = SomeObject();
object = something ? SomeObject() : SomeObject();

here, since the alpha, beta and object variables are not used before their value is reassigned, the code can be simplified to

final alpha = something ? 'true value' : 'false value';
final beta = something ? 'true value' : 'false value';
final object = SomeObject();

Flutter

avoid-recursive-widget-calls. Warns when a Widget is recursively used within the same Widget.

Recursive use of a widget is most often a sign of a bug and can get your application stuck in an endless loop.

For example,

class MyHomePage extends StatefulWidget {
const MyHomePage();


_MyHomePageState createState() => _MyHomePageState<MyHomePage>();
}

class _MyHomePageState extends State<MyHomePage> {

Widget build() {
return MyHomePage();
}
}

class MyOtherWidget extends StatelessWidget {

Widget build() {
return MyOtherWidget();
}
}

here, _MyHomePageState and MyOtherWidget recursively call themselves.

prefer-text-rich. Warns when a RichText widget is used instead of Text.rich.

RichText does not handle text scaling very well, so if accessability is important for you, use Text.rich instead.

For example,

RichText(
text: TextSpan(
text: 'Hello ',
children: [
TextSpan(text: 'bold'),
TextSpan(text: ' world!'),
],
),
);

should be rewritten to

Text.rich(
TextSpan(
text: 'Hello ',
children: [
TextSpan(text: 'bold'),
TextSpan(text: ' world!'),
],
),
)

Other rules that got updates

Rules below got several updates (including bug fixes, new options and new use-cases supported):

  • avoid-barrel-files
  • arguments-ordering
  • avoid-importing-entrypoint-exports
  • format-comment
  • avoid-unnecessary-futures
  • avoid-redundant-async
  • function-always-returns-null
  • unnecessary-trailing-comma
  • avoid-mutating-parameters
  • prefer-first
  • prefer-last
  • avoid-unsafe-collection-methods
  • prefer-explicit-parameter-names
  • prefer-correct-error-name
  • newline-before-return
  • avoid-unnecessary-return
  • member-ordering
  • prefer-moving-to-variable
  • list-all-equatable-fields
  • avoid-long-functions
  • avoid-explicit-type-declaration
  • no-equal-then-else
  • prefer-early-return
  • dispose-fields
  • avoid-unrelated-type-assertions
  • no-equal-arguments
  • avoid-unused-parameters

New quick fixes for existing rules 🔥

Rules below got quick fixes support (by dcm fix, in the IDE or both):

  • map-keys-ordering

What’s next

Improving the "unused code in the IDE" feature. There are plenty areas of improvements (for example, this feature only supports the "monorepo" mode for now, can be a bit annoying when you just created a new class and so on). That's why we are releasing it as "Beta" (and disabled by default) and will work on improving it in the upcoming releases.

New commands. Reworking metric HTML reports unblocked other commands that need an HTML report as well. We hope to add them in the upcoming release to make DCM cover even more use-cases.

Move performance improvements. We received a several reports about DCM performance issues on very large codebase. This is a serious problems that we also want to address. There is still some room to make DCM better.

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!