What’s new in DCM 1.15.0
Today we’re excited to announce the release of DCM 1.15.0!
This release includes extension types support for all existing rules and features, 13 new rules (4 for extension types), 30 new quick fixes (some of them are available via dcm fix
), Checkstyle format for all available commands, UX improvements and more. 🚀
Let's go through the highlights of this release!
Extension types support
Extension types are now fully supported by all existing rules and commands! 🔥
This includes supporting various new cases for 24 existing rule (e.g. avoid-passing-self-as-argument
, avoid-future-tostring
avoid-collection-methods-with-unrelated-types
and other).
Configuration
To help you with setting DCM up correctly, this release includes another config validation improvement, but this time it's for the top-level DCM config entries (e.g. rules
, rules-exclude
, etc.).
Here is how it looks in the IDE:
General command improvements
Checkstyle
To level up integration with BitBucket, all existing commands (including dcm run
) now support the Checkstyle
output format.
Previously, only dcm analyze
and dcm calculate-metrics
provided support for this format.
Relative path by default
All DCM commands now use relative paths for output (e.g. in warning locations) by default since all modern IDEs and terminals support relative paths just fine, but switching to relative paths makes the output easier to read for large projects with many folders.
Before:
After:
You can switch back to absolute paths by passing --absolute-path
(or -a
) CLI option to any of the commands.
Unused code improvements
While we are still working on taking the unused code IDE integration out of the beta version, this release includes two small improvements to how unused code is calculated.
Now, assigning a value to a variable is not considered as usage in some contexts (e.g. it's still a usage if it's part of a return statement).
class WithMutableVariables {
int value = 0;
int? _another = 1;
}
int mutate(WithMutableVariables input) {
input.value += 1; // This assignment does not mean that the `value` is used
return input._another ??= 5; // This one caches `5` after the first invocation
}
Additionally, some collection methods are now also not considered as usage.
For example,
class WithCollection {
final localItems = <String>{};
}
void insertItem(WithCollection input) {
input.localItems.add(''); // Adding a string is not a sign of usage
}
here, the localItems
field is never used to get its value, and therefore just adding new elements to it is not considered as usage.
Exports completeness improvements
With this release, all parameters marked @visibleForTesting
are now excluded from the public API graph and are not required to be exported.
Additionally, all exported declarations marked @internal
are now reported, since exporting them clearly violates the idea of the @internal
annotation.
To fix this warning, you can either make those declarations private or hide them explicitly using the hide
combinator of the export
directive.
const topLevel = 'value';
class PublicDeclaration {
...
}
export 'other_file.dart' hide topLevel;
IDEs
To improve the user experience of the "New Version" notification in both VS Code and IntelliJ, there is now a new "Open Changelog" button to help you get to our changelog faster.
You can now also toggle the visibility of issues from baseline using two new IDE commands: Show/Hide Issues From Baseline.
It works similarly to the config option that was introduced in the previous release, but now you can assign a shortcut to it. Plus, these commands automatically restart the analysis server.
Rule updates
New config options for "member-ordering"
Before this release, the member-ordering
rule worked only for class declarations, but now you can enable it for other declarations (extensions, mixins, enums and extension types).
dart_code_metrics:
...
rules:
...
- member-ordering:
ignore-mixins: true
ignore-extensions: true
ignore-extension-types: true
ignore-enums: true
Support for new declarations is disabled by default to avoid potentially overwhelming number of warnings after updating to this version.
New quick fixes
With this release, 30 existing rules now have a quick fix:
- avoid-banned-imports
- avoid-banned-annotations
- avoid-duplicate-test-assertions
- avoid-map-keys-contains
- avoid-nullable-tostring
- prefer-addition-subtraction-assignments
- prefer-moving-to-variable
- avoid-importing-entrypoint-exports
- avoid-duplicate-map-keys
- prefer-explicit-type-arguments
- prefer-both-inlining-annotations
- avoid-cascade-after-if-null
- prefer-parentheses-with-if-null
- prefer-returning-conditional-expressions
- prefer-correct-error-name
- move-variable-outside-iteration
- avoid-negated-conditions
- prefer-specific-cases-first
- enum-constants-ordering
- avoid-equal-expressions
- avoid-duplicate-mixins
- avoid-duplicate-cascades
- move-records-to-typedefs
- prefer-define-hero-tag
- prefer-action-button-tooltip
- prefer-widget-private-members
- prefer-text-rich
- avoid-async-callback-in-fake-async
- avoid-bloc-public-methods
- prefer-correct-bloc-provider
Not all new quick fixes are supported by dcm fix
as some of them need your decision (e.g. whether a duplicate can be safely removed or is actually a bug).
You can find the updated list of quick fixes that are not supported by dcm fix
here.
New rules
Flutter
avoid-unnecessary-gesture-detector. Warns when a GestureDetector
widget has no event handlers.
Gesture detectors with no handlers have no additional functionality and can be safely removed (or updated to have at least one handler).
For example,
SomeWidget(
...
child: GestureDetector(
child: ChildWidget(
...
),
),
)
can be replaced with
SomeWidget(
...
child: ChildWidget(
...
),
)
avoid-missing-controller. Warns when a TextFormField
, TextField
or EditableText
does not have at least one way to get the updated value.
Not providing a controller or not getting an updated value in a callback leads to the widget state not being updated when the user updates the field.
For example,
TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
initialValue: '',
)
has no way to get the updated value and should be rewritten to
TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
controller: _stateController,
)
// OR
TextFormField(
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
onChanged: (newValue) {
_value = newValue,
},
)
Common
avoid-returning-void. Suggests calling functions or methods with a void
return type separately from the return statement.
Returning void
invocations can be confusing for code reviewers and introduce unnecessary changes if the return type changes.
For example,
void someFunction() {
return someVoidFunction();
}
Future<void> asyncFn() async {
return someVoidFunction();
}
void someVoidFunction() {
...
}
here, both return
statements will be highlighted by the rule.
prefer-private-extension-type-field. Warns when an extension type has a public representation field.
Public representation fields can be accessed directly, which may lead to potential mistakes. Consider exposing only the required properties or methods.
For example,
extension type ET(String value) {
bool get isNotEmpty => ... // custom implementation
}
here, calling et.isNotEmpty
and et.value.isNotEmpty
can lead to potentially different results.
To avoid that, prefer making the representation field private
extension type ET(String _value) {
bool get isNotEmpty => ...
}
avoid-empty-spread. Warns when a collection literal in spread (...[]
) has no elements.
For example,
void fn(bool flag) {
final another = [
...<String>[],
if (flag) ...<String>[],
];
}
here, both these spreads are empty and can be safely removed.
avoid-renaming-representation-getters. Warns when an extension type exposes a member that renames an existing representation field member.
Renaming an existing member from the representation type can lead to confusing results.
For example,
extension type ET(String _value) implements String {
bool get isEmpty => _value.isNotEmpty;
bool get isNotEmpty => _value.isEmpty;
}
here, isEmpty
and isNotEmpty
are inverted which is not something an external user of the extension type expects. But this also can a typo.
So the correct version is
extension type ET(String _value) implements String {
bool get isEmpty => _value.isEmpty;
bool get isNotEmpty => _value.isNotEmpty;
}
Additionally, consider adding the @redeclare
annotation as it helps showing that the redeclaration is intentional.
avoid-missing-completer-stack-trace. Warns when Completer.completeError
is called without providing a stack trace.
By default, completeError gets an empty stack trace. Consider passing StackTrace.current
or Error(...).stackTrace
to the invocation.
import 'dart:async';
void fn() {
final completer = Completer();
completer.completeError('foo');
Completer().completeError('foo');
}
should be rewritten to
import 'dart:async';
void fn() {
final completer = Completer();
completer.completeError('foo', StackTrace.current);
// or if the Error object is present
completer.completeError('foo', error.stackTrace);
}
avoid-casting-to-extension-type. Warns when an expression is cast to an extension type.
Casting to an extension type can throw at runtime, whereas instantiating it is safe and you'll get a hint about the representation field type mismatch.
For example,
void fn(String input) {
final value1 = input as ET;
final value2 = input as ET1;
}
extension type const ET(String s) {}
extension type const ET1(int v) {}
here, even though the first cast is correct, the second one throws at runtime because of mismatching types of representation fields.
So the correct approach is to instantiate the extension type instead
void fn(String input) {
final value = ET(input);
}
extension type const ET(String s) {}
which instantly shows any potential type mismatch.
avoid-nested-extension-types. Warns when the representation field of an extension type is also an extension type.
As there is no limit on extension types created from other extension types, this rules helps with avoiding deep nesting.
avoid-slow-collection-methods. Warns when an invocation is a slow sync* invocation.
Using sync*
invocations can result in a ~2 times slower code compared to other approaches.
Additionally, this rule highlights .expand()
usages as replacing them with a spread gives ~3 times performance difference.
void fn() {
const iterable = [null, 2, 3, 4, 5, 6, 7, 8, 9];
iterable.whereNotNull();
}
here, whereNotNull
from package:collection has sync* implementation and can be replaced with whereType
void fn() {
const iterable = [null, 2, 3, 4, 5, 6, 7, 8, 9];
iterable.whereType<int>();
}
prefer-overriding-parent-equality. Warns when a parent class implements hashCode
and ==
, but the child class does not implement them.
Not providing an implementation in the child class can lead to tricky bugs where equality checks don't work as expected.
For example,
class SomeClass {
int get hashCode => 1;
bool operator ==(Object other) {
return super == other;
}
}
class AnotherClass extends SomeClass {
final String value;
AnotherClass(this.value);
}
here, using instances of AnotherClass
in contexts that rely on hashCode
(e.g. hash maps) can give unexpected results.
avoid-unnecessary-collections. Warns when a collection literal can be replaced by its first and only element.
For example,
Future<void> fn() async {
await Future.wait([future]);
await Future.any([future]);
_array.addAll([value]);
_array.addAll({value});
Stream.fromFutures([future]);
Stream.fromIterable([value]);
set.addAll([value]);
set.containsAll([value]);
}
can be simplified to
Future<void> fn() async {
await future;
_array.add(value);
Stream.fromFuture(future);
Stream.value(value);
set.add(value);
set.contains(value);
}
avoid-unknown-pragma. Warns when the @pragma
annotation has an unknown value.
Given that pragmas accept a string value, it's relatively easy to make a typo. This rule guards against that.
('vm:prefer-inlined')
Future<String> fn() {
...
}
here, vm:prefer-inlined
is a non-existing annotation and the correct version is vm:prefer-inline
.
What’s next
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.
New command to analyze project structure. This command will help you visualize and analyze how different files / folders / packages depend on each other. Such analysis can help you create better boundaries between different modules of your apps and make the code more maintainable. On top of that, the command will allow validation of the project structure including cases when a file is expected to be present, but it as actually missing.
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!