New lint in Dart 3.2

Dart 3.2 was released yesterday. In the official announcement It says what’s new. But it doesn’t say anything about the new linter rule.

annotate_redeclares

Let’s start from afar. In one of the next versions there will be a new design called extension type. It was easier to explain when the working title was ‘view‘, but the authors did not want to introduce a new keyword, so they renamed it to ‘extension type‘.

In general, it will be possible to do ‘view‘ on a class to show only part of its interface. This is useful if:

  • You don’t control the hierarchy. For example, you want AddOnlyMapwhich will wrap Map, but will only give access to the read and append methods. It would be ideal if Map expanded the interface AddOnlyMapbecause inheritance is an intuitive way to add functionality, but you don’t have control over the built-in classes. Therefore, you do view.

  • You need to show only part of the interface somewhere, but the case is too small to add the interface to a hierarchy that all your users will see.

Syntax:

extension type AddOnlyMap<K, V>(Map<K, V> map) {
  void addAll(Map<K, V> other) => map.addAll(other);
  // Все другие методы чтения и добавления.
}

This has nothing to do with the usual extensionjust forget about it for a while.

In the first line we write what this ‘view’ wraps. In this example — Map<K, V> entitled map. Using this name, we can then access the object to forward calls.

Now you can create AddOnlyMap like this. Note that only the methods we created are visible, e.g. addAll():

You can also see that the wrapped object is still accessible, so you can do this:

aom.map.clear();

You can make foolproof with a private variable:

extension type AddOnlyMap<K, V>(Map<K, V> _map) {
// ...

But you can still cast:

(aom as Map).clear();

All this could have been done before if you wrapped the object in a new class:

class AddOnlyMap<K, V> {
  final Map<K, V> _map;
  const AddOnlyMap(this._map);

  void addAll(Map<K, V> other) => map.addAll(other);
  // ...
}

But this slows down the program because method forwarding occurs at runtime. In contrast, an extension type exists only at compile time, and forwards will be replaced by calls to wrapping methods on the original object. By the way, that’s why the cast works. Read more about this in official discussion.

This design can be used already in Dart 3.2 as an experiment. To do this, add a flag when building or running:
--enable-experiment=inline-class

Now that we’ve figured out this design, let’s move on to cats to understand lint. Map explained the design well, but not the problem.

For example, there is such a hierarchy:

class Animal {
  void sound() {
    // По умолчанию без звука.
  }
}

class Cat extends Animal {
  @override
  void sound() {
    print('Мяу.');
  }

  void play() {
    print('Кусь!');
  }
}

Now you need SoundOnlyCatto hand the cat over to clients without it biting them. You can cast to Animalbut you want to protect your clients so that they don’t accidentally get Wolfso you need to make a view on Cat:

extension type SoundOnlyCat(Cat _c) {
  void sound() => _c.sound();
}

Then you become too lazy to forward each method separately, and you want to do it for the entire interface at once Animal. This is done like this:

extension type SoundOnlyCat(Cat _c) implements Animal {}

Not the best idea for the general case, because you can add in Animal something that doesn’t belong in SoundOnlyCatand forget about it, but there are still times when it is useful.

Next you will want to SoundOnlyCat made some other sound:

extension type SoundOnlyCat(Cat _c) implements Animal {
  void sound() {
    print('Хочу играть.');
  }
}

And here’s the problem. U Animal have your own sound(), but this new method has nothing to do with it. It hides the old method rather than overriding it:

final cat = SoundOnlyCat(Cat());
cat.sound(); // Хочу играть.
(cat as Cat).sound(); // Мяу.

It may even have a different signature:

extension type SoundOnlyCat(Cat _c) implements Animal {
  void sound({required bool loud}) {
    print('Хочу играть' + (loud ? '!!!' : '.'));
  }
}

final cat = SoundOnlyCat(Cat());
cat.sound(loud: true); // Хочу играть!!!
(cat as Cat).sound(); // Мяу.

It turns out to be a mess. If you rename a method to extension type, it will no longer hide the method from Animal, and you can accidentally call something you didn’t want. If the methods have the same signature, then you will only find out about it from complaints from clients.

When you hide something, you need to be sure that you are hiding it intentionally. An annotation has been added for this purpose. @redeclareshe appeared in the package meta in version 1.10.0.

And now we come to a new lint: annotate_redeclares. It forces us to write this annotation if we hide a method in an extension type:

import 'package:meta/meta.dart';

extension type SoundOnlyCat(Cat _c) implements Animal {
  @redeclare // OK, а без неё -- замечание линтера.
  void sound() {
    print('Хочу играть.');
  }
}

And if this method accidentally stops hiding a class method, the analyzer will issue a warning that the method with this annotation does not override anything.

Older lints

If you missed my previous articles, here they are:

How to connect a new lint

Here I’m writing how to do this manually.

Or you can use my package in your project total_lints, which includes most of the linter rules. I use it to avoid repeating the same configuration between my projects.

Don’t miss my articles, add yourself to the Telegram channel: ainkin_com

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *