Skip to main content

Dart 3.0 support in DCM for Teams 1.5.0

· 12 min read

Cover

Today we’re excited to announce the off-schedule release of DCM for Teams 1.5.0! In this version we added Dart 3.0 support to existing rules, 17 new rules for Dart 3.0-only features 🚀 and more!

Let’s go on a quick journey together to explore all the new features!

Existing rules that now support Dart 3.0

avoid-unnecessary-type-assertions. Will now help you spot unnecessary type assertions in if-case statements and pattern matching!

For example,

class Animal {}

class HomeAnimal extends Animal {}

void main() {
final animal = Animal();

if (animal case Animal result) {}
if (animal case Animal()) {}

final value = switch (animal) {
HomeAnimal() => 'good',
Animal() => 'bad',
};
}

here, "if-case" checks are completely unnecessary since animal already has the Animal type. Same goes for the second case in the switch expression.

avoid-unnecessary-type-casts. Will now help you spot unnecessary type casts in if-case statements and pattern matching!

For example,

class Animal {}

class HomeAnimal extends Animal {}

void patterns() {
final animal = Animal();

if (animal case Animal() as HomeAnimal) {}
if (animal case Animal() as Animal) {}
}

the second if-case is completely unnecessary since animal already has the Animal type.

avoid-unrelated-type-assertions. Will now help you spot unrelated type assertions in if-case statements and pattern matching!

For example,

class Animal {}

class NotAnimal {}

void patterns() {
final animal = Animal();

if (animal case NotAnimal result) {}
if (animal case NotAnimal()) {}
}

here, NotAnimal is not a subclass of Animal therefore the if-case checks will never evaluate to true.

avoid-unrelated-type-casts. Will now help you spot unrelated type casts in if-case statements and pattern matching!

For example,

class Animal {}

class HomeAnimal extends Animal {}

class NotAnimal {}

void patterns() {
final animal = Animal();

if (animal case Animal() as NotAnimal) {}
}

here, NotAnimal is not a subclass of Animal therefore the cast pattern will never evaluate to true and throw a runtime exception.

Other rules that got support for new syntax

Here is the list of other existing rules, that now support new if-case and patterns syntax:

  • avoid-banned-types
  • avoid-returning-widgets
  • avoid-similar-names
  • prefer-correct-identifier-length
  • avoid-collapsible-if
  • avoid-dynamic
  • avoid-explicit-type-declaration
  • avoid-non-null-assertion
  • avoid-shadowing
  • avoid-unsafe-collection-methods
  • avoid-iterable-of
  • prefer-trailing-comma
  • unnecessary-trailing-comma

Other existing features

All other existing features (commands, metrics, etc.) are now also support with Dart 3.0!

But if you face any issue with the new release, please let us know!

New rules

Dart 3.0

avoid-one-field-records. Records provide a great way to group data without introducing a class, but a record with only one field acts as unnecessary wrapper which should be avoided (plus, there are currently no use-cases for the one-field records). And in some cases the presence of records with one field may be a sign of an error.

For example,

final record = ('hello',);

can be simplified to

final value = 'hello';

and

(int,) getPoint() => (0,);

is likely a mistake that should be rewritten to

(int, int) getPoint() => (1, 1);

move-records-to-typedefs. Warns when a record type should be moved to a typedef.

Although records allow to group data without introducing a class, when a record type is changed in one place, all other record type declarations that accept the same record require an update as well. This does not scale way, so a good way to solve this problem is to move a repeating record type declaration to a typedef.

For example,

typedef MyDataClass = (String, int);

class MyClass {
MyDataClass getPoint() => ('1', 1);

(String, int) getOtherPoint() => ('1', 1);
}

the getOtherPoint method has the return type that can be safely replaced by MyDataClass.

This rule also accepts configuration allowing you to decide after how many fields in one record the rule should trigger or on how many occurrences.

avoid-bottom-type-in-records. Warns when a record type declaration contains fields with void, Never or Null.

Although the language allows you to add these types to record declarations, the presence of these types inside a record is most likely a bug.

For example,

(Null, int) nullable() => (...);

(void value, Never never) function() => (...);

avoid-nested-records. Warns when a record type declaration contains a nested record type declaration. Nesting multiple records can badly affect readability.

For example,

(int, (int, (int,))) triple() => (...);

(int, (int, (int, (int,)))) quadruple() => (...);

is already too much types, parentheses and commas, but if you take real-world type, it will be even worse.

You can also configure this rule to decide which nesting level you consider acceptable (default is 2).

avoid-function-type-in-records. Warns when a record type declaration contains a function type.

Records are usually treated as a way to group data, not behavior. Consider passing an instance of a class instead.

For example,

(String, String Function()) regular() => ('str', () {});

instead, consider passing the result of the function or replacing the whole record with a class.

class WithBehavior {}

WithBehavior regular() => WithBehavior(...);

(String, String) regular() => ('str', 'some result');

avoid-positional-record-field-access. Warns when a record positional field is accessed via $.

Accessing record positional fields via $1, $2, etc. might look like a great shortcut to write less code, but it also brings readability issues for others who read the code (it's really hard to tell what hides behind $1).

Instead, consider destructuring (final (x, y) = ...) the record first or converting its fields to named fields.

For example,

void function() {
final record = (
'hello',
'world',
);

...

record.$1;
record.$2;
}

can be rewritten to

void function() {
final record = (
first: 'hello',
second: 'world',
);

final first = record.first;
final second = record.second;

final another = (
'hello',
'world',
);

final (first, second) = another;
}

avoid-mixing-named-and-positional-fields. Warns when a record declaration contains both named and positional fields. Mixing positional and named fields adds extra complexity especially when you decide to destructure the record (for named fields you can ignore the initial order) and might be an indicator that record should be replaced with a class as a longer living entity.

For example,

final record = ('hello', hi: 'world');

class MyClass {
(int, {int named}) calculate() => (1, named: 0);
}

can be rewritten to use only named or only positional instead

final record = (first: 'hello', second: 'world');

class MyClass {
(int, int) calculate() => (1, 0);
}

avoid-long-records. Records with high number of fields are difficult to reuse and maintain because they are usually responsible for more than one thing.

Instead, consider creating a class or splitting the record into several ones.

For example,

final record =
('hello', 'world', 'and', 'this', 'is', 'also', 'a field');

can be split to

final record = ('hello', 'world', 'and');
final another = ('this', 'is', 'also', 'a field');

match-positional-field-names-on-assignment. Warns when a positional field name does not match a variable name on destructuring assignment.

Records syntax allows to declare two type of fields: named and positional (which can also have a name). This rule reuses the name of the positional field and requires it to be matched on any destructuring assignment.

This rule can help you reduce the verbosity of named fields, but also ensure the names are consistent with the record type declaration.

For example,

(int,) calculate() => (0,);

(int x, int y) getPoint() => (1, 1);

void main() {
final (value, ) = calculate(); // <-- good, positional field with no name

final (y, x) = getPoint();

final (first, second) = getPoint();
}

instead should be

(int,) calculate() => (0,);

(int x, int y) getPoint() => (1, 1);

void main() {
final (value, ) = calculate(); // <-- good, positional field has no name

final (x, y) = getPoint();
}

avoid-redundant-positional-field-name. Warns when a record positional field has a name.

Record positional field names do not affect code suggestions, but add extra noise with little benefit.

For example,

class MyClass {
(int x, int y) getPoint() => (1, 1);
}

can be converted to named fields

class MyClass {
({int x, int y}) getPointNamed() => (x: 1, y: 1);
}

avoid-nested-switch-expressions. Warns when a switch expression contains another switch expression.

Multiple nested switch expressions can significantly affect readability. Consider avoiding them or moving to separate functions.

avoid-bottom-type-in-patterns. Warns when a pattern contains a void, Never or Null type.

Although the language allows you to add these types to patterns, the presence of these types inside a pattern is most likely a bug.

For example,

final object = WithField('hello');

if (object case Null()) {}
if (object case Never()) {}
if (object case final Null value) {}

final value = switch (object) {
Null() => 'bad',
Never() => 'bad',
};

avoid-explicit-pattern-field-name. Warns when an object pattern has an explicit field name.

Patterns syntax allows you to discard the field name in variable pattern if the name of variable matches the name of the field. This can help reduce the overall noise (and the patterns syntax is pretty verbose).

For example,

const iterable = {...};

if (iterable case Set(firstOrNull: final firstOrNull)) {}
if (iterable case Set(first: final first)) {}

can be rewritten to

const iterable = {...};

if (iterable case Set(:final firstOrNull)) {}
if (iterable case Set(firstOrNull: final alias)) {}

if (iterable case Set(:final first)) {}
if (iterable case Set(first: final alias)) {}

prefer-wildcard-pattern. Warns when a dynamic or Object type is used instead of a wildcard pattern.

Although it's not a mistake to use them, patterns provide a better option to match unknown value called a wildcard pattern _.

For example,

final object = WithField('hello');

final value = switch (object) {
WithField() => 'good',
Object() => 'bad',
dynamic() => 'bad',
};

can use the wildcard pattern instead

final object = WithField('hello');

final value = switch (object) {
WithField() => 'good',
_ => 'good',
};

no-equal-switch-expression-cases. Warns when a switch expression has cases with equal bodies.

Patterns feature also has logical patterns (|| and &&) that help you avoid duplicating logic inside a switch expression.

For example,

class WithField {
final String field;

const WithField(this.field);
}

final object = WithField('hello');

final value = switch (object) {
Object() => 'string literal',
WithField() => 'string literal', // LINT

WithField() => object.field,
(WithField f) => f.field,

WithField(:final field) => field,
(WithField(:final field)) => field, // LINT

Object o => o.toString(),
dynamic o => o.toString(), // LINT
};

can be simplified with help of || to

final value = switch (object) {
Object() || WithField() => 'string literal',

WithField() => object.field,
(WithField f) => f.field,

WithField(:final field) => field,

(Object() || WithField()) && final o => o.toString(),
};

prefer-simpler-patterns-null-check. Warns when a patterns check for non-nullability can be simplified.

For example,

final object = WithField('hello');

if (object.field case != null && final field) {}
if (object.field case final field && != null) {}
if (object.field case != null && final String field) {}
if (object.field case final String field && != null) {}

can be simplified to

final object = WithField('hello');

if (object.field case final String field) {}
if (object.field case final field?) {}
if (object.field case != null) {}
if (object.field case != null || final field) {}

avoid-duplicate-patterns. Warns when a LogicalOrPattern or LogicalAndPattern contains duplicated patterns.

With new syntax and complex conditions it's easy to introduce an unnecessary duplicate in the patterns chain.

For example,

final object = WithField('hello');

if (object case Object() || Object()) {}
if (object case dynamic() && dynamic()) {}

if (object case != null && Object() && != null) {}

if (object case WithField() || AnotherClass() || WithField()) {}

if (object case > 10 || > 10) {}

final value = switch (object) {
!= null && Object() && != null => 'bad',
WithField() || AnotherClass() || WithField() => 'bad',
};

can be simplified to

final object = WithField('hello');

if (object case Object()) {}
if (object case dynamic()) {}

if (object case != null && Object()) {}

if (object case WithField() || AnotherClass()) {}

if (object case > 10) {}

final value = switch (object) {
!= null && Object() => 'good',
WithField() || AnotherClass() => 'good',
};

Common

prefer-returning-conditional-expressions. Warns when several returns inside a function body can be replace with a single conditional expression.

For example,

const _value = 1;

int? anotherOne() {
final value = 1;
if (value == 2) {
return value;
}

return null;
}

can be simplified to

int? anotherOne() {
final value = 1;

return value == 2 ? value : null;
}

Flutter

avoid-stateless-widget-initialized-fields. Warns when a StatelessWidget has an initialized final field.

Non-static initialized fields are usually a sign of a state. Consider converting the widget to StatefulWidget.

class AnotherWidget extends StatelessWidget {
final initialized = <String>{};

late final someField = <String>{};
}

can be rewritten to

class AnotherWidget extends StatelessWidget {
static final initialized = <String>{};

final someField;

const AnotherWidget(this.someField);
}

// Or

class _AnotherWidgetState extends State<AnotherWidget> {
final initialized = <String>{};

late final someField = <String>{};
}

Improved rules

prefer-const-border-radius, avoid-unused-generics, function-always-returns-null, avoid-border-all, avoid-watch-outside-build and prefer-enums-by-name got some false-positives clean up, so time to give them a second chance!

avoid-equal-expressions was significantly improved and now can spot more complex duplicates (uses the same detection logic as avoid-duplicate-patterns).

What’s next

More 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.