Skip to content

prefer_overriding_parent_equality

v0.4.0WarningFixConfigurableCollection & Type

Flags a concrete subclass that does not override both == and hashCode when an ancestor overrides them. The inherited equality ignores whatever the child added, so two children that differ compare equal.

PremiumAccount inherits Account’s equality, which compares id only. Two premium accounts on different tiers are now indistinguishable — to ==, to Set, and to any state comparison that rebuilds on change:

class Account {
const Account(this.id);
final String id;
@override
bool operator ==(Object other) => other is Account && id == other.id;
@override
int get hashCode => id.hashCode;
}
class PremiumAccount extends Account {
const PremiumAccount(super.id, this.tier);
final String tier;
}
class PremiumAccount extends Account {
const PremiumAccount(super.id, this.tier);
final String tier;
@override
bool operator ==(Object other) =>
other is PremiumAccount && id == other.id && tier == other.tier;
@override
int get hashCode => Object.hash(id, tier);
}

Overriding == without hashCode is the same bug wearing a disguise: the objects compare equal but hash differently, so a Set or Map keeps both. Each half is reported on its own:

// Don't — == is overridden, hashCode is not
class PremiumAccount extends Account {
const PremiumAccount(super.id, this.tier);
final String tier;
@override
bool operator ==(Object other) =>
other is PremiumAccount && id == other.id && tier == other.tier;
}

A parent with no equality of its own is not reported

Section titled “A parent with no equality of its own is not reported”

The rule fires only when an ancestor overrides both == and hashCode. Plain inheritance is left alone:

// No warning — Point does not override equality
class Point {
const Point(this.x);
final int x;
}
class Point3D extends Point {
const Point3D(super.x, this.z);
final int z;
}

Abstract classes are skipped too — the concrete subclass is where the override belongs.

Widgets and test doubles are never reported

Section titled “Widgets and test doubles are never reported”

Three types are exempt by default, each because its identity is deliberately not its fields:

Type Why Where it is declared
Widget Identity is runtimeType + key, which element reuse depends on. One comparing fields would break canUpdate package:flutter
Mock Identity is the instance, which is what verify() matches on. One comparing fields would break verification package:mocktail, package:mockito
Fake Implements an interface it does not hold the data for, so it inherits that interface’s == with no fields to compare package:test_api (re-exported by mocktail)

Without these, every StatelessWidget in a Flutter project and every mock in its test tree would report.

The match is by supertype, so your own widget and mock base classes are covered without naming each one. Reach for additional_ignored_types rather than an exclude: ['test/**'] path: the exemption is a fact about those base classes, not about a directory, and a path exclusion would also hide a real equality bug in a test fixture or DTO.

Fields are not counted. Any concrete subclass of an equality-overriding parent is reported, including one that adds only behaviour and no state. Such a class inherits the parent’s equality correctly, so silence it with // ignore: many_lints/prefer_overriding_parent_equality or re-declare the parent’s implementation.

Both halves must be present on the ancestor. A parent that overrides == but not hashCode does not trigger the rule on its children — though it is a bug in its own right, which the SDK’s hash_and_equals catches.

Each default is pinned to its declaring package. Mock and Fake are ordinary English words, so a domain class of the same name cannot silently exempt its whole subtree. Names you add through additional_ignored_types are matched by bare name, since a type declared in your own package has no package: URI to pin against.

Setting ignored_types replaces the defaults. Widgets and mocks start reporting again unless you re-list them; use additional_ignored_types to extend instead.

See also: Dart operator == and hashCode | Dart lint: hash_and_equals

This rule is in the opinionated preset, so it is on with preset: opinionated, or by name:

many_lints.yaml
rules:
prefer_overriding_parent_equality: true

To turn it off again:

many_lints.yaml
rules:
prefer_overriding_parent_equality: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
rules:
prefer_overriding_parent_equality:
additional_ignored_types: [Entity]
Option Type Default Description
ignored_types list of strings [Widget, Mock, Fake] Replaces the set of types whose subtypes are never reported. Setting it drops the defaults, so widgets and mocks report again
additional_ignored_types list of strings [] Extends the default set instead of replacing it, so Widget, Mock and Fake stay excluded

Types are matched by supertype: naming a base class covers everything that extends or implements it.