Ejemplo
equatable 2.1.0: Give a Dart class value equality with no code generation, so two instances with the same fields are ==, hash the same, and work as Set members and Map keys
Muestra verificada para pub equatable 2.1.0: Give a Dart class value equality with no code generation, so two instances with the same fields are ==, hash the…
sha256:89d2bef982143e36175a1174a40d2ddf617e91bf571cde4cd217db418fbf1a1b
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 1
- Claves de firma que lo compilaron
- 1
Entorno declarado
dart 3 linux x64 dart 3 dart pub
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| dart 3 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · pub@1 |
2026-08-14 |
Caso
HOW- Objetivo
- Give a Dart class value equality with no code generation, so two instances with the same fields are ==, hash the same, and work as Set members and Map keys
- Paquetes
- Símbolos
-
- Equatable
- Equatable.props
- Equatable.stringify
- EquatableMixin
- EquatableConfig
- EquatableConfig.stringify
- copyWith
- hashCode
- Set.contains
- Map.containsKey
- Entorno
- dart 3
- Creado
- 2026-08-14T20:03:00Z
Contrato
- assert two instances with equal fields are == where the same class without Equatable compares by identity, that their hashCodes agree, that a difference in either field is a difference, and that const canonicalization is what makes the unhelped class look right sometimes
- assert equatable generates no copyWith, so a hand-written one returns an equal-but-not-identical instance and carries untouched fields over, and that the usual null-coalescing body cannot clear a nullable field while a private sentinel default plus identical can
- assert a field left out of props is silently dropped from equality: two orders with different line items are ==, hash the same, collapse to one entry in a Set, and overwrite each other as Map keys with no warning anywhere
- assert a List in props is compared element-wise and in order while a Set and a Map prop are order-insensitive, that a nested Equatable composes through its own ==, and that two nums in props compare and hash across int and double
- assert a rebuilt equal instance is found by Set.contains and as a Map key, but mutating a List prop in place after insertion changes the hashCode, leaves the object unfindable and unremovable inside the Set, and lets the Set accept an equal duplicate
- assert copying that Set does not repair it, because a spread literal and Set.of given another Set carry the stale entry across and Map.of does the same for keys, while routing the elements through a plain Iterable or adding them in a loop rehashes and does repair it
- assert runtimeType is compared before props, so a subclass inheriting props verbatim is not == to its parent in either direction, and an inherited hand-written copyWith downcasts it into an unequal parent instance
- assert toString prints the props under asserts and only the type name when EquatableConfig.stringify is false, and that a per-instance stringify overrides the global setting
- assert Equatable is an abstract mixin class in 2.1.0 so it can be mixed into a class that already extends something, while EquatableMixin still works but is deprecated and is not a subtype of Equatable
Archivos
- csx.json
- lib/value.dart
- pubspec.lock
- pubspec.yaml
- test/contract_test.dart
Código fuente
{"case":{"caseId":"case:sha256:95e4bdce11c56096e985df47633988d05b6ad6de7f408ef27da1a6755808c60a","constraints":{"runtime":"dart"},"contract":["assert two instances with equal fields are == where the same class without Equatable compares by identity, that their hashCodes agree, that a difference in either field is a difference, and that const canonicalization is what makes the unhelped class look right sometimes","assert equatable generates no copyWith, so a hand-written one returns an equal-but-not-identical instance and carries untouched fields over, and that the usual null-coalescing body cannot clear a nullable field while a private sentinel default plus identical can","assert a field left out of props is silently dropped from equality: two orders with different line items are ==, hash the same, collapse to one entry in a Set, and overwrite each other as Map keys with no warning anywhere","assert a List in props is compared element-wise and in order while a Set and a Map prop are order-insensitive, that a nested Equatable composes through its own ==, and that two nums in props compare and hash across int and double","assert a rebuilt equal instance is found by Set.contains and as a Map key, but mutating a List prop in place after insertion changes the hashCode, leaves the object unfindable and unremovable inside the Set, and lets the Set accept an equal duplicate","assert copying that Set does not repair it, because a spread literal and Set.of given another Set carry the stale entry across and Map.of does the same for keys, while routing the elements through a plain Iterable or adding them in a loop rehashes and does repair it","assert runtimeType is compared before props, so a subclass inheriting props verbatim is not == to its parent in either direction, and an inherited hand-written copyWith downcasts it into an unequal parent instance","assert toString prints the props under asserts and only the type name when EquatableConfig.stringify is false, and that a per-instance stringify overrides the global setting","assert Equatable is an abstract mixin class in 2.1.0 so it can be mixed into a class that already extends something, while EquatableMixin still works but is deprecated and is not a subtype of Equatable"],"goal":"Give a Dart class value equality with no code generation, so two instances with the same fields are ==, hash the same, and work as Set members and Map keys","kind":"HOW","packages":["pkg:pub/equatable@2.1.0"],"schemaVersion":1,"symbols":["Equatable","Equatable.props","Equatable.stringify","EquatableMixin","EquatableConfig","EquatableConfig.stringify","copyWith","hashCode","Set.contains","Map.containsKey"]},"contractCommand":["dart","test","--reporter=expanded"],"environment":{"arch":"x64","ecosystem":"pub","executionContext":"dart","language":"dart","os":"linux","packageManager":"pub","runtime":"dart","runtimeVersion":"3","schemaVersion":1},"license":"MIT-0","packages":["pkg:pub/equatable@2.1.0"],"schemaVersion":1,"symbols":["Equatable","Equatable.props","Equatable.stringify","EquatableMixin","EquatableConfig","EquatableConfig.stringify","copyWith","hashCode","Set.contains","Map.containsKey"],"verifierAdapter":"pub@1"}
import 'package:equatable/equatable.dart';
/// Dart classes compare by identity. `Money(500, 'USD') == Money(500, 'USD')`
/// is false, because `Object.==` asks "is this the same object" and nothing in
/// the language upgrades that answer for you. seeds/dart-collection-equality
/// raises the same problem from the collection side; this is the object side,
/// and it is worse, because a List at least has a matcher in package:test that
/// papers over it. A value object has nothing: the wrong answer flows straight
/// into `contains`, `indexOf`, Set membership, Map keys, and every `if (a == b)`
/// guard in the codebase.
///
/// The usual fix is code generation — freezed, built_value, dart_mappable — and
/// all three need build_runner, a `part` directive, and a generated file that
/// must be regenerated and committed. equatable is the version that needs none
/// of it: you extend (or, since 2.1.0, mix in) `Equatable` and declare a `props`
/// list. `==` becomes an element-wise comparison of `props` plus a runtimeType
/// check, and `hashCode` is derived from the same list, so the two can never
/// drift apart the way a hand-written pair does.
///
/// What equatable buys you is exactly `==`, `hashCode` and `toString`. It does
/// not generate `copyWith`. That is the honest difference from freezed and the
/// reason this seed writes `copyWith` by hand: the equality half is free, the
/// construction half is not, and the hand-written half has its own trap
/// (see [Customer]).
///
/// The failure mode that makes this worth a sample is that `props` is an
/// ordinary list you maintain by hand. Add a field to the class, forget to add
/// it to `props`, and nothing breaks loudly: the class still compiles, `==`
/// still returns a bool, the tests that compare whole objects still pass — and
/// two genuinely different objects are now equal, collapse into one entry in a
/// Set, and overwrite each other as Map keys. There is no analyzer warning for
/// it. [LooseOrder] is that bug, held still.
///
/// The second trap is that `props` holding a collection is compared *deeply*.
/// equatable's `objectsEquals` dispatches on the runtime type: Sets go through
/// `setEquals` (order-insensitive), Maps through `mapEquals`, other Iterables
/// through `iterableEquals` (order-sensitive), nested Equatables through their
/// own `==`, and two `num`s through `==` even across int/double. That is more
/// than `==` on a List gives you and less than you get if you never put the
/// list in `props` at all — so the entire behaviour hinges on one line you have
/// to remember to edit.
///
/// The third trap is that `final` on a List field freezes the reference, not
/// the contents. equatable's hash is computed from the list's contents, so
/// mutating the list in place after the object has been filed in a Set moves
/// its hash out from under the table and the object becomes unfindable while
/// still sitting inside it — and the Set will then happily accept a freshly
/// built object that is `==` to the one already in there, so a Set ends up
/// holding two equal elements.
///
/// Measured on Dart 3.13.0, and the reverse of what this seed first asserted:
/// copying that Set does not repair it. `{...aSet}`, `Set.of(aSet)` and
/// `LinkedHashSet()..addAll(aSet)` all carry the stale entry across, and
/// `Map.of(aMap)` and `{...aMap}` do the same for keys. Routing the elements
/// through a plain Iterable first — `Set.of(aSet.toList())`, `Set.of(aSet
/// .where(...))`, or a `for` loop calling `add` — does rehash and does repair
/// it. Same source, same elements, opposite result, decided by whether the
/// argument is still a Set. So "just copy the set" is not the fix; not mutating
/// a field that is in `props` is.
/// The baseline, with no help: two of these with identical fields are two
/// unrelated objects as far as `==` is concerned.
class PlainMoney {
const PlainMoney(this.amountMinor, this.currency);
final int amountMinor;
final String currency;
}
/// The same class as a value object. `props` lists both fields, so both
/// participate in `==` and in `hashCode`.
class Money extends Equatable {
const Money(this.amountMinor, this.currency);
final int amountMinor;
final String currency;
/// equatable does not generate this. Written by hand, it is unremarkable
/// here because neither field is nullable — `??` is enough. [Customer] is
/// where that stops being true.
Money copyWith({int? amountMinor, String? currency}) => Money(
amountMinor ?? this.amountMinor,
currency ?? this.currency,
);
@override
List<Object?> get props => [amountMinor, currency];
}
/// A subclass that adds no state and reuses the inherited `props`. It is still
/// never equal to its parent, because `Equatable.==` compares `runtimeType`
/// before it looks at `props`. Worth knowing before you reach for subclassing
/// to model a variant, and worth knowing before you write a sealed hierarchy
/// where one case wraps another.
class TaxedMoney extends Money {
const TaxedMoney(super.amountMinor, super.currency);
}
/// The correct version: the nested List is in `props`, so equatable compares it
/// element by element and hashes it by content.
class Order extends Equatable {
const Order(this.id, this.lineItems);
final String id;
final List<String> lineItems;
Order copyWith({String? id, List<String>? lineItems}) =>
Order(id ?? this.id, lineItems ?? this.lineItems);
@override
List<Object?> get props => [id, lineItems];
}
/// The bug. Identical fields to [Order], one line different: `lineItems` never
/// made it into `props`. Every order with the same id is now the same order, in
/// `==`, in `hashCode`, in a Set, and as a Map key. Nothing warns.
class LooseOrder extends Equatable {
const LooseOrder(this.id, this.lineItems);
final String id;
final List<String> lineItems;
@override
List<Object?> get props => [id];
}
/// The sentinel type for [Customer.copyWithClearing]. It is a private class so
/// that no caller outside this library can construct one and collide with it.
/// `const Object()` looks like it would do, and does not: const instances are
/// canonicalized, so every `const Object()` in the program is the *same*
/// object, and a caller passing one would be read as "argument omitted".
class _Unset {
const _Unset();
}
const _Unset _unset = _Unset();
/// A value object with a nullable field, which is where a hand-written
/// `copyWith` goes wrong. This is the piece freezed generates and equatable
/// does not, so it is the piece you own.
class Customer extends Equatable {
const Customer({required this.id, this.nickname});
final String id;
final String? nickname;
/// The `??` version everyone writes first. It cannot distinguish "the caller
/// omitted nickname" from "the caller passed null on purpose", because both
/// arrive as `null`. So this method can set a nickname and change a nickname
/// and can never clear one: `copyWith(nickname: null)` returns an object
/// equal to the original, and the call reads as if it worked.
Customer copyWith({String? id, String? nickname}) => Customer(
id: id ?? this.id,
nickname: nickname ?? this.nickname,
);
/// The fix, without code generation: default the parameters to a private
/// sentinel and test with `identical`, so an explicit `null` is a value like
/// any other. freezed generates this shape; here you write it.
Customer copyWithClearing({
Object? id = _unset,
Object? nickname = _unset,
}) =>
Customer(
id: identical(id, _unset) ? this.id : id! as String,
nickname:
identical(nickname, _unset) ? this.nickname : nickname as String?,
);
@override
List<Object?> get props => [id, nickname];
}
/// Collections other than List in `props`. equatable picks the comparison from
/// the runtime type, so the Set field is order-insensitive and the Map field is
/// key-order-insensitive without any extra declaration.
class Tagged extends Equatable {
const Tagged(this.name, this.tags, this.meta);
final String name;
final Set<String> tags;
final Map<String, int> meta;
@override
List<Object?> get props => [name, tags, meta];
}
/// Composition: a value object holding another value object. equatable checks
/// for a nested Equatable before it checks for a collection, so the inner
/// object's own `==` is used and the nesting composes to any depth.
class LineItem extends Equatable {
const LineItem(this.sku, this.price);
final String sku;
final Money price;
@override
List<Object?> get props => [sku, price];
}
/// A `num` field. equatable special-cases num-to-num comparison (added in
/// 2.0.7) so that `1` and `1.0` in `props` compare the way Dart's own `==`
/// compares them, rather than being split apart by the runtimeType fallback
/// that catches every other pair of unrelated types.
class Quantity extends Equatable {
const Quantity(this.value);
final num value;
@override
List<Object?> get props => [value];
}
/// A hierarchy that already uses `extends`, which is the case `EquatableMixin`
/// existed for.
abstract class Failure {
const Failure();
String get code;
}
/// The 2.1.0 spelling. `Equatable` is declared `abstract mixin class`, so it
/// can be mixed into a class that already extends something. This is the
/// replacement for `EquatableMixin`, which 2.1.0 deprecates.
class ApiFailure extends Failure with Equatable {
const ApiFailure(this.code, this.status);
@override
final String code;
final int status;
@override
List<Object?> get props => [code, status];
/// Opt this one class into printing its props, so the contract can assert
/// `toString` without depending on whether asserts are enabled.
@override
bool? get stringify => true;
}
/// The pre-2.1.0 spelling, kept here because it is what every tutorial written
/// before mid-2026 shows. It still compiles and still gives value equality, but
/// it is deprecated, and it is not a subtype of `Equatable` — so a codebase
/// migrating class by class cannot use `is Equatable` as the test for "does
/// this have value equality yet".
// ignore: deprecated_member_use
class LegacyFailure extends Failure with EquatableMixin {
LegacyFailure(this.code, this.status);
@override
final String code;
final int status;
@override
List<Object?> get props => [code, status];
}
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19"
url: "https://pub.dev"
source: hosted
version: "105.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9"
url: "https://pub.dev"
source: hosted
version: "14.1.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
url: "https://pub.dev"
source: hosted
version: "1.31.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
url: "https://pub.dev"
source: hosted
version: "0.7.13"
test_core:
dependency: transitive
description:
name: test_core
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
url: "https://pub.dev"
source: hosted
version: "0.6.19"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.13.0 <4.0.0"
name: csx_freezed_style_equality
description: Value equality for Dart classes without code generation, and the props list that silently drops a field.
publish_to: none
environment:
sdk: ^3.13.0
dependencies:
# equatable is the dependency-light, no-codegen answer: two transitive
# packages (collection, meta), no build_runner, no generated part file.
# 2.1.0 is the newest published stable; 3.0.0-dev.1 exists but is a
# prerelease and pub will not select it without an explicit prerelease
# constraint. The alternatives all failed the "no code generation" bar:
# freezed, built_value and dart_mappable each require build_runner and a
# generated .g.dart / .freezed.dart part, which cannot be shipped as
# hand-written source in this seed.
equatable: 2.1.0
dev_dependencies:
test: 1.31.2
import 'dart:collection';
import 'package:csx_freezed_style_equality/value.dart';
import 'package:equatable/equatable.dart';
import 'package:test/test.dart';
void main() {
test('value equality replaces identity, and hashCode is derived from props',
() {
// The baseline: same two fields, no Equatable, and `==` answers the only
// question Object knows how to answer.
expect(PlainMoney(500, 'USD') == PlainMoney(500, 'USD'), isFalse);
final a = Money(500, 'USD');
final b = Money(500, 'USD');
expect(identical(a, b), isFalse);
expect(a == b, isTrue);
expect(a.hashCode, equals(b.hashCode));
expect(a == a, isTrue);
// Both fields are in props, so a difference in either is a difference.
expect(a == Money(501, 'USD'), isFalse);
expect(a == Money(500, 'EUR'), isFalse);
expect(a.hashCode == Money(501, 'USD').hashCode, isFalse);
expect(a.hashCode == Money(500, 'EUR').hashCode, isFalse);
// hashCode comes from the same props list as `==`, which is the point:
// hand-written pairs drift when a field is added to one and not the other.
expect(a.props, equals(<Object?>[500, 'USD']));
// The one case where the unhelped class looks right, and the reason people
// think identity is sometimes value equality: const canonicalizes, so both
// literals are the same object. Nothing about PlainMoney changed.
expect(identical(const PlainMoney(500, 'USD'), const PlainMoney(500, 'USD')),
isTrue);
expect(const PlainMoney(500, 'USD') == const PlainMoney(500, 'USD'), isTrue);
});
test('copyWith is yours to write; `??` cannot express "set this to null"', () {
final original = Money(500, 'USD');
// equatable generates no copyWith. Written by hand, an empty call must
// still produce a new object that is equal to the old one.
final copy = original.copyWith();
expect(identical(original, copy), isFalse);
expect(copy == original, isTrue);
expect(copy.hashCode, equals(original.hashCode));
final changed = original.copyWith(amountMinor: 750);
expect(changed == original, isFalse);
expect(changed.amountMinor, equals(750));
expect(changed.currency, equals('USD'));
expect(original.amountMinor, equals(500));
// Now the nullable field, which is where the hand-written version breaks.
// `nickname ?? this.nickname` reads an omitted argument and an explicit
// null identically, so the clear is silently a no-op.
final named = Customer(id: 'c1', nickname: 'Ada');
final notCleared = named.copyWith(nickname: null);
expect(notCleared.nickname, equals('Ada'));
expect(notCleared == named, isTrue);
expect(identical(notCleared, named), isFalse);
// A sentinel default plus `identical` distinguishes the two cases. This is
// the shape freezed generates for you.
final cleared = named.copyWithClearing(nickname: null);
expect(cleared.nickname, isNull);
expect(cleared == named, isFalse);
expect(named.copyWithClearing() == named, isTrue);
expect(named.copyWithClearing(nickname: 'Grace').nickname, equals('Grace'));
expect(named.copyWithClearing(id: 'c2').nickname, equals('Ada'));
});
test('a field left out of props is silently dropped from equality', () {
// Both fields declared, both fields in props: the orders differ.
final full = Order('o1', ['sku-a', 'sku-b']);
final fullOther = Order('o1', ['sku-a', 'sku-c']);
expect(full == fullOther, isFalse);
// The same class with one line removed from props. Nothing else differs,
// nothing warns, and two different orders are now the same order.
final loose = LooseOrder('o1', ['sku-a', 'sku-b']);
final looseOther = LooseOrder('o1', ['sku-a', 'sku-c']);
expect(loose == looseOther, isTrue);
expect(loose.hashCode, equals(looseOther.hashCode));
// The fields really are different; only equality has stopped looking.
expect(loose.lineItems, isNot(equals(looseOther.lineItems)));
// Which is how the data disappears. A Set keeps the first and drops the
// second, and a Map keeps the first key with the second value.
final deduped = {loose, looseOther};
expect(deduped.length, equals(1));
expect(identical(deduped.single, loose), isTrue);
expect(deduped.single.lineItems, equals(['sku-a', 'sku-b']));
final byOrder = {loose: 'first', looseOther: 'second'};
expect(byOrder.length, equals(1));
expect(byOrder[loose], equals('second'));
expect(byOrder[looseOther], equals('second'));
expect(identical(byOrder.keys.single, loose), isTrue);
});
test('collections in props are compared deeply, by their own kind', () {
// Dart itself does not do this: two Lists with the same contents are not
// ==. Everything below is equatable's dispatch, not the language's.
expect(['sku-a'] == ['sku-a'], isFalse);
// A List prop is compared element-wise and in order.
expect(Order('o1', ['a', 'b']) == Order('o1', ['a', 'b']), isTrue);
expect(Order('o1', ['a', 'b']).hashCode,
equals(Order('o1', ['a', 'b']).hashCode));
expect(Order('o1', ['a', 'b']) == Order('o1', ['b', 'a']), isFalse);
expect(Order('o1', ['a', 'b']).hashCode == Order('o1', ['b', 'a']).hashCode,
isFalse);
expect(Order('o1', ['a']) == Order('o1', ['a', 'b']), isFalse);
expect(Order('o1', []) == Order('o1', []), isTrue);
// A Set prop and a Map prop are not order-sensitive, because equatable
// dispatches on the runtime type of each prop rather than treating
// everything as an ordered Iterable.
final t1 = Tagged('x', {'red', 'blue'}, {'a': 1, 'b': 2});
final t2 = Tagged('x', {'blue', 'red'}, {'b': 2, 'a': 1});
expect(t1 == t2, isTrue);
expect(t1.hashCode, equals(t2.hashCode));
expect(t1 == Tagged('x', {'red'}, {'a': 1, 'b': 2}), isFalse);
expect(t1 == Tagged('x', {'red', 'blue'}, {'a': 1, 'b': 3}), isFalse);
expect(t1 == Tagged('x', {'red', 'green'}, {'a': 1, 'b': 2}), isFalse);
// A nested Equatable is checked before the collection branches, so value
// objects compose to any depth.
expect(LineItem('s', Money(500, 'USD')) == LineItem('s', Money(500, 'USD')),
isTrue);
expect(LineItem('s', Money(500, 'USD')) == LineItem('s', Money(501, 'USD')),
isFalse);
expect(LineItem('s', Money(500, 'USD')).hashCode,
equals(LineItem('s', Money(500, 'USD')).hashCode));
// Two nums are compared as nums, so int and double meet the way Dart's own
// `==` makes them meet rather than being split by the runtimeType fallback.
expect(1 == 1.0, isTrue);
expect(Quantity(1) == Quantity(1.0), isTrue);
expect(Quantity(1).hashCode, equals(Quantity(1.0).hashCode));
expect(Quantity(1) == Quantity(2), isFalse);
});
test('Sets and Map keys work by value, until a props field mutates in place',
() {
// This is what the equality was for: a rebuilt key finds the stored entry.
final prices = {Money(500, 'USD'), Money(500, 'USD'), Money(750, 'USD')};
expect(prices.length, equals(2));
expect(prices.contains(Money(500, 'USD')), isTrue);
final cache = <Money, String>{Money(500, 'USD'): 'five'};
expect(cache[Money(500, 'USD')], equals('five'));
expect(cache.containsKey(Money(500, 'USD')), isTrue);
expect(cache[Money(750, 'USD')], isNull);
// `final List<String> lineItems` freezes the reference, not the contents.
// equatable hashes the contents, so mutating the list moves the object's
// hash after the Set has already filed it under the old one.
final order = Order('o1', ['sku-a']);
final orders = {order};
expect(orders.contains(order), isTrue);
final hashBefore = order.hashCode;
order.lineItems.add('sku-b');
expect(order.hashCode == hashBefore, isFalse);
// The object is in the Set and the Set cannot find it, and cannot remove
// it. Not a collision: the lookup hashes to a bucket it is not filed in.
expect(orders.contains(order), isFalse);
expect(orders.length, equals(1));
expect(identical(orders.first, order), isTrue);
expect(orders.remove(order), isFalse);
expect(orders.length, equals(1));
// Worse than a missed lookup: nothing can dedupe against an entry that
// cannot be found, so the Set accepts an equal object and ends up holding
// two elements that are == to each other with the same hashCode.
final duplicate = Order('o1', ['sku-a', 'sku-b']);
expect(duplicate == order, isTrue);
expect(duplicate.hashCode, equals(order.hashCode));
orders.add(duplicate);
expect(orders.length, equals(2));
expect(orders.first == orders.last, isTrue);
expect(orders.first.hashCode, equals(orders.last.hashCode));
// A Map key goes the same way: the entry is still there and unreachable.
final keyed = Order('o2', ['a']);
final byOrder = <Order, String>{keyed: 'stored'};
expect(byOrder[keyed], equals('stored'));
keyed.lineItems.add('b');
expect(byOrder[keyed], isNull);
expect(byOrder.containsKey(keyed), isFalse);
expect(byOrder.length, equals(1));
expect(identical(byOrder.keys.single, keyed), isTrue);
});
test('copying that Set does not rehash it, so the copy is broken too', () {
final order = Order('o1', ['sku-a']);
final orders = {order};
order.lineItems.add('sku-b');
expect(orders.contains(order), isFalse);
// MEASURED on Dart 3.13.0, and the reverse of the obvious repair: a spread
// literal, Set.of() and addAll() given another Set all carry the stale
// entry across unchanged, so the copy is exactly as broken as the original.
expect(<Order>{...orders}.contains(order), isFalse);
expect(Set<Order>.of(orders).contains(order), isFalse);
expect((LinkedHashSet<Order>()..addAll(orders)).contains(order), isFalse);
// Putting an ordinary Iterable in between does rehash, because then the
// elements go in one at a time. Same source, same elements, opposite
// answer, decided by whether the argument is still a Set.
final healed = Set<Order>.of(orders.toList());
expect(healed.contains(order), isTrue);
expect(Set<Order>.of(orders.where((_) => true)).contains(order), isTrue);
final looped = <Order>{};
for (final o in orders) {
looped.add(o);
}
expect(looped.contains(order), isTrue);
// The healed copy is the control: it refuses the duplicate the stale one
// accepts, on the same value.
orders.add(Order('o1', ['sku-a', 'sku-b']));
expect(orders.length, equals(2));
healed.add(Order('o1', ['sku-a', 'sku-b']));
expect(healed.length, equals(1));
// Maps copy the same damage, and a fresh equal key becomes a second entry.
final keyed = Order('o2', ['a']);
final byOrder = <Order, String>{keyed: 'stored'};
keyed.lineItems.add('b');
expect(Map<Order, String>.of(byOrder)[keyed], isNull);
expect(<Order, String>{...byOrder}[keyed], isNull);
final mapCopy = Map<Order, String>.of(byOrder)
..[Order('o2', ['a', 'b'])] = 'again';
expect(mapCopy.length, equals(2));
});
test('runtimeType is compared before props, so a subclass never equals its '
'parent', () {
final base = Money(500, 'USD');
final taxed = TaxedMoney(500, 'USD');
// Same props, inherited verbatim.
expect(taxed.props, equals(base.props));
expect(taxed is Money, isTrue);
// And still not equal, in either direction.
expect(base == taxed, isFalse);
expect(taxed == base, isFalse);
expect(base.hashCode == taxed.hashCode, isFalse);
expect({base, taxed}.length, equals(2));
// Two of the subclass are equal to each other, so the class is not broken.
expect(TaxedMoney(500, 'USD') == taxed, isTrue);
// The hand-written copyWith compounds it. Inherited verbatim, its body
// still constructs a Money, so copying a TaxedMoney silently downcasts and
// the copy is not equal to what it copied. freezed generates a copyWith
// per class; a hand-written one has to be overridden per class too.
final copied = taxed.copyWith();
expect(copied.runtimeType, equals(Money));
expect(copied == taxed, isFalse);
});
test('toString prints props under asserts and hides them without', () {
// Reading the getter latches the value, so save it before touching it.
final ambient = EquatableConfig.stringify;
addTearDown(() => EquatableConfig.stringify = ambient);
// Measured under `dart test`, which runs with asserts enabled: the default
// is true. A release build flips it, so the same log line that showed the
// values in development prints the bare type name in production.
expect(ambient, isTrue);
expect(Money(500, 'USD').toString(), equals('Money(500, USD)'));
EquatableConfig.stringify = false;
expect(Money(500, 'USD').toString(), equals('Money'));
expect(Order('o1', ['a']).toString(), equals('Order'));
// A per-instance `stringify` overrides the global either way, which is how
// you keep one type readable in release.
expect(ApiFailure('E_TIMEOUT', 504).toString(),
equals('ApiFailure(E_TIMEOUT, 504)'));
EquatableConfig.stringify = true;
expect(Order('o1', ['a', 'b']).toString(), equals('Order(o1, [a, b])'));
});
test('Equatable is a mixin class in 2.1.0 and EquatableMixin is deprecated',
() {
// `with Equatable` is the 2.1.0 spelling, and it is what lets a class that
// already extends something get value equality without code generation.
final a = ApiFailure('E_TIMEOUT', 504);
final b = ApiFailure('E_TIMEOUT', 504);
expect(identical(a, b), isFalse);
expect(a == b, isTrue);
expect(a.hashCode, equals(b.hashCode));
expect(a == ApiFailure('E_TIMEOUT', 503), isFalse);
expect(a is Failure, isTrue);
expect(a is Equatable, isTrue);
// The pre-2.1.0 spelling still gives value equality.
final legacy = LegacyFailure('E_TIMEOUT', 504);
expect(legacy == LegacyFailure('E_TIMEOUT', 504), isTrue);
expect(legacy.hashCode, equals(LegacyFailure('E_TIMEOUT', 504).hashCode));
// But it is not a subtype of Equatable, so `is Equatable` is not a valid
// "has this class been migrated yet" check during a partial migration.
expect(legacy is Equatable, isFalse);
// ignore: deprecated_member_use
expect(legacy is EquatableMixin, isTrue);
// Two classes are never equal to each other whatever they mix in.
expect(a == legacy, isFalse);
expect(legacy == a, isFalse);
});
}