What’s new in DCM for Teams 1.4.0
Today we’re excited to announce the release of DCM for Teams 1.4.0! In this version we added a new assist called "Wrap with ...", new command to find unused or missing dependencies, 22 new rules (and 5 of them are for the provider
package 😱) and more!
Let’s go on a quick journey together to explore all the new features!
New command - Check Dependencies
If you want to check whether some dependencies in your projects are missing, under-promoted, over-promoted, or even unused and were unsatisfied with the existing solutions, wait no more! DCM now provides a command dcm check-dependencies
to check project dependencies which is based on the AST resolution (brining correct support for commented code, conditional imports / exports, etc.)
Here is an example of this command output:
Wrap with custom widget assist
Assists can help you automate routine tasks, but one of the most used group of assists that allows you to wrap an existing widget into another one (like "Wrap with Column" or "Wrap with Row") is not extendable with custom options which becomes really inconvenient when you have your own UI-Kit of widgets and want to be able to insert them fast as well.
That's whe DCM now provides a custom configurable assist called "Wrap with ..." that you can fully customize for your needs (but also avoid the assist menu in the IDE to be bloated with different options).
Here is an example for VS Code:
And for IntelliJ / AS:
Config example
To configure this assist, add the assists
entry to the dart_code_metrics
config section:
dart_code_metrics:
assists:
wrap-with:
- WidgetName
- MyOtherWidget
Each wrap-with
list entry also support extended config options:
prop
- the name of the property where the current widget should be passed (default ischild
).list
- a boolean flag indicating whether theprop
accepts an array of widgets (default isfalse
).target
- a class name, for which the widget should be suggested (default is unset).
Example of a more advanced config:
dart_code_metrics:
assists:
wrap-with:
- WidgetName
- MyOtherWidget:
prop: children
list: true
target: MyListWidget
resulting in MyOtherWidget
be only suggested if a selected widget is an instance of MyListWidget
or its subclass. When this option is selected, the following code will be inserted MyListWidget(children: [...])
.
FVM support
FVM is a pretty popular tool that allows you to quickly switch between different Flutter version, but it was not supported by DCM and you needed to pass the pass to the SDK manually both for CLI and the IDE extensions config.
But no more! DCM is not capable of locating .fvm/flutter_sdk
and picking the SDK even when called from a CLI in a project subfolder (DCM will automatically traverse the parent directories in order to find flutter_sdk
).
New rules
Provider
avoid-watch-outside-build. Warns when a context.watch
or context.select
are used outside the widget's build method. watch
is designed to subscribe to changes which is not needed when the value is accessed outside of the build
method.
For example,
class MyHomePage extends StatelessWidget {
const MyHomePage();
void callback(BuildContext context) {
final value = context.watch<String>();
}
Widget build(BuildContext context) {
final value = context.watch<String>();
return ...
}
}
the watch
usage inside the build
method is correct and the widget will rebuild when the value changes. As for the callback, watch
should be replaced with read
instead.
Another example:
class MyHomePage extends StatelessWidget {
const MyHomePage();
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('You have pushed the button this many times:'),
FloatingActionButton(onPressed: () {
context.watch<String>();
}),
],
),
);
}
}
In this case the rule is aware that context.watch
is used inside an inline callback, so this usage will be highlighted as well.
avoid-read-inside-build. Warns when a context.read
is used inside the widget's build method. 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 MyHomePage extends StatelessWidget {
const MyHomePage();
Widget build(BuildContext context) {
final value = context.read<int>();
return Scaffold(
...
);
}
}
the read
usage here is incorrect. watch
or select
should be used instead.
dispose-providers. Warns when a provided class with a dispose
method does not have this method called in the Provider's dispose
callback. Not calling a dispose method can lead to a memory leak.
For example,
Provider(
create: () => DisposableService(),
);
class DisposableService {
void dispose() {}
}
in this case the rule will trigger on the Provider
instantiation since the provided service has a dispose
method, but the dispose
callback is not passed to the Provider
.
prefer-multi-provider. Warns when multiple nested Providers can be replaced with MultiProvider
instead. Replacing with MultiProvider
improves the readability and reduces the widget tree nesting level.
For example,
final provider = Provider(
create: () => RegularService(),
child: Provider(
create: () => RegularService(),
child: ListenableProvider(
create: () => RegularService(),
child: Widget(),
),
),
);
will be suggested to replace with
final multiProvider = MultiProvider(
providers: [
Provider(create: () => RegularService()),
Provider(create: () => RegularService()),
ListenableProvider(create: () => RegularService()),
],
child: Widget(),
);
avoid-instantiating-in-value-provider. Warns when a Provider.value
returns a new instance instead of reusing an existing one.
For example,
Provider.value(
value: RegularService(),
);
should instead provide an existing value, like:
final existingInstance = RegularService();
...
Provider.value(
value: existingInstance,
);
Bloc
check-is-not-closed-after-async-gap. Warns when an async handler does not have isClosed
check before dispatching an event after an async gap. If the event is emitted after the bloc is closed, you will likely get an exception.
For example,
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementEvent>(_handle);
on<CounterDecrementEvent>((event, emit) async {
emit(state + 1);
await Future.delayed(const Duration(seconds: 3));
add(CounterDecrementEvent());
});
}
Future<void> _handle(CounterEvent event, Emitter<int> emit) async {
emit(state + 1);
await Future.delayed(const Duration(seconds: 3));
emit(CounterDecrementEvent());
}
}
here add
and emit
calls after await Future.delayed
are not wrapped with if (!isClosed)
checks leading to the potential runtime exception if the bloc is closed before the Future is resolved.
Here is the correct one:
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementEvent>(_handle);
on<CounterDecrementEvent>((event, emit) async {
emit(state + 1);
await Future.delayed(const Duration(seconds: 3));
if (!isClosed) {
add(CounterDecrementEvent());
}
});
}
Future<void> _handle(CounterEvent event, Emitter<int> emit) async {
emit(state + 1);
await Future.delayed(const Duration(seconds: 3));
if (!isClosed) {
emit(CounterDecrementEvent());
}
}
}
Flutter
avoid-unnecessary-stateful-widgets. Warns when a StatefulWidget
can be converted to a StatelessWidget
. Using a StatelessWidget
where appropriate leads to more compact, concise, and readable code and is more idiomatic.
For example,
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Widget build(BuildContext context) {
return Container();
}
}
this widget does not use any StatefulWidget
features, so can be easily converted to a StatelessWidget
instead.
prefer-action-button-tooltip. Warns when a FloatingActionButton
does not have a tooltip
specified. Adding tooltip is important if you want your users with screen readers to be able to understand what the action is supposed to do.
For example,
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
),
);
}
will be highlighted by the rule. The correct version is:
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
Widget build(BuildContext context) => Scaffold(
floatingActionButton: FloatingActionButton(
tooltip: 'some tooltip',
onPressed: () {},
),
);
}
avoid-inherited-widget-in-initstate. Warns when context.dependOnInheritedWidgetOfExactType
is transitively called from any invocation in initState
. initState
is called only once and any change of the InheritedWidget
won't be properly recalculated.
For example,
class _MyHomePageState<T> extends State<MyHomePage> {
int _counter = 0;
void initState() {
super.initState();
Theme.of(context);
}
}
here Theme.of(context)
should not be called in the initState
.
avoid-late-context. Warns when the context
is used inside a late
field. Using the context
in late
fields might result in an unexpected behavior due to late
fields being initialized lazily.
For example,
class _SomeState extends State<T> {
late final _myState = _create(context);
}
should be refactored to avoid the context
usage.
avoid-state-constructors. Warns when a State
has a constructor with non-empty body. The constructor of a State
object should never contain any logic. That logic should always go into initState
.
For example,
class _MyHomePageState<T> extends State<MyHomePage> {
_MyHomePageState() {
// ...
}
}
should be refactored to
class _MyHomePageState<T> extends State<MyHomePage> {
void initState() {
// ...
}
}
Common
avoid-redundant-pragma-inline. Warns when a @pragma('vm:prefer-inline')
annotation has no effect. The Dart compiler can't inline certain methods, even if they are annotated with @pragma("vm:prefer-inline")
.
Those include methods with a try
block, methods declared async
, async*
, sync*
and certain core library methods.
For example,
class SomeClass {
('vm:prefer-inline')
Future<String> good() {
return Future.value('hello');
}
('vm:prefer-inline')
Future<void> asyncMethod() async {
await good();
}
('vm:prefer-inline')
void methodWithTry() {
try {
// ...
} catch (_) {
print('error');
}
}
}
here @pragma('vm:prefer-inline')
has no effect on asyncMethod
and methodWithTry
.
prefer-unwrapping-future-or. Warns when a FutureOr
is not unwrapped before being used. Awaiting FutureOr
without checking if the returned value is actually a Future
results in unnecessary async gap.
For example,
FutureOr<int> futureOrFunction() async => 1;
Future<void> main() async {
await futureOrFunction();
}
should instead check if futureOrFunction
is a Future
before awaiting it:
FutureOr<int> futureOrFunction() async => 1;
Future<void> main() async {
final futureOr = instance.futureOrFunction();
int result;
if (futureOr is Future) {
result = await futureOr;
} else {
result = futureOr;
}
// Or
final value = futureOr is Future ? await futureOr : futureOr;
}
Although the resulting code is a bit longer, there will be no unnecessary async gap.
avoid-unused-generics. Warns when a function or method declares unused generic types. Sometimes unused generics might appear after the code is refactored, so this rule helps avoid any manual checks.
For example,
class SomeClass {
String unused<T extends num>() {
// ...
}
String multiple<T, U>(U param) {
// ...
}
}
void function<T>() {
return;
}
all these declarations have an unused generic type which can be safely removed.
avoid-unsafe-collection-methods. Warns when first
, last
, single
, firstWhere
, lastWhere
, singleWhere
or []
methods are used on Iterable
or its subclasses. Accessing elements via these methods can result in an exception being thrown at runtime, if there are no element found. To avoid that, use firstOrNull
, lastOrNull
, singleOrNull
, firstWhereOrNull
, lastWhereOrNull
, singleWhereOrNull
or elementAtOrNull
from the collection package instead.
For example,
List<String> someList = [...];
someList.first;
someList.last;
someList.single;
someList.firstWhere(...);
someList.lastWhere(...);
someList.singleWhere(...);
should be refactored to
List<String> someList = [...];
someList.firstOrNull;
someList.lastOrNull;
someList.singleOrNull;
someList.firstWhereOrNull(...);
someList.lastWhereOrNull(...);
someList.singleWhereOrNull(...);
prefer-visible-for-testing-on-members. Warns when the @visibleForTesting
annotation is applied to the class declaration. Applying this annotation to the class declaration does not stop the class members from being accessed from outside of the tests.
Instead, add this annotation to particular class members.
For example,
class SomeClass {
final String someState;
SomeWidget(this.someState);
bool get getter => someState.isEmpty;
void someMethod(String value) {
// ...
}
}
here @visibleForTesting
won't stop the class from being instantiated in non-test code as well as it's members from being accessed.
avoid-banned-types. Allows you to configure types that you want to ban.
With the given config:
dart_code_metrics:
...
rules:
...
- avoid-banned-types:
entries:
- paths: ["some/folder/.*\.dart", "another/folder/.*\.dart"]
deny: ["SomeType"]
message: "Do not use SomeType here."
- paths: ["core/.*\.dart"]
deny: ["CounterBloc"]
message: 'State management should be not used inside "core" folder.'
will trigger on
void function(SomeType someParam) {
// ...
}
since SomeType
is listed as banned.
avoid-unnecessary-negations. Warns when a negation can be simplified.
For example,
void main() {
final someCondition = true;
if (!!someCondition) {} // LINT
if (!true) {} // LINT
}
can be refactored to
void main() {
final someCondition = true;
if (someCondition) {}
if (false) {}
}
avoid-banned-file-names. Allows you to configure file names that you want to ban.
With the given config:
dart_code_metrics:
...
rules:
...
- avoid-banned-file-names:
entries:
- .*example.dart
will trigger on
void main () {
// ...
}
avoid-inverted-boolean-checks. Warns when a condition has an inverted check. Inverted checks are harder to notice due to one symbol !
that completely inverts the condition.
Refactoring the code to be without the inverted check helps other developers to understand it faster.
For example,
if (!(x == 1)) {}
if (!(x != 1)) {}
if (!(x > 1)) {}
if (!(x < 1)) {}
if (!(x >= 1)) {}
if (!(x <= 1)) {}
can be simplified to
if (x != 1) {}
if (x == 1) {}
if (x <= 1) {}
if (x >= 1) {}
if (x < 1) {}
if (x > 1) {}
function-always-returns-null. Warns when a function with nullable return type returns only null
.
For example,
String? function() {
if (value == 2) {
// ...
}
return null;
}
avoid-throw-objects-without-tostring. Warns when a thrown object does not implement toString
. Objects that do not have an implemented toString
method will be represented as "Instance of ..." when serialized.
For example,
void function() {
try {
// ...
} on Object {
throw MyException();
}
}
class MyException {}
here MyException
should have toString
implemented.
New metrics
Number of Implemented Interfaces (NOII). Helps measure the number of interfaces implemented by a class. Too many implemented interfaces indicate a high complexity or entities that are responsible for too many things.
Number of Added Methods (NOAM). Helps measure the number of own class methods. Too many or too few might convince you to refactor the class.
Number of Overridden Methods (NOOM). Helps measure the number of overridden class methods. Too many or too few might convince you to refactor the class.
Number of External Imports (NOEI). The number of external imports in a file. External import is a package:
import. Multiple imports from the same external package count as one external import.
What’s next
Support Dart 3.0 by all rules. Dart 3.0 introduces new syntax that is might not work well with current rules implementation. With the next release we aim to eliminate this inconvenience.
New rules for Dart 3.0 features. New features also bring new ways to make mistakes. Next release will introduce lint rules and assists to help you migrate to Dart 3.0 well prepared!
Move performance improvements. We received a several reports about DCM performance issues on very large codebases. This is a serious problems that we also want to address. There is still some room to make DCM better.
Reworking metrics and reporting. There are still some work that needs to be done to polish new metrics implementation. We also want to add ability to view metric reports directly in the IDE.
New commands. Reworking metric HTML reports unblocked other commands that need an HTML report as well. We hope to add them in the upcoming release to make DCM cover even more use-cases.
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 - there are several ways to share: you can join our Discord server or send us feedback directly from our website.