Skip to main content

What’s new in DCM for Teams 1.4.0

· 13 min read

Cover

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:

Example

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:

analysis_options.yaml
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 is child).
  • list - a boolean flag indicating whether the prop accepts an array of widgets (default is false).
  • target - a class name, for which the widget should be suggested (default is unset).

Example of a more advanced config:

analysis_options.yaml
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

some_example.dart
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.