What’s new in DCM 1.34.0

Today we’re excited to announce the release of DCM 1.34.0!
This release includes 6 new rules; updated "recommended" preset; new baseline flag to update only a section of the existing baseline; numerous improvements to all existing metrics to support the latest Dart features and provide more accurate calculations; support for displaying which lines contribute to a particular metric in the metrics HTML reports; and more!
Let’s go through the highlights of this release (and the full list of changes is in our changelog)!
Baseline Improvements
Flag to Update Only a Particular Section
With this release, the dcm init baseline command now has a new --merge flag to update only a particular section of the existing baseline.
For example, the baseline was generated using --analyze, --unused-code and --unused-files and if you would like to update only the "analyze" section, running dcm init baseline . --analyze --merge will update it while also keeping other section intact.
When used together with the --only-rules option, it will only add issues from the passed rules to the "analyze" section of the baseline.
You can use this flag together with the --update flag.
New Hint on Incorrect --update Usages
For the --update flag to work, it needs to be used with the same number of commands as the initially generated baseline (because this flag only removes no longer reported issue without adding new ones).
With this release you will now see a new hint for cases when the initially used commands does not match the ones passed with the --update flag:

We hope this improvement removes any confusion when using the --update flag.
Check Exports Completeness Improvements
Exact Entrypoint Path
Prior to this release, the initial file with the export declaration (from which the export tree is calculated) was simplified to entrypoint in the command output.
Unfortunately, while that help reduce the total length of the issue message, it was completely uninformative and hard to work with.
With this release, such entrypoint files now have clear relative path displayed:

Improved Excludes and Ignores
Additionally, we have improved how excludes are applied to the results of this command.
Before, the excludes were only working for the entrypoint files (the ones under lib/, but not lib/src/) and were not working for the transitively not exported entities.
Now, if you can pass a pattern to the --exclude option and ignore any file reported by this command.
dcm ec lib --exclude="{**/some_file.dart,**/another.dart}"
Plus, both // ignore: exports and // ignore_for_file: exports are now correctly working for any reported issue.
Metric Improvements
First of all, if you are already using the latest Dart changes (e.g. dot shorthands), this release brings full metrics support for them (if you were getting incorrect results for the latest language features before, updating should resolve this problem).
We've been actively working on metrics for the past 3 releases (various improvements can be found in the changelog), but we are not yet done! If you have any suggestions for new metrics or improvements, please let us know.
Understanding What Contributes to the Metric Value
With this release we are also excited to announce a new feature in the metrics HTML reports: an ability to highlight the lines that affect the metric value:
We expect this feature to significantly simplify refactoring and general understanding of how each metric is calculated.
Updated Metrics
number-of-used-widgets and widgets-nesting-level
Both metrics are now calculated for any method that returns a widget, not just the build method.
This can be particularly useful if a widget has methods that are passed to builders.
class MyAppBar extends StatelessWidget {
const MyAppBar({required this.title});
final Widget title;
// WNL: 3
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
IconButton(
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
Expanded(child: title),
],
),
);
}
// WNL: 3
Widget _buildSome() {
return Container(
child: Row(children: [Expanded(child: title)]),
);
}
}
number-of-overridden-methods
Now is also calculated for other declarations (not just classes) that can have overridden methods.
// NOOM: 1
class SomeClass extends AnotherClass {
void method() {}
void someMethod() {}
void first() {}
}
// NOOM: 1
enum E implements AnotherClass {
item;
void first() {}
}
// NOOM: 1
mixin M on AnotherClass {
void first() {}
}
abstract class AnotherClass {
void first() {
...
}
}
number-of-added-methods
Now no longer calculates the value for classes that do not extend any other class as they cannot introduce "added methods" (before that all their methods were considered "added" which was confusing).
// NOAM: 1
class Child extends Parent {
void someMethod() {}
}
// No longer counted since it has no parent class
class SomeClass {
void someMethod() {}
}
abstract class Parent {
void first() {}
}
number-of-parameters
Now skips not just the copyWith method (which is used to create a new copy of an instance and usually has a lot of parameters which match the number of fields the instance has), but any method that returns an instance of the enclosing class.
class SomeClass {
final String field;
final int another;
// Skipped
SomeClass copyWith({String? field, int? another}) {
...
}
// Now also skipped
SomeClass copyWithout({String? field, int? another}) {
...
}
// Also skipped since it returns SomeClass
SomeClass anyOtherName({String? field, int? another}) {
...
}
}
maximum-nesting-level
For this metric, we have reverted the change made in 1.32.0 and while some expressions (especially in widget code) do increase nesting, we decided to keep this metric strictly about statements.
// MNL: 5
void fn(Object string) {
switch (string) {
case '123':
{
final variable = () {
...
try {
...
} catch (_) {
} finally {
if (string is String) {
...
}
}
};
}
}
}
We might introduce another metric to help deal with the nesting of expressions.
number-of-external-imports
To align this metric with the number-of-imports, it now counts the total number of external imports (not just unique). Plus, it now correctly takes into account "dart:" imports.
// NOEI: 4
import 'dart:io';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
number-of-implemented-interfaces
Now correctly includes all transitive interfaces, not just direct. Additionally, the number of interfaces is now calculated for enums and mixins (which can also have interfaces).
// NOII: 3
class Regular extends SomeOtherBase
implements SomeInterface, MyClass, MyPerfectClass {
final String value;
Regular(this.value);
void implement() {}
}
// NOII: 1
enum E implements SomeInterface { first, second }
// NOII: 1
mixin M implements MyClass {}
cyclomatic-complexity
Now does not consider some statements and expressions as increasing complexity.
The full list of statements and expressions that affect the metric is:
if(statement and expression),catch,do,while,for(statement and expression)- conditional expressions (
? ... : ...) casein a switch statement or expression (defaultor last case does not count)- null-aware operators (
?[],?.,?..,...?,{key: ?value}) &&and||in binary expressions or patterns??and??=
coupling-between-object-classes
Now correctly counts method invocations on complex targets.
void someMethod() {
// now also counted as coupling
some.variable.field.method();
}
Preset Updates
We've updated the recommended preset to include rules from previous releases:
- avoid-always-null-parameters
- avoid-constant-conditions
- avoid-constant-switches
- avoid-default-tostring
- avoid-nested-shorthands
- avoid-never-passed-parameters
- avoid-stream-tostring
- avoid-suspicious-global-reference
- avoid-unassigned-fields
- avoid-unnecessary-late-fields
- avoid-unnecessary-length-check
- avoid-unnecessary-local-variable
- avoid-unnecessary-null-aware-elements
- avoid-unnecessary-nullable-parameters
- avoid-unnecessary-statements
- avoid-unreachable-for-loop
- avoid-unremovable-callbacks-in-listeners
- avoid-unused-local-variable
- avoid-wildcard-cases-with-sealed-classes
- match-base-class-default-value
- prefer-null-aware-elements
- prefer-returning-shorthands
- prefer-shorthands-with-constructors
- prefer-shorthands-with-enums
- prefer-shorthands-with-static-fields
- prefer-specifying-future-value-type
- avoid-mounted-in-setstate
If you're using extends: - recommended, these changes will apply automatically.
Rule Updates
banned-usage
The banned-usage rule now supports new syntax for synthetic enum fields that applies to all enums (without the need to list each enum name separately).
For example,
dcm:
rules:
- banned-usage:
entries:
- name: Enum.values
description: Do not use "values"
- name: Enum.name
description: Do not use "name"
enum MyEnum { first }
enum AnotherEnum { first }
void fn() {
MyEnum.values; // LINT: Do not use "values"
AnotherEnum.first.name; // LINT: Do not use "name"
}
New Rules
Discovering and Adding Rules from New Releases
Each DCM release introduces new rules. To explore all available rules and filter by version, use the dcm init lints-preview command:
dcm init lints-preview lib --rule-version=1.34.0 # Show rules added in 1.34.0
This command displays:
- Rule names and their violations in your codebase
- Estimated effort to fix all violations of each rule
- Whether a rule supports automatic fixes
You can also generate the output in different formats.
pass-optional-argument
Warns when the configured invocation does not have the configured optional argument.
This rule helps with cases when an external declaration (e.g. a pub package or SDK code) has optional parameters that you'd want to always pass. Alternatively, you might want to use this rule to gradually migrate to a required parameter.
For other cases try using the built-in required keyword to make parameters required.
For example,
dcm:
rules:
- pass-optional-argument:
entries:
- name: SomeClass
args:
- label
class SomeClass {
final int regular;
final String? label;
const SomeClass(this.regular, {this.label});
}
void fn() {
// LINT: This invocation is missing required optional arguments: label. Try passing them.
final instance = SomeClass(1);
}
To fix this issue, pass the label argument:
void fn() {
final instance = SomeClass(1, label: 'value');
}
avoid-immediately-invoked-functions
Suggests moving functions out instead of using then in immediately invoked function expressions.
Immediately invoked function expressions can easily make code hard to read and understand. Instead, consider moving them out as methods/functions or widgets.
For example,
Widget build(BuildContext context) {
return Container(
// LINT: Avoid immediately invoked function expressions.
// Try moving this function out as a method or widget.
child: () {
int taskCount = getTasks();
return Text('Tasks remaining: $taskCount');
}(),
);
}
can be simplified to
late int taskCount;
void initState() {
super.initState();
taskCount = getTasks();
}
Widget build(BuildContext context) {
return Container(
child: Text('Tasks remaining: $taskCount'),
);
}
avoid-complex-conditions
Warns when the complexity of a binary expression exceeds the configured threshold.
Overly complex conditions reduce readability and require additional effort to understand how different values affect the result. Consider simplifying such conditions by extracting methods or variables.
For example,
void fn() {
final list = <String>[];
// LINT: The complexity of this condition (9) exceeds the configured threshold (3).
// Try simplifying this condition.
if ((list.contains('123') ?? false) && _condition() ||
(list
.where((item) => item.isNotEmpty)
.any((item) => item.contains('123')))) {}
}
bool _condition() => false;
can be simplified to
void fn() {
final list = <String>[];
final leftCondition = (list.contains('123') ?? false) && _condition();
final rightCondition = list.where((item) => item.isNotEmpty).any((item) => item.contains('123'));
if (leftCondition || rightCondition) {}
}
bool _condition() => false;
This rule has a configurable threshold.
avoid-assigning-notifiers
Warns when a notifier instance obtained from ref.read is assigned to a variable.
Assigning notifier instances to variables is generally not recommended, as it can lead to stale references to already unmounted notifiers (especially, when it comes to async gaps). Accessing an unmounted notifier can lead to unexpected behavior and exceptions.
Instead, consider always using ref.read to get the latest reference.
For example,
class HomeConsumerState extends ConsumerState<HomeConsumerStatefulWidget> {
Widget build(context) {
return FooWidget(
onChange: (value) async {
// LINT: Assigning notifiers to a variable can lead to accessing unmounted notifiers.
// Try using 'ref.read' in all places.
final value = ref.read<MyNotifier>(counterProvider.notifier);
await ...
value.fn(); // can be unmounted
},
);
}
}
should be rewritten to
class HomeConsumerState extends ConsumerState<HomeConsumerStatefulWidget> {
Widget build(context) {
return FooWidget(
onChange: (value) async {
ref.read<MyNotifier>(counterProvider.notifier).someFn();
await ...
ref.read<MyNotifier>(counterProvider.notifier).fn(); // always the latest state
},
);
}
}
use-ref-and-state-synchronously
Warns when a notifier's ref or state are called past an await point (also known as asynchronous gap).
Using ref or state after an async gap will lead to UnmountedRefException if the notifier is already unmounted.
Try checking for ref.mounted before using ref or state.
class CountNotifier extends _$CountNotifier {
int build() => 0;
Future<void> incrementDelayed() async {
await Future<void>.delayed(const Duration(seconds: 1));
// LINT: Avoid accessing 'ref' or 'state' past an await point without checking if the ref is mounted.
state += 1;
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
Widget build(BuildContext context) => GestureDetector(
child: const Text('show dialog'),
onTap: () => showDialog(
context: context,
builder: (context) => Consumer(
builder: (context, ref, _) => AlertDialog(
actions: [
GestureDetector(
child: const Text('increment and close'),
onTap: () {
ref.read(countProvider.notifier).incrementDelayed();
Navigator.of(context).pop();
},
),
],
),
),
),
);
}
here, modifying the state property will throw an exception. To avoid that, check for ref.mounted first:
class CountNotifier extends _$CountNotifier {
int build() => 0;
Future<void> incrementDelayed() async {
await Future<void>.delayed(const Duration(seconds: 1));
if (ref.mounted) { // Correct, checks for mounted first
state += 1;
}
}
}
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
Widget build(BuildContext context) => GestureDetector(
child: const Text('show dialog'),
onTap: () => showDialog(
context: context,
builder: (context) => Consumer(
builder: (context, ref, _) => AlertDialog(
actions: [
GestureDetector(
child: const Text('increment and close'),
onTap: () {
ref.read(countProvider.notifier).incrementDelayed();
Navigator.of(context).pop();
},
),
],
),
),
),
);
}
avoid-missing-tr-on-strings
Warns when the tr extension method is not called on string literals.
For example,
void fn() {
// LINT: Missing 'tr' invocation on this string literal.
Text('str');
// LINT: Missing 'tr' invocation on this string literal.
Text(key: 1, 'str');
}
should be rewritten to
void fn() {
Text('str'.tr());
Text('str').tr();
Text(key: 1, 'str'.tr());
}
This rule also comes with auto-fix.
What’s next
To learn more about upcoming features, keep an eye on our public roadmap.
And to learn more about our upcoming videos and "Rules of the Week" content, subscribe to our Youtube Channel.
Sharing your feedback
If there is something you miss from DCM right now, want us to make something better, or have any general feedback — join our Discord server! We’d love to hear from you.
