What’s new in DCM 1.30.0
Today we’re excited to announce the release of DCM 1.30.0!
This release includes 22 new rules, improvements in ~55 existing rules, baseline for code quality commands (e.g. dcm check-unused-code
), a new flag to update the existing baseline, support for different levels of baseline sensitivity, more rules in Free and Starter plans, updated "recommended" preset, better support for baselined issues in DCM Dashboards, optimizations for DCM Dashboards uploaded metadata, and more!
❗️ With the next release we plan to discontinue all DCM versions prior to 1.24.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)
Dashboards Updates
Removal of the "--with-details" Flag And Detailed Views
With this release, the --with-details
flag of the dcm run
command no longer includes detailed issues and metric violations into the uploaded metadata.
This change was made because we received a lot of feedback regarding general usability of the details view for issues and metrics on the project page in DCM Dashboards.
Those detailed views have been replaced with new summary views (that match the output of dcm init lints-preview
and dcm init metrics-preview
) and these views do not require additional metadata (such as messages, file path hashes and lines, etc.).
If this experiment with new views is successful, we'll also delete all collected metadata for detailed issues and metrics.
Changes in Uploaded Metadata
On top of the removal of the detailed issues and metrics (which also significantly reduced the payload size of the uploaded metadata), in this release we updated the collected metadata for configuration.
It now includes some common options such as ignorable
, effort
, default threshold for metrics and so on (but as before, does not include any sensitive information and does not include files paths/source code).
{
"config": {
"rules": {
"use-setstate-synchronously": {
"severity": "warning",
"ignorable": true,
"hasIncludes": false,
"hasExcludes": false
}
},
"metics": {
"tight-class-cohesion": {
"threshold": 0.5,
"effort": 10
},
}
},
}
As before you can always preview the uploaded metadata by adding --preview-upload
flag.
Better Support for Baseline
The Overview and Open Issues Overtime sections now have a new toggle to switch from the open issues to baselined issues:
Plus, each project now has a new separate tab for baselined issues:
We hope these changes make working with baselines issues in DCM Dashboards even better!
Presets Update
We’ve updated the recommended preset to better reflect real-world usage and reduce noise:
- ✅ Some of the newly introduced rules from recent releases are now included by default.
- ❌ Removed
prefer-trailing-comma
andunnecessary-trailing-comma
.
If you're using extends: recommended
, these changes will automatically apply.
Expanded Pricing Plans
We’ve expanded what’s available on both Free and Starter plans:
- Free plan: Now includes 23 additional rules (80 rules in total).
- Starter plan: Expanded with 26 additional rules (213 rules in total).
This update brings more linting coverage to smaller teams and solo developers. The newly included rules are available immediately and follow the same update cycle as other plans.
IDE Extensions Improvements
While we are on the topic of pricing plans, both VS Code and IntelliJ plugins now give instant visual feedback when a rule isn’t available in your current pricing plan.
This is great as you’ll immediately know if a rule in your config is outside your plan’s scope, making it easier to troubleshoot and adjust without diving into docs.
Baseline
Flag to Update Existing Baseline
If you already have a baseline in your project and want to update it from time-to-time without adding new issues, this flag does exactly that.
dcm init baseline --analyze --update .
However, keep in mind to have the baseline updated correctly, use the same flag (e.g. various commands) as in the initial run (without the flag).
Flag to Configure Baseline Sensitivity
With this release we are also introducing a new option called --type
that helps you configure the baseline sensitivity.
By default, baseline takes the exact issue area and the analyzer won't show that issue in the IDE or CLI unless its exact area changes. While this approach is quite useful in cases when you have a lot of issues and don't want to address them all on any file change, some teams prefer addressing baseline issues faster.
That's why the --type
option supports 3 values: exact
, line
, and file
. The exact
gives you the same baseline as before this release, line
invalidates any issue on a line change and file
invalidates any issue on any containing file change.
dcm init baseline --analyze --type=file .
We hope this new feature (on top of the support for additional DCM commands) helps you integrate the tool into existing projects even faster.
Support for Additional Commands
We are exited to present one of the long awaited features for the baseline: support for additional commands, not just dcm analyze
.
Baseline now supports 7 more commands (and we plan to extend this number to all available commands in the future releases):
dcm check-unused-code
dcm check-unused-files
dcm check-code-duplication
dcm check-unused-l10n
dcm check-dependencies
dcm analyze-assets
dcm check-exports-completeness
And the generated file has an updated structure:
{
"date": "2025-07-08 13:29:52.546749Z",
"version": "1.30.0",
"sensitivity": "exact",
"commands": {
"analyze": {
"lib/src/features/about_me/about_me.dart": {
"prefer-dedicated-media-query-methods": [
"8bf8854bebe108183caeb845c7676ae4"
],
},
},
"unused-code": {
"lib/gen/assets.gen.dart": {
"unused-code-issue": [
"7bf6ed78cae287618b2a11146b1ae46e",
],
}
},
}
}
As for the CLI command, it now uses the same flags as dcm run
(you can get a list of all available flags by running dcm init baseline -h
):
dcm init baseline --analyze --unused-code .
If you are still using the old baseline format, the new implementation is backward compatible so you don't need to update your baseline after upgrading to DCM 1.30.0.
Windows Line Endings
Starting from this release, the baseline (with --type=exact
or --type=line
) no longer requires configuring the line endings for Windows devices.
New Command for Previewing Metric Values
Same as for the lint rules, but now for metrics dcm init metrics-preview
runs all available metrics against the provided folder and shows summaries for each metric:
This command supports various additional flags to filter and configure the list of metrics and also the same list of output formats as dcm init lints-preview
.
Here is the analysis_options
output example:
Since dcm init preview
only runs lint rules, it was renamed to dcm init lints-preview
with the introduction of this new command for metrics.
New Rules
DCM 1.30.0 brings 22 new rules across common Dart patterns, Flutter-specific best practices, Provider usage, and even Pubspec hygiene. Each rule is linked to documentation for deeper reference and exploring how to fix this issue for good code.
Common
prefer-pushing-conditional-expressions
suggests pushing conditional expressions into arguments when both branches call the same constructor or method.
void fn(int left, int right) {
// LINT: The inner expressions differ only by one argument.
// Consider using this conditional expression inside that argument.
final value = left == right ? SomeClass(1, 'hello') : SomeClass(2, 'hello');
// LINT: The inner expressions differ only by one argument.
// Consider using this conditional expression inside that argument.
final value = left == right
? OtherClass(value: 1, other: 'hello')
: OtherClass(value: 2, other: 'hello');
}
avoid-suspicious-global-reference
warns when a subclass or extension refers to a global identifier that has the same name as a member in its superclass. Dart resolves the global one unless this.
or super.
is used explicitly, which can lead to subtle bugs.
const value = 'hi';
class A {
final value = 0;
void work() {
value;
}
}
class C extends A {
void another() {
// LINT: This identifier refers to a global declaration, but the parent class has a field or method with the same name.
// Consider checking for the missing 'this' or 'super'.
value;
}
}
class D {
String get override => 'A.override';
String debug() => 'Debug: $override';
}
extension on D {
// LINT: This identifier refers to a global declaration, but the parent class has a field or method with the same name.
// Consider checking for the missing 'this' or 'super'.
String extDebug() => 'Debug: $override';
}
pass-correct-accepted-type
warns when an argument is passed to a parameter or field annotated with @AcceptedTypes()
but the value’s type does not match any of the allowed types. Helps avoid unintentional runtime mismatches when using Object
.
void fn() {
// LINT: This type does not match any of the @AcceptedTypes types.
// Try passing another expression or updating the annotation.
fn(1);
// LINT: This type does not match any of the @AcceptedTypes types.
// Try passing another expression or updating the annotation.
SomeClass(false);
}
void fn(({String, bool}) Object value) {}
class SomeClass {
({String, int})
final Object field;
const SomeClass(this.field);
}
avoid-unreachable-for-loop
warns when a for
loop is placed inside an if
statement whose condition guarantees the loop will never run. This typically signals a logic mistake or dead code.
void fn(List<int> arr) {
// LINT: This condition makes the inner for loop unreachable.
// Try updating the condition or removing the whole if statement.
if (arr.isEmpty) {
for (final item in arr) {
// ...
}
}
// LINT: This condition makes the inner for loop unreachable.
// Try updating the condition or removing the whole if statement.
if (arr.length == 0) {
arr.forEach((_) {
// ...
});
}
}
avoid-unnecessary-length-check
warns when a collection length check is used before a loop, even though the loop itself won’t run if the collection is empty. The condition can safely be removed.
void fn(List<int> arr) {
// LINT: This length check is unnecessary because the inner loop will not be executed when there are no elements.
// Consider removing this length check.
if (arr.isNotEmpty) {
for (final item in arr) {
// ...
}
}
// LINT: This length check is unnecessary because the inner loop will not be executed when there are no elements.
// Consider removing this length check.
if (arr.length != 0) {
arr.forEach((_) {
// ...
});
}
}
prefer-specifying-future-value-type
Warns when a nullable value is passed to Future.value
while the expected type is non-nullable. This can lead to runtime exceptions unless the type argument is made nullable.
void fn(int? nullable) {
// LINT: This Future can throw at runtime because the provided value is nullable, but the expected type is not.
// Try specifying the type argument as nullable or passing a non-nullable value.
final int value = Future.value(nullable);
// LINT: This Future can throw at runtime because the provided value is nullable, but the expected type is not.
// Try specifying the type argument as nullable or passing a non-nullable value.
final value = Future<int>.value(nullable);
}
avoid-default-tostring
warns when toString()
is called on a class that doesn't override it. This leads to unhelpful output like Instance of SomeClass
which is rarely intentional.
void fn(SomeClass some) {
// LINT: The class does not implement 'toString' and will give no meaningful output.
// Try implementing 'toString' or replacing with another value.
some.toString();
// LINT: The class does not implement 'toString' and will give no meaningful output.
// Try implementing 'toString' or replacing with another value.
print('$some');
// LINT: The class does not implement 'toString' and will give no meaningful output.
// Try implementing 'toString' or replacing with another value.
[some].toString();
}
class SomeClass {}
avoid-stream-tostring
warns when toString()
is called on a Stream
or when a Stream
is used in string interpolation. This results in unhelpful output like Instance of '_ControllerStream'
and is rarely intended.
dart showLineNumbers
void fn() {
final myStream = Stream.value([1, 2, 3]);
// LINT: Avoid calling 'toString' on 'Stream'.
print(myStream.toString());
// LINT: Avoid calling 'toString' on 'Stream'.
print('hello $myStream');
}
prefer-null-aware-elements
suggests using null-aware collection elements (?element
) instead of if (x != null)
checks when building maps or lists. This makes the code more concise and readable.
void fn(int? x, Some? input) {
final map = {
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (x != null) "key1": x,
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (x != null) x: "",
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (x != null) x: x,
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (input != null) "key": input.value,
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (input != null) input.value: "value",
};
final list = [
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (input != null) input,
// LINT: Prefer null-aware elements (?element) instead of checking for a potential null value.
if (input != null) input.value,
];
}
avoid-unnecessary-null-aware-elements
warns when a null-aware element (?element
) is used unnecessarily—such as wrapping a constant null value. These cases can be safely removed for clarity.
void fn(int? x) {
final list = [
?x,
// LINT: This null-aware element is unnecessary. Try removing it.
?null,
];
}
avoid-unnecessary-statements
warns when a statement does nothing or has no effect and can be removed. These often occur after early returns or conditionals that serve no functional purpose.
const _value = 1;
void someFunction() {
someOtherFunction();
// LINT: This statement is unnecessary. Try removing it.
if (_value == 2) return;
}
avoid-unnecessary-nullable-fields
warns when a field is marked as nullable (?
) but always has a default value or is always initialized, making the nullability redundant.
class SomeClass {
// LINT: Declared field type is unnecessary marked as nullable. Try removing '?'.
final String? value;
const SomeClass({this.value = '1'});
const SomeClass.another({this.value = '2'});
}
class SomeOtherClass {
// LINT: Declared field type is unnecessary marked as nullable. Try removing '?'.
final String? value;
const SomeOtherClass(String val) : value = val;
}
avoid-banned-exports
warns when an export matches a configured denylist. This rule is fully customizable via entries
and helps enforce architectural or layering boundaries in your codebase.
// LINT: This export is not allowed
// (Do not reexport Flutter Material Design library!).
export 'package:flutter/material.dart';
This rule comes with multiple config including:
- Set
paths
(can be a regular expression, optional) to configure the list of paths the entry will apply to. If unset, applies to all files. - Set
exclude-paths
(can be a regular expression, optional) to configure the list of paths the entry will not apply to. - Set
deny
(can be a regular expression) to configure the list of exports to ban. - Set
message
to configure a user-facing message for each issue created from this config entry. - Set
severity
(optional) to override default severity for the given entry.
dart_code_metrics:
rules:
- avoid-banned-exports:
entries:
- paths: ['some/folder/.*\.dart', 'another/folder/.*\.dart']
deny: ['package:flutter/material.dart']
message: 'Do not reexport Flutter Material Design library!'
severity: error
max-imports
warns when a file contains more imports than the configured limit. This helps reduce file complexity and encourages better separation of concerns.
// LINT: The number of imports in this file exceeds the configured threshold.
// Try reducing the number of imports.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:another/another.dart';
The max number is also configurable:
dart_code_metrics:
rules:
- max-imports:
max-number: 10
avoid-bitwise-operators-with-booleans
warns when bitwise operators (&
, |
) are used in boolean expressions. Use logical operators (&&
, ||
) instead to ensure correct short-circuiting behavior.
void fn() {
var x = true;
// LINT: Avoid bitwise operators with boolean expression. Try using a logical operator instead.
if (x & false) {
// ...
}
// LINT: Avoid bitwise operators with boolean expression. Try using a logical operator instead.
if (x | false) {
// ...
}
}
You can also set assignment ignorance to the rule config:
dart_code_metrics:
rules:
- avoid-bitwise-operators-with-booleans:
ignore-assignments: true
prefer-compound-assignment-operators
suggests replacing standard assignments like x = x + y
with compound operators like x += y
to improve clarity and conciseness.
void fn(num x) {
// LINT: Prefer compound assignment operators over regular assignments.
x = x + 3;
// LINT: Prefer compound assignment operators over regular assignments.
x = x / 3;
// LINT: Prefer compound assignment operators over regular assignments.
x = x * 3;
// LINT: Prefer compound assignment operators over regular assignments.
x = x - 3;
}
avoid-deprecated-usage
warns when code references elements marked with @deprecated
or @Deprecated(...)
. This helps you gradually migrate away from deprecated APIs, especially when using features like baseline.
void fn(SomeClass value) {
// LINT: 'Parent' is deprecated and should not be used.
final instance = SomeClass();
}
class SomeClass {}
Flutter
prefer-single-setstate
suggests combining multiple consecutive setState
calls into a single one to keep all rebuild logic in one place.
class _SomeState extends State<StatefulWidget> {
void someMethod() {
// ...
setState(() {
// some logic
});
// LINT: Prefer calling 'setState' only once.
// Try moving all callback logic to the previous invocation.
setState(() {
// another
});
}
}
avoid-mounted-in-setstate
warns when mounted
is checked inside a setState
callback. This check comes too late and can lead to exceptions—always check mounted
before calling setState
.
void someMethod() {
setState(() {
// LINT: Avoid checking for 'mounted' inside the 'setState' callback.
// Try moving this check out.
if (context.mounted) {
// ...
}
});
}
Provider
prefer-immutable-selector-value
warns when a Selector
or Selector2
returns a mutable object. Returning mutable values can cause missed or incorrect widget rebuilds. Always return immutable values instead.
// LINT: Prefer returning immutable values from selector to avoid skipped or incorrect rebuilds.
Selector(selector: (context, object) => SomeClass());
// LINT: Prefer returning immutable values from selector to avoid skipped or incorrect rebuilds.
Selector2(selector: (context, object, another) => SomeClass());
class SomeClass {}
prefer-nullable-provider-types
warns when context.watch
, context.read
, or Provider.of
uses a non-nullable type. Prefer nullable types to handle cases where the provider value may be missing.
// LINT: Prefer nullable provider types to always handle cases when the provided value is not available.
final value = context.watch<String>();
// LINT: Prefer nullable provider types to always handle cases when the provided value is not available.
final anotherValue = context.read<int>();
// LINT: Prefer nullable provider types to always handle cases when the provided value is not available.
Provider.of<String>(context);
Pub
prefer-commenting-pubspec-ignores
warns when # ignore:
comments in pubspec.yaml
are left without any explanation. Adding a short reason improves project maintainability and avoids confusion for others.
dev_dependencies:
# ignore: prefer-caret-version-syntax // LINT Prefer adding a description to the pubspec ignores.
lints: any
# ignore: some-other-rule // LINT Prefer adding a description to the pubspec ignores.
screenshots:
- description: Some description
path: some/path.txt
Note that this rule doesn't trigger on global # ignore_for_file:
comments.
New Configuration Options
Several rules has receive new config options that makes them even more flexible to address your specific needs. Here is a short list but to learn more click and go to documentation to learn more.
ignore-typedefs
forprefer-match-file-name
ignore-visible-for-testing
forprefer-single-declaration-per-file
exclude-paths
forbanned-usage
exclude-paths
foravoid-banned-types
exclude-paths
foravoid-banned-names
exclude-paths
foravoid-banned-imports
exclude-paths
foravoid-banned-annotations
ignore-visible-for-testing
forprefer-single-widget-per-file
ignore-int
forprefer-number-format
Improved Rules
More than 55 existing rules have been enhanced in this release with better precision, reduced false positives, and improved edge case handling.
These updates affect popular rules like avoid-unsafe-collection-methods
, prefer-match-file-name
, dispose-class-fields
, and many more.
To see the full list of improvements, check the 1.30.0 changelog.
What’s next
To learn more about upcoming features, keep an eye on our public roadmap. We have exciting plans to expand the DCM Dashboards, introduce additional rule configuration options, and continue improving the performance of all DCM commands.
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.