| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:analysis_server/src/session_logger/log_entry.dart'; | |
| import 'package:collection/collection.dart'; | |
| /// An [Equality] for [Message]s. | |
| /// | |
| /// Similar to [DeepCollectionEquality] but with some customizations: | |
| /// | |
| /// - Only supports JSON objects | |
| /// - Supports ignoring specific map keys (applies to all nested maps) | |
| /// - Uses unordered list comparisons for lists that are not made of ints. | |
| /// - Supports conditionally skipping the top level `id` entry. | |
| class MessageEquality implements Equality<Message> { | |
| final _CustomDeepCollectionEquality _recursiveEquality; | |
| new({Set<String> ignoredKeys = const {}}) | |
| : _recursiveEquality = _CustomDeepCollectionEquality(ignoredKeys); | |
| /// If [skipMatchId] is `true`, then the top level `id` field is ignored | |
| /// during comparison. | |
| @override | |
| bool equals(Message a, Message b, {bool skipMatchId = false}) { | |
| if (a.method != b.method) return false; | |
| if (!skipMatchId && a.id != b.id) return false; | |
| return switch (a.method) { | |
| // No method means this is a response, compare the result. | |
| null => _recursiveEquality.equals(a.result, b.result, path: 'result'), | |
| // A method means this is a request, compare the params. | |
| String() => _recursiveEquality.equals(a.params, b.params, path: 'params'), | |
| }; | |
| } | |
| @override | |
| int hash(Message e) { | |
| return _recursiveEquality.hash(e.map); | |
| } | |
| @override | |
| bool isValidKey(Object? o) => o is Message; | |
| } | |
| /// Implementation for checking equality of the `params` and `result` fields | |
| /// of [Message]s. | |
| class _CustomDeepCollectionEquality implements Equality<Object?> { | |
| final Set<String> _ignoredKeys; | |
| late final _orderedListEquality = ListEquality<Object?>(this); | |
| late final _unorderedListEquality = UnorderedIterableEquality<Object?>(this); | |
| new(this._ignoredKeys); | |
| @override | |
| bool equals(Object? e1, Object? e2, {String? path}) { | |
| if (identical(e1, e2)) return true; | |
| if (e1 is Map<String, Object?> && e2 is Map<String, Object?>) { | |
| return _mapEquals(e1, e2, path); | |
| } else if (e1 is List<Object?> && e2 is List<Object?>) { | |
| if (e1.every((entry) => entry is int)) { | |
| return _orderedListEquality.equals(e1, e2); | |
| } | |
| return _unorderedListEquality.equals(e1, e2); | |
| } | |
| return const DefaultEquality().equals(e1, e2); | |
| } | |
| @override | |
| int hash(Object? e) { | |
| if (e is Map<String, Object?>) { | |
| return _mapHash(e); | |
| } else if (e is Iterable<Object?>) { | |
| return _unorderedListEquality.hash(e); | |
| } | |
| return const DefaultEquality().hash(e); | |
| } | |
| @override | |
| bool isValidKey(Object? o) => true; | |
| /// Joins an optional [parentPath] and child [key]. | |
| String _childPath(String? parentPath, String key) => | |
| parentPath != null ? '$parentPath.$key' : key; | |
| /// Checks if [key] is an ignored field when it exists as a child in a path | |
| /// of [parentPath]. | |
| /// | |
| /// Paths can be partial, so an ignored path of 'b.c' will ignore a field | |
| /// at 'a.b.c'. | |
| bool _isIgnoredPath(String? parentPath, String key) { | |
| var actualPath = _childPath(parentPath, key); | |
| return _ignoredKeys.any( | |
| (ignoredKey) => | |
| actualPath == ignoredKey || actualPath.endsWith('.$ignoredKey'), | |
| ); | |
| } | |
| /// Checks if [e1] and [e2] are equal, taking into account any ignored paths. | |
| /// | |
| /// If these maps are not from the top level, [path] is the qualified path to | |
| /// them. | |
| bool _mapEquals( | |
| Map<String, Object?> e1, | |
| Map<String, Object?> e2, | |
| String? path, | |
| ) { | |
| var keys1 = e1.keys.where((key) => !_isIgnoredPath(path, key)).toSet(); | |
| var keys2 = e2.keys.where((key) => !_isIgnoredPath(path, key)).toSet(); | |
| if (keys1.length != keys2.length) return false; | |
| for (var key in keys1) { | |
| if (!keys2.contains(key)) return false; | |
| var childPath = _childPath(path, key); | |
| if (!equals(e1[key], e2[key], path: childPath)) return false; | |
| } | |
| return true; | |
| } | |
| /// A relatively weak but cheap underdered hash, which is important. | |
| /// | |
| /// We cannot use [MapEquality] because it doesn't support ignoring keys. | |
| int _mapHash(Map<String, Object?> e) { | |
| var resultMapHash = 0; | |
| for (var key in e.keys) { | |
| if (_ignoredKeys.contains(key)) continue; | |
| resultMapHash = resultMapHash ^ key.hashCode; | |
| resultMapHash = resultMapHash ^ hash(e[key]); | |
| } | |
| return resultMapHash; | |
| } | |
| } |