The new lint in Dart 3.2
Dart 3.2 is out yesterday. The official announcement goes into many new things there. What it does not tell us is a new lint they added.
annotate_redeclares
Let’s set the stage first. A new feature is coming in some of the next Dart releases, and it is called ‘extension types’. It was easier to understand when the working title for it was ‘views’, but they did not want to introduce a new keyword, so they renamed it to ‘extension type’.
Anyway, we will be able to use a ‘view’ on a class to only expose some of its interface. This is useful when:
- You do not control the hierarchy yourself. Like if you want to make an
AddOnlyMap
that would wrap aMap
and only expose methods to read or add. Ideally, you wantMap
to implementAddOnlyMap
because inheritance is an intuitive way to add functionality, but you cannot control those built-in types. So you can make a view. - You have a local use case for some of your types where you want a subset of its interface available, but it is not significant enough to introduce that interface into the type hierarchy visible to your clients.
The syntax is the following:
extension type AddOnlyMap<K, V>(Map<K, V> map) {
void addAll(Map<K, V> other) => map.addAll(other);
// All other reading and adding methods.
}
It has nothing to do with just extension
, you can forget it completely while diving into this new subject.
In the first line, you specify what this ‘view’ wraps. In this case, we are wrapping a Map<K, V>
that we call map
and can refer to it by that name when we dispatch the calls.
An AddOnlyMap
is then constructed like below. Note that it only exposes the methods we manually created, like addAll()
:
You can see that the wrapped object is still accessible, so we can still delete entries like this:
aom.map.clear();
We can make it full-proof by making the wrapped object private with the underscore:
extension type AddOnlyMap<K, V>(Map<K, V> _map) {
// ...
But then someone can still cast your wrapper to the original type:
(aom as Map).clear();
All of that could be done earlier by wrapping 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 that results in overhead because this new type exists at runtime, and all methods get forwarded at runtime. In contrast, an extension type only exists at compile time and adds no overhead against just calling the methods on the original object. This is why casting to the original type works. Read more on that in the official discussion.
This feature is available in Dart 3.2 as an experiment. To use it, add--enable-experiment=inline-class
to the Dart command line when you run or build the program.
Now that we set the stage, let’s switch to cats to better explain the new lint because maps were good to explain the idea but not the problem.
Let’s say you have this hierarchy:
class Animal {
void sound() {
// Mute by default.
}
}
class Cat extends Animal {
@override
void sound() {
print('Meow.');
}
void play() {
print('Chomp!');
}
}
Now you want a SoundOnlyCat
. It will make sure your clients will not play with it and not get bitten. Casting it to Animal
would solve this, but you want your clients to always be passed a Cat
and not a Wolf
for their safety, so you must use a view on a Cat
:
extension type SoundOnlyCat(Cat _c) {
void sound() => _c.sound();
}
Next, you get lazy on manually forwarding the methods and just forward everything that was in Animal
. You can do it like this:
extension type SoundOnlyCat(Cat _c) implements Animal {}
It is probably not a good idea in a general case because you may later add something to Animal
that you do not want in SoundOnlyCat
and forget it, but there are still use cases for that mass forwarding.
Next, you want your sound-only-cat to sound differently:
extension type SoundOnlyCat(Cat _c) implements Animal {
void sound() {
print('I want to play.');
}
}
And this is where it gets problematic. Animal
has sound()
, but this new method does not override it but shadows it:
final cat = SoundOnlyCat(Cat());
cat.sound(); // I want to play.
(cat as Cat).sound(); // Meow.
It can even have a different signature:
extension type SoundOnlyCat(Cat _c) implements Animal {
void sound({required bool loud}) {
print('I want to play' + (loud ? '!!!' : '.'));
}
}
final cat = SoundOnlyCat(Cat());
cat.sound(loud: true); // I want to play!!!
(cat as Cat).sound(); // Meow.
So this easily gets messy. If you then rename the method in the extension type, it will no longer shadow the one in Animal
, and you can accidentally call a method you wanted to not be visible. If they have the same signature, you will not know it until clients report bugs to you.
When you shadow anything, you want to make sure you do this on purpose. For this, a new annotation @redeclare
is used. It is defined in meta
package starting at version 1.10.0.
Here comes the new lint annotate_redeclares. It forces you to annotate whenever you redeclare anything on an extension type:
import 'package:meta/meta.dart';
extension type SoundOnlyCat(Cat _c) implements Animal {
@redeclare // OK, or lint without it.
void sound() {
print('I want to play.');
}
}
Then, if a method stops being redeclared, this annotation produces a warning for you.
The Earlier Lints
If you have missed my articles on older lints, read them now:
- The 3 new lints in Dart 2.18.
- The 8 new lints in Dart 2.19.
- The 6 new lints in Dart 3.0.
- The 2 new lints in Dart 3.1.
Enabling the New Lint
Read this article on how to enable those new lints manually.
Alternatively, you can use my package total_lints which enables most of the lints for your project. I use it to avoid repeating the linter configuration for my projects.
Never miss a story, follow my Telegram channel: ainkin_com