Skip to main content

What’s new in DCM 1.40.0

· 18 min read
Dmitry ZhifarskyFounder

Cover

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

This release includes 16 new rules (including 4 rules for Primary constructors); built-in mechanism for switching between different DCM versions without manual installation; support for subcommand aliases for "dcm init baseline" and "dcm run"; baseline for Analyze Widgets and Calculate Metrics commands; improved flow analysis for "widgets-nesting-level" and "number-of-used-widgets" metrics; new "--no-format" mode for "dcm fix" to not run formatter after fixes; and more! 🚀

warning

In the next release we'll update the recommended preset to include new rules. If you use this preset, please be ready to address new warnings.

Let’s go through the highlights of this release (and the full list of changes is in our changelog)!

Regarding the Partial Service Degradation in September​

Before we move to the new features in this release, we would like to say a few words about the partial service degradation in September.

First of all, if you were affected by that, we would like to sincerely apologize. What basically happened is that in attempt to enable Cloudflare anti-bot rules for our website, those same rules were enabled for our backend which resulted in all CLI and CI/CD verification requests going thorough the Cloudflare managed challenge and failing.

Unfortunately, those requests were rejected before reaching our application layer, so all monitoring was silent and showed no errors (we also do not collect any errors from the client for security reasons). Initial testing after the rules were enabled also showed no problems.

This was the longest period of degraded service quality since our launch, and it was completely our fault. We have learned our lesson and hope never to make such a mistake again.

Documentation Updates​

Among many other changes to the docs, there are a few notable changes that you might want to pay closer attention.

First, previously undocumented synthetic issues for ignores and configuration now have their own section. The main difference between regular and synthetic issues is that the latter cannot be enabled or disabled and cannot be ignored, so we hope having them listed helps you quickly understand and resolve any of such issues.

Then, also previously undocumented, comes the difference between two supported syntaxes for rules: yaml list and yaml map. When using the yaml list syntax in a configuration that is used by other configurations as a preset, the documentation now clearly highlights that dependant configs will override any specific rule configuration. However, when using a yaml map for the list of rules, the resulting config will be a merged config. Refer to this section for more info and examples.

And lastly, the Global Configuration and Managing DCM Versions pages were updated to reflect the newly introduced feature to automatically switch between different DCM versions without any script or manual installation.

Automatic Switching Between Different DCM Versions​

We are excited to announce a new feature for one of the frequently requested capabilities: an ability to switch between DCM versions without relinking, using a script and so on.

Prior to DCM 1.40, the dcm_global.yaml version constraint and the DCM Teams account settings were both affecting the installed version of the tool and the tool was showing a mismatched version error in case the installed version didn't match the constraint.

Now, the version constrain in dcm_global.yaml affects the launched DCM version allowing you to have one installed DCM version on your device (e.g., via homebrew) and have different launched versions in different projects without the need to reinstall/relink DCM when switching between them.

Starting from DCM 1.40, if the dcm_global.yaml file is present and has a version constraint below the currently installed version, DCM will automatically download, verify and switch to that configured version. This configuration will be picked up by the IDE, CLI or CI runs. You can also bypass that behavior for CLI by passing the --no-global-version CLI flag which will run the installed version of the tool.

You will also see additional CLI output for when the tool switches/downloads an older version.

Refer to the Global Configuration page for more details.

info

You can still configure the constraint for the installed DCM version by setting it up in your account settings.

Baseline for Metrics and Widgets​

With this release the remaining commands (dcm calculate-metrics and dcm analyze-widgets) are now also supported by baseline.

The only difference with other supported commands is that instead of completely ignoring the baselined metrics or widgets, they are counted as below the threshold. This still allows viewing the metrics/widgets when passing --report-all or generating the HTML report.

In those reports, the baselined metric or widget will have a label indicating that it is baselined.

{
"date": "2026-10-20 07:54:10.382583Z",
"version": "1.40.0",
"sensitivity": "exact",
"commands": {
"analyze-widgets": {
"lib/src/features/home_page.dart": {
"widget-quality-report": ["c3867028fbb58a9bf468914b90f201ec"]
}
},
"metrics": {
"lib/gen/assets.gen.dart": {
"weight-of-class": [
"615f36eae9fcfaa9ed6450c7aef2e770",
"c547cafe620d988c2b3e5b796d019a76",
"5256676438174cf015fa35c8c12b139c",
"21dee899f9bf0ff6393d4fe02c3103b2"
],
"number-of-parameters": [
"e2623b551b4d199fa482cfafa44c7c72",
"77c54e5146b954daa6fdd06b1eb4ddef"
]
}
}
}
}
lib/src/features/home_page.dart (1 widget):
100% HomePage stateless (baselined)
at lib/src/features/home_page.dart:13:1

Number of fields: 0
Number of methods: 1
Number of used widgets (total): 32
HomePage.build method: 88%

Baselined Label Example

New "--no-format" mode for "dcm fix"​

By default, dcm fix also runs the Dart formatter after it applies fixes so there is no formatting issues.

Unfortunately, that was a problem for teams that were not using the formatter and therefore were unable to run dcm fix.

With this release, we are happy to present a new CLI option called no-format which does exactly that: only applies fixes without running the formatter.

The only downside is that it requires also passing --only-rules with strictly one rule so there is no conflicts among fixes and no broken code.

Metric Updates​

widgets-nesting-level​

With this release, the widgets-nesting-level metric supports additional complex cases for calculating widgets nesting. For example,

Widget _chainedBuild2(bool flag) {
Widget widget = InkResponse(onTap: () {});

if (flag) {
widget = Semantics(button: true, child: widget);
} else {
widget = Column(children: [Semantics(button: true, child: widget)]);
}

if (flag) {
return widget;
}

widget = InkResponse(onTap: () {});

return widget;
}

Widget _withSwitch(int value) {
var widget = Center(child: Center(child: Center()));

switch (value) {
case 1:
widget = Center();

case 2:
{
widget = Center(child: widget);
}
}

return widget;
}

Widget _withPatternAssignment() {
var widget = Center(child: Center(child: Center()));

(widget,) = (Center(child: widget),);

return widget;
}

Widget _withFunctionExpressionInvocation(bool flag) {
var widget = () {
return Center(child: Center(child: Center()));
}();

return Center(
child: Center(
child: () {
return Center(child: Center(child: widget));
}(),
),
);
}

Widget _withNested(bool flag) {
var widget = Center(child: Center(child: Center()));

if (flag) {
widget = Badge(child: widget);

return widget;
} else if (flag) {
widget = Center(child: Center(child: widget));
}

return widget;
}

Widget _withElse(bool flag) {
var widget = Center(child: Center(child: Center()));

if (flag) {
// ...
} else {
widget = Center();
}

return widget;
}

all of the cases above (complex if/else, switches, immediately invocated functions, complex conditions with returns, reassignments and so on) are now properly supported.

Additionally, this metric is now calculated for getters and top-level functions that return widgets.

number-of-used-widgets​

This metric is now calculated for getters and top-level functions that return widgets, so you might see new reported entries.

// now shows 5 instead of 0
Widget _withSwitch(int value) {
var widget = Center(child: Center(child: Center()));

switch (value) {
case 1:
widget = Center();

case 2:
{
widget = Center(child: widget);
}
}

return widget;
}

// now shows 2 instead of 0
Widget get widgets =>
Semantics(button: true, child: InkResponse(onTap: () {}));

Rule Updates​

Some of the existing localization-related rules got an updated flow analyzer to detect advanced cases for when the widget is inside ExcludeSemantics or Semantics.

This should eliminate all false positives for when the widget is assigned to a variable that is not immediately used.

For example, for prefer-icon-button-tooltip:

Widget _build(bool flag) {
// highlighted, because `ExcludeSemantics` is used only in one branch
final widget = IconButton(onPressed: () {})I;

if (flag) {
return ExcludeSemantics(child: widget);
}

return widget;
}

Widget _conditionalBuild(bool flag) {
// highlighted, because `ExcludeSemantics` is used only in one branch
Widget widget = IconButton(onPressed: () {});

if (flag) {
widget = ExcludeSemantics(child: widget);
} else {
widget = Column(children: [widget]);
}

return widget;
}

Widget _conditionalBuild(bool flag) {
// not highlighted, all branches use `ExcludeSemantics`
Widget widget = IconButton(onPressed: () {});

if (flag) {
widget = ExcludeSemantics(child: widget);
} else {
widget = Column(children: [ExcludeSemantics(child: widget)]);
}

return widget;
}

Widget _build(bool flag) {
// no longer highlighted even though not initially in `ExcludeSemantics`
final widget = IconButton(onPressed: () {});

return ExcludeSemantics(child: widget);
}

Widget _build(bool flag) {
// works as before
return ExcludeSemantics(child: IconButton(tooltip: ''));
}

New Rules​

info

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.40.0  # Show rules added in 1.40.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.

prefer-initializing-in-declaration​

info

This rule is for Primary constructors and requires Dart 3.13+.

Suggests moving the constructor field initializer to the field declaration.

For example,

class Super(final String _first) {
final String val;

// LINT: This field should be initialized in its declaration.
// Try initializing the field where it is declared.
this : val = _first;
}

here, initializing in the primary constructor body is not necessary as primary constructors come with new syntax that allows using the primary constructor field directly in the initializer:

class Super(final String _first) {
final val = _first;
}

this helps simply the code.

This rule also comes with auto-fix.

avoid-unnecessary-declaring-parameters​

info

This rule is for Primary constructors and requires Dart 3.13+.

Suggests converting a declaring parameter to a regular parameter when it's not used outside of the primary constructor.

For example,

// LINT: This declaring parameter can be converted to a regular parameter as it is not used outside the constructor.
// Try removing the 'final' keyword.
class Primary(final String _value) {
final another = _value;

final String third;

this : third = _value;
}

here, _value is not referenced anywhere outside the primary constructor body and field initializer which makes it eligible to be converted to a regular parameter (by simply removing the final keyword):

class Primary(String _value) {
final another = _value;

final String third;

this : third = _value;
}

class Another(final String _value) {
void fn() {
print(_value);
}
}

This rule also comes with auto-fix.

avoid-skipping-tests ​

Warns when a test file, individual test or test group is skipped.

For example,

// LINT: Avoid adding '@Skip()' to test files.
()
library;

import 'package:test/test.dart';

void main() {
// LINT: Avoid adding 'skip: true' to individual tests or test groups.
test('some test', skip: true, () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
}

here, both the file-level annotation and the skip argument are highlighted.

Ideally, you should avoid having skipped tests at all.

This rule also comes with auto-fix.

avoid-test-in-test-invocation​

Warns when a test is invoked within another test invocation.

Such invocations will not be properly called by the testing framework and should be moved out.

For example,

void main() {
test('some test', () {
// LINT: Avoid declaring tests or test groups inside other tests.
// Try moving this test out.
test('some other test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
});
}

should be rewritten to

void main() {
test('some test', () {
...
});

test('some other test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
}

avoid-test-on​

Warns when a test invocation has testOn.

Incorrectly configured or left over testOn argument results in the test never being invoked, without any indication from the testing framework.

Instead of adding testOn to individual invocations, consider moving such tests to separate files with a dedicated file name and a top-level @TestOn annotation.

For example,

void main() {
// LINT: Avoid adding 'testOn' to individual test cases.
// Consider moving this test to a separate file with a top-level @TestOn() annotation.
test('some test', testOn: 'windows', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
}

here, the rule will highligh the testOn argument.

prefer-test-name-first​

Suggests having the name of a test invocation or test group as its first argument.

For example,

void main() {
// LINT: The name of a test or group should go before the other arguments.
// Try moving the name above other arguments.
test(() {
final array = [1, 2, 3];
expect(array, hasLength(1));
}, 'some test');

// LINT: The name of a test or group should go before the other arguments.
// Try moving the name above other arguments.
group(skip: true, 'some group', () {
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
});
}

should be rewritten to

void main() {
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});

group('some group', skip: true, () {
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
});
}

This rule also comes with auto-fix.

prefer-test-body-last​

Suggests having the body of a test invocation or test group as its last argument.

For example,

void main() {
// LINT: The body of a test or group should go after the other arguments.
// Try moving the body argument below other arguments.
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
}, skip: '');

// LINT: The body of a test or group should go after the other arguments.
// Try moving the body argument below other arguments.
group('some group', () {
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
}, skip: true);
}

should be rewritten to

void main() {
test('some test', skip: '', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});

group('some group', skip: true, () {
test('some test', () {
final array = [1, 2, 3];
expect(array, hasLength(1));
});
});
}

prefer-block-in-switch-case​

Suggests wrapping all switch case statements in a code block.

For example,

void fn(int val) {
switch (val) {
// LINT: Prefer wrapping all switch case statements in a code block.
case 1:
print('hi');
{
final value = 4;
}

// LINT: Prefer wrapping all switch case statements in a code block.
case 2:
print('hi');

case 3:
{
final value = 4;
}
}
}

should be rewritten to

void fn(int val) {
switch (val) {
case 1:
{
print('hi');
{
final value = 4;
}
}

case 2:
{
print('hi');
}

case 3:
{
final value = 4;
}
}
}
warning

This rule is not compatible with avoid-block-in-switch-case.

avoid-block-in-switch-case​

Suggests to avoid wrapping all switch case statements in a code block.

For example,

void fn(int val) {
switch (val) {
// LINT: Avoid wrapping all switch case statements in a code block.
// Try moving all statements out and removing this code block.
case 1:
{
print('hi');
{
final value = 4;
}
}

// LINT: Avoid wrapping all switch case statements in a code block.
// Try moving all statements out and removing this code block.
case 2:
{
print('hi');
}
}
}

should be rewritten to

void fn(int val) {
switch (val) {
case 1:
print('hi');
{
final value = 4;
}

case 2:
print('hi');
}
}

This rule also comes with auto-fix.

avoid-multiple-blocks-in-switch-case​

Suggests to avoid multiple code blocks in a single switch case.

For example,

void fn(int val) {
switch (val) {
// LINT: Avoid multiple code blocks in a single switch case.
case 1:
{
final value = 4;
}
{
print('hi');
}
}
}

should be rewritten to

void fn(int val) {
switch (val) {
case 1:
{
final value = 4;
print('hi');
}
}
}

avoid-legacy-web-import-conditions​

Warns when an import or export condition references a deprecated web library and should be updated to use js_interop instead.

For example,

import 'package:some/package.dart'
// LINT: This library is deprecated and should not be used. Use 'dart:js_interop' instead.
if (dart.library.html) 'package:some/package.dart'
// LINT: This library is deprecated and should not be used. Use 'dart:js_interop' instead.
if (dart.library.js_utils) 'package:some/package.dart'
// LINT: This library is deprecated and should not be used. Use 'dart:js_interop' instead.
if (dart.library.js) 'package:some/package.dart';

should be rewritten to

import 'package:some/package.dart'
if (dart.library.js_interop) 'package:some/package.dart';

This rule also comes with auto-fix.

prefer-primary-constructors​

info

This rule is for Primary constructors and requires Dart 3.13+.

Suggests converting simple classes (with only fields) to primary constructors.

For example,

class MyClass {
final String value;
final int another;

// LINT: Prefer primary constructors for simple classes.
const new(this.another, this.value);
}

class WithStaticField {
static const _field = 'value';

final String value;
final int another;

// LINT: Prefer primary constructors for simple classes.
const new(this.another, this.value);
}

should be rewritten to

class Some {
String? value;
int? another;
}

class Another {
final String value;
final int another;

const new(this.another, this.value);

new named(this.another, this.value);
}

class WithMethods {
final String value;
final int another;

const new(this.another, this.value);

void work() {}
}

This rule also comes with auto-fix.

primary-parameters-ordering​

info

This rule is for Primary constructors and requires Dart 3.13+.

Enforces consistent order for primary constructor parameters.

Supported modifiers (can be used in any order):

  • overridden or protected
  • public or private
  • required
  • super
  • nullable
  • initialized
  • var or final
  • positional or named or optional (when not specified, applies to all parameters)
  • declaring or regular

With the default config:

  • final-declaring
  • var-declaring
  • regular

This rule also comes with auto-fix.

avoid-hardcoded-numbers​

This rule requires configuration in order to highlight any issues.

Warns when a configured class invocation has an argument with a hardcoded number literal.

For example,

analysis_options.yaml
dcm:
rules:
- avoid-hardcoded-numbers:
entries:
- name: Column
args:
- spacing
- name: SomeClass.named
args:
# if there is no "padding" named argument,
# the positional argument with the same name will be checked instead
- padding
Column(
// LINT: Avoid hardcoded numbers. Try using an existing constant or variable.
spacing: 10,
children: [
...
],
);

Column(
// LINT: Avoid hardcoded numbers. Try using an existing constant or variable.
spacing: 10.5,
children: [
...
],
);

should be rewritten to

const _spacing = 10;

Column(
spacing: _spacing,
children: [
...
],
);

prefer-gestures-semantics-role​

Warns when a custom tappable widget is not wrapped in Semantics.

Wrapping custom tappable widgets in Semantics is required to ensure proper role announcement.

For example,

Widget _build(bool flag) {
// LINT: Prefer wrapping custom tappable widgets in 'Semantics' to ensure proper role announcement.
final widget = InkResponse(onTap: () {});

if (flag) {
return Semantics(button: true, child: widget);
}

return widget;
}

Widget _conditionalBuild(bool flag) {
// LINT: Prefer wrapping custom tappable widgets in 'Semantics' to ensure proper role announcement.
Widget widget = InkResponse(onTap: () {});

if (flag) {
widget = Semantics(button: true, child: widget);
} else {
widget = Column(children: [widget]);
}

return widget;
}

should be rewritten to

Widget _build(bool flag) {
final widget = InkResponse(onTap: () {});

return Semantics(button: true, child: widget);
}

Widget _conditionalBuild(bool flag) {
Widget widget = InkResponse(onTap: () {});

if (flag) {
widget = Semantics(button: true, child: widget);
} else {
widget = Column(children: [Semantics(button: true, child: widget)]);
}

return widget;
}

prefer-merge-semantics-composite​

Warns when a Column, Row or Flex widget with the configured children is not wrapped in MergeSemantics.

Without MergeSemantics, screen readers treat those interactive children widgets as separate, fragmented focus stops.

For example,

// LINT: This widget contains the configured interactive widgets and should be wrapped with 'MergeSemantics'.
Row(children: [Text(), Checkbox()]);

// LINT: This widget contains the configured interactive widgets and should be wrapped with 'MergeSemantics'.
Column(children: [Text(), Checkbox()]);

should be rewritten to

MergeSemantics(child: Row(children: [Text(), Checkbox()]));
MergeSemantics(child: Column(children: [Text(), Checkbox()]));

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.