Skip to main content

What’s new in DCM 1.17.0

· 11 min read

Cover

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

This release includes 9 new rules (3 for Riverpod 🔥), early access to the DCM Teams license management portal, per-package monorepo mode, global configuration with DCM version constrains and more 🚀.

info

The recommended preset has been updated and now includes some rules from 1.14.0, 1.15.0 and 1.16.0, so you may get new warnings if you use the build-in recommended preset or once you update the dart-code-metrics-presets package dependency.

warning

❗️ With the next release we plan to discontinue all DCM versions prior to 1.11.0.

If you're still using one of those, consider upgrading to a newer version.

Let's go through the highlights of this release! (And you can find the full list of changes in our changelog)

DCM Teams Console early access

We are excited to announce that DCM Teams Console (the admin portal to manage DCM seats and access) is now in early access!

Console Preview

While we are finishing testing all available features, here are a few key upcoming changes:

  1. We are moving away from the approach of having a single activation key for the entire team. Now each developer will have their own key, which they will receive separately (via email) after the Console administrator adds them.
  2. "dcm deactivate" will no longer work for licenses with the enabled DCM Teams Console.
  3. Because the purpose of the admin panel is to simplify access management, license activations are now associated with an email address, and since there is currently no such association, enabling a DCM Teams Console account for your license will require a complete reset of all current activations. This means your entire team will have to reactivate their devices. That's why we don't enable new accounts for everyone right now, but instead perform manual activation on a per-license basis.
  4. We'll first enable the Console for just a few teams to make sure everything works as expected. After that, we plan to enable the console for all new licenses and eventually migrate all existing teams by the end of 2024. If you would like the DCM Teams Console to be enabled for your license in the near future, please let us know.
  5. To use the Console you will need DCM 1.17.0 or later.
  6. In the near future, we also plan to introduce a feature to increase/decrease (for monthly subscriptions only) the number of seats through the DCM Teams Console.

If you still have questions about the DCM Teams Console, please reach out via Discord or email. We are looking forward to your feedback!

Global DCM configuration

To help you with managing DCM version across CI/CD and your team, DCM now support global configuration file called dcm_global.yaml.

To configure DCM version globally, add a dcm_global.yaml file to the root of your project:

dcm_global.yaml
version: '>=1.17.0 <2.0.0'

If the installed executable does not match the constraint, you'll get a notification in your IDE and all analysis-related DCM commands will produce a warning and exit with an error code.

And if a new DCM version does not match the constraint, you won't get a new version notification in your IDE and CLI.

It's important to mention that the tool will continue to work even if the installed DCM version does not match the configured constraint, to avoid any confusion.

Per-package monorepo mode

Prior to this release, the monorepo mode for commands such as check-unused-code, check-unused-files (and others that support the --monorepo flag) was applied to either all analyzed packages/contexts or none of them.

Now, with the new monorepo: true | false config entry in analysis_options.yaml, you can override the CLI flag for any particular package.

For example, if you have package in your monorepo that you publish to pub and therefore won't to avoid any breaking changes to its API, you can now set monorepo: false config option in its analysis_options.yaml file, and even if you call dcm check-unused-code packages --monorepo, the package would still be analyzed with the monorepo mode disabled.

"widgets-nesting-level" metric improvement

This release includes a notable fix to the widgets-nesting-level metrics.

Consider the following example:

class MyCustomWidget extends StatelessWidget {
const MyCustomWidget();


Widget build(BuildContext context) {
final widget = const Expanded(
child: Center(
child: Text('Hello, world!'),
),
);

return Material(
child: Column(
children: [
MyAppBar(title: Text('Example title')),
widget,
],
),
);
}
}

before this release, DCM did not take into account variables, so the widget-nesting-level metric value for this widget was 4.

Now, the metric correctly analyzes variables and their usage and shows nesting level for this widget equal 5 (as it should have).

Unused localization improvements

To avoid any confusion with the check-unused-l10n command showing empty output, the command now requires the --class-pattern option to be always present and shows a hint if the provided regular expression does not match any classes inside the analyzed folders.

Rule updates

"dispose-fields" improvements

This rule now correctly excludes disposable fields that are assigned from the context.

For example,

class _ShinyState extends State<ShinyWidget> {
SomeDisposable? _someDisposable;
SomeDisposable? _anotherDisposable;

void someUpdate() {
_someDisposable = SomeDisposable.of(context);

final local = SomeDisposable.of(context);
_anotherDisposable = local;
}

void dispose() {
super.dispose();
}
}

class SomeDisposable implements Disposable {

void dispose() { ... }

static SomeDisposable of(BuildContext context) => ...;
}

here, even though both _someDisposable and _anotherDisposable have a dispose method that is not called inside the state's dispose, the rule won't trigger since it's usually the responsibility of the entity that creates the service to dispose it. And here this state only uses already created instances.

"banned-usage" improvements

With this release the rule now supports configuration for banning static fields, enum constants and explicit call invocations.

For example,

dart_code_metrics:
...
rules:
...
- banned-usage:
entries:
- ident: StaticClass.field
description: Do not use this field
- ident: MyEnum.firstValue
description: Do not use this enum value
- type: SomeClass
entries:
- ident: call
description: Do not use implicit call()

will trigger on

void main() {
StaticClass.field; // <--- banned
MyEnum.firstValue; // <--- banned

final instance = SomeClass();
instance(); // <--- banned
}

class StaticClass {
static const field = 'some';
}

enum MyEnum { firstValue }

class SomeClass {
void call() { ... }
}

"avoid-unnecessary-stateful-widgets" improvements

The rule now correctly excludes static members and overridden members that have only a super call or an empty body.

For example,

class MyWidget9 extends StatefulWidget {
const MyWidget9({super.key});


State<MyWidget9> createState() => _MyWidget9State();
}

class _MyWidget9State extends State<MyWidget9> {

void initState() {
super.initState();
}


Widget build(BuildContext context) {
return Container();
}
}

prior to this release, the rule would consider this as a valid stateful widget because it has an overridden member (initState). But this declaration can be simply removed.

Now, the rule correctly highlights such cases.

"member-ordering" improvements

This release includes a fix for an edge case for the field-getter-setter config entry and several fields with matching names.

For example,


class Some {
final String b123;

final String _b123;
}

before the fix the rule was trying to group those two fields together (since they both have matching b123 part).

Now, it correctly treats them as separate members.

New rules

Riverpod

avoid-ref-read-inside-build. Warns when ref.read is used inside a build method.

ref.read is designed to read the value one time which is, if done inside the build method, can lead to missing the event when the provided value changes.

For example,

class HomeConsumerWidget extends ConsumerWidget {
const HomeConsumerWidget({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.read(counterProvider);

return Text('$counter');
}
}

here, the widget won't be rebuilt when the counter value changes.

To avoid this bug, use ref.watch instead:

class HomeConsumerWidget extends ConsumerWidget {
const HomeConsumerWidget({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

avoid-ref-watch-outside-build. Warns when ref.watch is used outside of the build method.

ref.watch is designed to subscribe to changes which is not needed when the value is red outside of the build method.

For example,

class HomeConsumerWidget extends ConsumerWidget {
const HomeConsumerWidget();

void callback(WidgetRef ref) {
final value = ref.watch(counterProvider);
...
}

...
}

here, the callback should use ref.read instead of ref.watch:

class HomeConsumerWidget extends ConsumerWidget {
const HomeConsumerWidget();

void callback(WidgetRef ref) {
final value = ref.read(counterProvider);
...
}
}

avoid-unnecessary-consumer-widgets. Warns when a ConsumerWidget has an unused ref and can be converted to a regular widget.

For example,

class HomeConsumerWidget extends ConsumerWidget {
const HomeConsumerWidget({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Text('$counter');
}
}

class AnotherHomeConsumerWidget extends ConsumerWidget {
const AnotherHomeConsumerWidget();


Widget build(BuildContext context, WidgetRef ref) {
return Consumer(
builder: (context, ref, child) {
final value = ref.watch(helloWorldProvider);
return Text(value);
},
);
}
}

here, both HomeConsumerWidget and AnotherHomeConsumerWidget don't reference WidgetRef ref in their build methods and therefore can be rewritten to be regular StatelessWidgets.

Flutter Hooks

prefer-use-callback. Suggests using useCallback instead of useMemoized when applicable.

For example,

void fn() {
useMemoized(() => () => {}, keys);
}

here, instead of passing the inner callback, useMemoized can be replaced with useCallback:

void fn() {
useCallback(() => {}, keys);
}

avoid-misused-hooks. Warns when hooks are used in non-hook widgets.

For example,

class SomeWidget extends StatelessWidget {
const SomeWidget();


Widget build(BuildContext context) {
useMemoized(() => {}, keys);

return Container();
}
}

should be rewritten to (or with any other widget that supports hooks):

class SomeHookWidget extends HookWidget {
const SomeHookWidget();


Widget build(BuildContext context) {
useMemoized(() => {}, keys);

return Container();
}
}

Common

avoid-collection-equality-checks. Warns when a collection is checked for equality with another collection.

Collections in Dart have no inherent equality. Two sets are not equal, even if they contain exactly the same objects as elements.

To compare for equality, use custom deep equality checks (or one of the existing packages, e.g. collection).

For example,

void fn() {
final anotherList1 = [1, 2, 3];
final anotherList2 = [1, 2, 3];

print(anotherList1 == anotherList2);
}

here, anotherList1 is never equal to anotherList2 and therefore the result of comparison is always false.

This example can use ListEquality (from the package collection) instead:

void fn() {
final anotherList1 = [1, 2, 3];
final anotherList2 = [1, 2, 3];

ListEquality().equals(anotherList1, anotherList2); // actually works
}

avoid-multi-assignment. Warns when multiple assignments are placed on the same line.

For example,

class SomeClass {
String someString = 'some';
String another = 'another';

void update(String str) {
someString = another = str;

final instance = SomeClass();
instance.another = someString = str;
}
}

here, both multi assignments can be easily confused with equality checks and the rule recommends placing them on separate lines:

class SomeClass {
String someString = 'some';
String another = 'another';

void update(String str) {
someString = str;
another = str;
}
}

prefer-switch-with-sealed-classes. Suggests to use a switch statement or expression instead of conditionals with sealed class instances.

When it comes to sealed classes, the analyzer warns you when a new class is not covered by a switch case. This feature is not available for if statements and conditional expressions.

Therefore using switches where possible can reduce the number of potential bugs.

For example,

void fn() {
final Vehicle? value;

if (value is Car || value is Truck) {}
if (value case Car() || Truck()) {}
}

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck implements Vehicle {
const Truck();
}

should be rewritten to:

void fn() {
final Vehicle? value;

switch (value) {
case Car():
...

case Truck():
...
}
}

prefer-single-declaration-per-file. Warns when a file contains more than one top-level declaration (class, mixin, extension, enum or extension type).

What’s next

Baseline for all commands. To further simplify DCM integration into existing projects, we want to expand baseline support from just "dcm analyze" to all commands that can produce analysis issues ("dcm check-unused-code", "dcm check-unused-files" and other).

Support for all commands in DCM GitHub action. One of the ideas behind "dcm run" was to integrate it into DCM GitHub action so that it's not limited to just "dcm analyze". Expanding the number of supported commands will make the action even better.

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.

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!