| // ignore_for_file: public_member_api_docs, sort_constructors_first |
| // Copyright (c) 2024, 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 'dart:convert'; |
| |
| import 'package:meta/meta.dart'; |
| |
| import 'canonicalization_context.dart'; |
| import 'constant.dart'; |
| import 'definition.dart'; |
| import 'helper.dart'; |
| import 'loading_unit.dart'; |
| import 'metadata.dart'; |
| import 'reference.dart'; |
| import 'serialization_context.dart'; |
| import 'syntax.g.dart'; |
| |
| /// Holds all information recorded during compilation. |
| /// |
| /// Associate [Definition]s annotated with `@RecordUse()` from `package:meta` |
| /// with their corresponding recorded usages. |
| /// |
| /// The definition annotated with `@RecordUse()` must be inside the `lib/` |
| /// directory of the package. If the definition is a member of a class (e.g. a |
| /// static method), the class must be in the `lib/` directory. |
| /// |
| /// The class uses a normalized JSON format, allowing the reuse of constants |
| /// across multiple recordings to optimize storage. |
| class Recordings { |
| /// [Metadata] such as the recording protocol version. |
| final Metadata metadata; |
| |
| /// The collected [CallReference]s for each [Definition]. |
| /// |
| /// Recorded when `@RecordUse()` is placed on a static member (top-level |
| /// functions, static methods, getters, setters, or operators) in any |
| /// container (library, class, mixin, enum, extension, or extension type). |
| /// |
| /// For example, to record calls to a static method: |
| /// |
| /// <!-- file://./../../example/api/usage.dart#static-call --> |
| /// ```dart |
| /// abstract class PirateTranslator { |
| /// @RecordUse() |
| /// static String speak(String english) => 'Ahoy $english'; |
| /// } |
| /// ``` |
| /// |
| /// Supported Locations: |
| /// - Top-level function / getter / setter. |
| /// - Static method / getter / setter in a class, mixin, or enum. |
| /// - Extension/Extension type method / getter / setter / operator (both |
| /// static and instance). |
| /// |
| /// What is Recorded: |
| /// - [CallWithArguments]: Recorded for direct invocations. |
| /// - [CallWithArguments.positionalArguments] and |
| /// [CallWithArguments.namedArguments]: Captured if they are constant; |
| /// otherwise recorded as [NonConstant]. Any non-provided arguments with |
| /// default values will have their default values filled in. |
| /// - [CallReference.receiver]: For extension instance members, the receiver |
| /// is captured if it is a constant. |
| /// - [CallTearoff]: Recorded for method tear-offs. |
| /// - Getters/Setters: Simple access is recorded as a [CallWithArguments]. For |
| /// setters, the assigned value is captured as a positional argument. |
| /// |
| /// Supported Constants: |
| /// - [NullConstant]: The `null` literal. |
| /// - [BoolConstant]: `true` and `false`. |
| /// - [IntConstant]: All constant integer values. |
| /// - [StringConstant]: All constant string values. |
| /// - [SymbolConstant]: Both public (e.g. `#mySymbol`) and private (e.g. |
| /// `#_myPrivateSymbol`). Private symbols include the library URI in the |
| /// recording to ensure they are unambiguous. |
| /// - [ListConstant]: Constant lists where every element is also a |
| /// supported constant. |
| /// - [MapConstant]: Constant maps where every key and value is a |
| /// supported constant. |
| /// - [RecordConstant]: Constant records (positional and named fields) |
| /// containing supported constants. |
| /// - [EnumConstant]: Constants of an enum type, provided the enum itself is |
| /// annotated with `@RecordUse()`. The recording includes the index, name, |
| /// and any field values (for enhanced enums). |
| /// - [InstanceConstant]: `const` instances of a `final` class, provided the |
| /// class is annotated with `@RecordUse()`. The recording includes all |
| /// constant field values. |
| /// |
| /// Unsupported Constants: |
| /// The following types are explicitly not supported and will be recorded as |
| /// an [UnsupportedConstant] with a descriptive message if encountered: |
| /// - Doubles: Double literals are currently excluded from recording to avoid |
| /// precision/portability issues across different platforms (VM vs JS). |
| /// - Sets: Constant sets are currently not supported (they are handled |
| /// differently than Lists/Maps in the compiler backends). |
| /// - Type Literals: Passing a type itself (e.g. `MyClass`) as a constant |
| /// argument is not supported. |
| /// https://github.com/dart-lang/native/issues/3199 |
| /// - Function/Method Tear-offs: While the tool records the fact that a |
| /// method was torn off (as a usage), passing a method tear-off as a |
| /// constant value into another recorded call is not supported (it will not |
| /// be recorded as a constant value). |
| /// |
| /// Usage in a link hook: |
| /// |
| /// <!-- file://./../../example/api/usage_link.dart#static-call --> |
| /// ```dart |
| /// final calls = uses.calls[methodId] ?? []; |
| /// for (final call in calls) { |
| /// switch (call) { |
| /// case CallWithArguments( |
| /// positionalArguments: [StringConstant(value: final english), ...], |
| /// ): |
| /// // Shrink a translations file based on all the different translation |
| /// // keys. |
| /// print('Translating to pirate: $english'); |
| /// case _: |
| /// print('Cannot determine which translations are used.'); |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// Notes: |
| /// - Type Arguments: Intentionally not recorded (e.g., `myMethod<int>()`), |
| /// as we don't currently have a serialization format for types. |
| /// https://github.com/dart-lang/native/issues/3198 |
| /// - Non-redirecting Factory Constructors: Not yet supported for static |
| /// calls, because they can be the target of redirecting constructors. |
| /// https://github.com/dart-lang/native/issues/3192 |
| final Map<Definition, List<CallReference>> calls; |
| |
| /// The collected [InstanceReference]s for each [Definition]. |
| /// |
| /// Recorded when `@RecordUse()` is placed on a `final class` or `enum` to |
| /// track the lifecycle of instances. |
| /// |
| /// For example, to record instances of a class: |
| /// |
| /// <!-- file://./../../example/api/usage.dart#const-instance --> |
| /// ```dart |
| /// @RecordUse() |
| /// final class PirateShip { |
| /// final String name; |
| /// final int cannons; |
| /// |
| /// const PirateShip(this.name, this.cannons); |
| /// } |
| /// ``` |
| /// |
| /// Supported Locations: |
| /// - `final class` (must be `final` to ensure all creation points are known). |
| /// - `enum` (implicitly `final`). |
| /// |
| /// What is Recorded: |
| /// - [InstanceConstantReference]: Recorded for constant instances and enum |
| /// elements. |
| /// - [InstanceConstantReference.instanceConstant]: The captured constant |
| /// value. |
| /// - [InstanceCreationReference]: Recorded for generative constructor |
| /// invocations (non-const). |
| /// - [InstanceCreationReference.positionalArguments] and |
| /// [InstanceCreationReference.namedArguments]: Captured if they are |
| /// constant; otherwise recorded as [NonConstant]. Any non-provided |
| /// arguments with default values will have their default values |
| /// filled in. |
| /// - [ConstructorTearoffReference]: Recorded for constructor tear-offs. |
| /// - Redirecting Factories (`=`): Resolved to the effective target class and |
| /// recorded as an instance creation, constant, or tear-off of that class. |
| /// - Typedefs: Resolved back to the underlying class. |
| /// - Redirecting Generative Constructors (`: this.`): Recorded at the entry |
| /// point only. |
| /// |
| /// Supported Constants: |
| /// - [NullConstant]: The `null` literal. |
| /// - [BoolConstant]: `true` and `false`. |
| /// - [IntConstant]: All constant integer values. |
| /// - [StringConstant]: All constant string values. |
| /// - [SymbolConstant]: Both public (e.g. `#mySymbol`) and private (e.g. |
| /// `#_myPrivateSymbol`). Private symbols include the library URI in the |
| /// recording to ensure they are unambiguous. |
| /// - [ListConstant]: Constant lists where every element is also a |
| /// supported constant. |
| /// - [MapConstant]: Constant maps where every key and value is a |
| /// supported constant. |
| /// - [RecordConstant]: Constant records (positional and named fields) |
| /// containing supported constants. |
| /// - [EnumConstant]: Constants of an enum type, provided the enum itself is |
| /// annotated with `@RecordUse()`. The recording includes the index, name, |
| /// and any field values (for enhanced enums). |
| /// - [InstanceConstant]: `const` instances of a `final` class, provided the |
| /// class is annotated with `@RecordUse()`. The recording includes all |
| /// constant field values. |
| /// |
| /// Unsupported Constants: |
| /// The following types are explicitly not supported and will be recorded as |
| /// an [UnsupportedConstant] with a descriptive message if encountered: |
| /// - Doubles: Double literals are currently excluded from recording to avoid |
| /// precision/portability issues across different platforms (VM vs JS). |
| /// - Sets: Constant sets are currently not supported (they are handled |
| /// differently than Lists/Maps in the compiler backends). |
| /// - Type Literals: Passing a type itself (e.g. `MyClass`) as a constant |
| /// argument is not supported. |
| /// https://github.com/dart-lang/native/issues/3199 |
| /// - Function/Method Tear-offs: While the tool records the fact that a |
| /// method was torn off (as a usage), passing a method tear-off as a |
| /// constant value into another recorded call is not supported (it will not |
| /// be recorded as a constant value). |
| /// |
| /// Usage in a link hook: |
| /// |
| /// <!-- file://./../../example/api/usage_link.dart#const-instance --> |
| /// ```dart |
| /// final ships = uses.instances[classId] ?? []; |
| /// for (final ship in ships) { |
| /// switch (ship) { |
| /// case InstanceConstantReference( |
| /// instanceConstant: InstanceConstant( |
| /// fields: {'name': StringConstant(value: final name)}, |
| /// ), |
| /// ): |
| /// // Include the 3d model for this ship in the application but not |
| /// // bundle the other ships. |
| /// print('Pirate ship found: $name'); |
| /// case _: |
| /// print('Cannot determine which ships are used.'); |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// Notes: |
| /// - Type Arguments: Intentionally not recorded (e.g., `MyClass<int>()`), |
| /// as we don't currently have a serialization format for types. |
| /// https://github.com/dart-lang/native/issues/3198 |
| /// - Non-redirecting Factories: Invocations of non-redirecting factories are |
| /// NOT recorded as instances. Instead, the body of the factory is analyzed |
| /// like a static method, and any generative constructor calls inside the |
| /// body are recorded as instances of the class. |
| /// - Non-final classes: This is not (yet) supported due to the extra |
| /// complexity with reasoning about instances of subtypes. If we ever |
| /// support this we will likely not allow type hierarchies to cross package |
| /// boundaries due to the ambiguity of to which packages' link hook the |
| /// information should be sent. |
| /// https://github.com/dart-lang/native/issues/3200 |
| final Map<Definition, List<InstanceReference>> instances; |
| |
| Recordings({ |
| Metadata? metadata, |
| required this.calls, |
| required this.instances, |
| }) : metadata = metadata ?? Metadata(); |
| |
| /// Decodes a JSON representation into a [Recordings] object. |
| /// |
| /// The format is specifically designed to reduce redundancy and improve |
| /// efficiency. Definitions and constants are stored in separate tables, |
| /// allowing them to be referenced by index in the `recordings` map. |
| factory Recordings.fromJson(Map<String, Object?> json) { |
| try { |
| final syntax = RecordedUsesSyntax.fromJson(json); |
| final syntaxErrors = syntax.validate(); |
| if (syntaxErrors.isNotEmpty) { |
| final errorsString = syntaxErrors.map((e) => ' - $e').join('\n'); |
| throw FormatException( |
| 'Validation errors for record use file:\n$errorsString\n', |
| ); |
| } |
| return Recordings._fromSyntax(syntax); |
| } on FormatException catch (e) { |
| throw FormatException(''' |
| Invalid JSON format for Recordings: |
| ${const JsonEncoder.withIndent(' ').convert(json)} |
| Error: $e |
| '''); |
| } |
| } |
| |
| factory Recordings._fromSyntax(RecordedUsesSyntax syntax) { |
| final loadingUnitContext = _deserializeLoadingUnits(syntax); |
| final definitionContext = _deserializeDefinitions( |
| syntax, |
| loadingUnitContext, |
| ); |
| final context = _deserializeConstants(syntax, definitionContext); |
| |
| final callsForDefinition = <Definition, List<CallReference>>{}; |
| final instancesForDefinition = <Definition, List<InstanceReference>>{}; |
| |
| final uses = syntax.uses; |
| if (uses != null) { |
| for (final callRecording in uses.staticCalls ?? <CallRecordingSyntax>[]) { |
| final definition = context.definitions[callRecording.definitionIndex]; |
| final callSyntaxes = callRecording.uses; |
| final callReferences = callSyntaxes |
| .map<CallReference>( |
| (callSyntax) => CallReferenceProtected.fromSyntax( |
| callSyntax, |
| context, |
| ), |
| ) |
| .toList(); |
| callsForDefinition |
| .putIfAbsent(definition, () => []) |
| .addAll(callReferences); |
| } |
| for (final instanceRecording |
| in uses.instances ?? <InstanceRecordingSyntax>[]) { |
| final definition = |
| context.definitions[instanceRecording.definitionIndex]; |
| final instanceSyntaxes = instanceRecording.uses; |
| final instanceReferences = instanceSyntaxes |
| .map<InstanceReference>( |
| (instanceSyntax) => InstanceReferenceProtected.fromSyntax( |
| instanceSyntax, |
| context, |
| ), |
| ) |
| .toList(); |
| instancesForDefinition |
| .putIfAbsent(definition, () => []) |
| .addAll(instanceReferences); |
| } |
| } |
| |
| return Recordings( |
| metadata: MetadataProtected.fromSyntax(syntax.metadata), |
| calls: callsForDefinition, |
| instances: instancesForDefinition, |
| ); |
| } |
| |
| Recordings _canonicalizeChildren(CanonicalizationContext context) { |
| final newCalls = <Definition, List<CallReference>>{}; |
| for (final entry in calls.entries) { |
| final definition = context.canonicalizeDefinition(entry.key); |
| final references = [ |
| for (final r in entry.value) r.canonicalizeChildren(context), |
| ]; |
| newCalls.putIfAbsent(definition, () => []).addAll(references); |
| } |
| final newInstances = <Definition, List<InstanceReference>>{}; |
| for (final entry in instances.entries) { |
| final definition = context.canonicalizeDefinition(entry.key); |
| final references = [ |
| for (final r in entry.value) r.canonicalizeChildren(context), |
| ]; |
| newInstances.putIfAbsent(definition, () => []).addAll(references); |
| } |
| return Recordings( |
| metadata: metadata, |
| calls: newCalls, |
| instances: newInstances, |
| ); |
| } |
| |
| static LoadingUnitDeserializationContext _deserializeLoadingUnits( |
| RecordedUsesSyntax syntax, |
| ) { |
| final loadingUnits = <LoadingUnit>[]; |
| for (final unit in syntax.loadingUnits ?? <LoadingUnitSyntax>[]) { |
| loadingUnits.add(LoadingUnit(unit.name)); |
| } |
| return LoadingUnitDeserializationContext(loadingUnits); |
| } |
| |
| static DefinitionDeserializationContext _deserializeDefinitions( |
| RecordedUsesSyntax syntax, |
| LoadingUnitDeserializationContext loadingUnitContext, |
| ) { |
| final definitions = <Definition>[]; |
| for (final definitionSyntax in syntax.definitions ?? <DefinitionSyntax>[]) { |
| definitions.add(DefinitionProtected.fromSyntax(definitionSyntax)); |
| } |
| return DefinitionDeserializationContext.fromPrevious( |
| loadingUnitContext, |
| definitions, |
| ); |
| } |
| |
| static DeserializationContext _deserializeConstants( |
| RecordedUsesSyntax syntax, |
| DefinitionDeserializationContext definitionContext, |
| ) { |
| final constants = <MaybeConstant>[]; |
| // Create a context that includes an empty list for the constants. This |
| // list will be populated by [_deserializeConstants], providing the |
| // self-referential access needed to resolve recursive constants (e.g. |
| // list and map constants). |
| final context = DeserializationContext.fromPrevious( |
| definitionContext, |
| constants, |
| ); |
| for (final constantSyntax in syntax.constants ?? <ConstantSyntax>[]) { |
| final constant = MaybeConstantProtected.fromSyntax( |
| constantSyntax, |
| context, |
| ); |
| if (!constants.contains(constant)) { |
| constants.add(constant); |
| } |
| } |
| return context; |
| } |
| |
| /// Encodes this object into a JSON representation. |
| /// |
| /// This method normalizes identifiers and constants for storage efficiency. |
| Map<String, Object?> toJson() => _toSyntax().json; |
| |
| RecordedUsesSyntax _toSyntax() { |
| final canonContext = CanonicalizationContext(); |
| final canon = _canonicalizeChildren(canonContext); |
| |
| final sortedLoadingUnits = canonContext.loadingUnits.toList() |
| ..sort((a, b) => a.name.compareTo(b.name)); |
| final sortedDefinitions = canonContext.definitions.toList() |
| ..sort((a, b) => a.toString().compareTo(b.toString())); |
| final sortedConstants = canonContext.constants.toList() |
| ..sort((a, b) => a.compareTo(b)); |
| |
| final context = SerializationContext( |
| loadingUnits: sortedLoadingUnits.asMapToIndices, |
| definitions: sortedDefinitions.asMapToIndices, |
| constants: sortedConstants.asMapToIndices, |
| ); |
| |
| final callRecordings = <CallRecordingSyntax>[]; |
| for (final definition in context.definitions.keys) { |
| final callsForDefinition = canon.calls[definition]; |
| if (callsForDefinition == null || callsForDefinition.isEmpty) continue; |
| callRecordings.add( |
| CallRecordingSyntax( |
| definitionIndex: context.definitions[definition]!, |
| uses: callsForDefinition |
| .map((call) => call.toSyntax(context)) |
| .toList(), |
| ), |
| ); |
| } |
| final instanceRecordings = <InstanceRecordingSyntax>[]; |
| for (final definition in context.definitions.keys) { |
| final instancesForDefinition = canon.instances[definition]; |
| if (instancesForDefinition == null || instancesForDefinition.isEmpty) { |
| continue; |
| } |
| instanceRecordings.add( |
| InstanceRecordingSyntax( |
| definitionIndex: context.definitions[definition]!, |
| uses: instancesForDefinition |
| .map((instance) => instance.toSyntax(context)) |
| .toList(), |
| ), |
| ); |
| } |
| |
| final uses = (callRecordings.isEmpty && instanceRecordings.isEmpty) |
| ? null |
| : UsesSyntax( |
| staticCalls: callRecordings.isEmpty ? null : callRecordings, |
| instances: instanceRecordings.isEmpty ? null : instanceRecordings, |
| ); |
| |
| return RecordedUsesSyntax( |
| metadata: metadata.toSyntax(), |
| constants: sortedConstants.isEmpty |
| ? null |
| : [ |
| for (final constant in sortedConstants) |
| constant.toSyntax(context), |
| ], |
| loadingUnits: sortedLoadingUnits.isEmpty |
| ? null |
| : [ |
| for (final unit in sortedLoadingUnits) |
| LoadingUnitSyntax(name: unit.name), |
| ], |
| definitions: sortedDefinitions.isEmpty |
| ? null |
| : [ |
| for (final definition in sortedDefinitions) definition.toSyntax(), |
| ], |
| uses: uses, |
| ); |
| } |
| |
| @override |
| bool operator ==(covariant Recordings other) { |
| if (identical(this, other)) return true; |
| |
| return other.metadata == metadata && |
| deepEquals(other.calls, calls) && |
| deepEquals(other.instances, instances); |
| } |
| |
| @override |
| int get hashCode => cacheHashCode( |
| () => Object.hash( |
| metadata.hashCode, |
| deepHash(calls), |
| deepHash(instances), |
| ), |
| ); |
| |
| /// Compares this set of usages ('actual') with the [expected] set |
| /// ('expected') for semantic equality. |
| /// |
| /// This method performs a configurable semantic comparison that can account |
| /// for variations in compiler optimizations. Its behavior is controlled by |
| /// the named parameters. |
| /// |
| /// Note that this method is quadratic in input size. |
| /// |
| /// If [expectedIsSubset] is `true`, performs a subsumption check instead of a |
| /// strict one-to-one equality check. |
| /// |
| /// The [uriMapping] is a function to map URIs before comparison. Useful when |
| /// compilers use different schemes (e.g., `package:` vs `file:`). |
| /// |
| /// The [loadingUnitMapping] is a function to align loading unit identifiers |
| /// between two sets of recordings. |
| /// |
| /// If [allowDeadCodeElimination] is `true`, the comparison will pass even if |
| /// a usage from [expected] cannot be found in `this`, simulating the effect |
| /// of a compiler optimizing away a call entirely. |
| /// |
| /// If [allowTearoffToStaticPromotion] is `true`, allows an [expected] |
| /// function tear-off to match an `actual` static call. |
| /// |
| /// If [allowMoreConstArguments] is `true`, `null` arguments in an `expected` |
| /// call are ignored during comparison. This can be used to accommodate |
| /// differences in how compilers handle default or optional arguments. |
| /// |
| /// If [allowMetadataMismatch] is `true`, the [metadata] does not need to |
| /// match. |
| @visibleForTesting |
| bool semanticEquals( |
| Recordings expected, { |
| bool expectedIsSubset = false, |
| bool allowDeadCodeElimination = false, |
| bool allowTearoffToStaticPromotion = false, |
| bool allowMoreConstArguments = false, |
| bool allowPromotionOfUnsupported = false, |
| bool allowMetadataMismatch = false, |
| String Function(String)? uriMapping, |
| String Function(String)? loadingUnitMapping, |
| }) { |
| if (!allowMetadataMismatch && metadata != expected.metadata) { |
| return false; |
| } |
| bool definitionMatches(Definition a, Definition b) => |
| // ignore: invalid_use_of_visible_for_testing_member |
| a.semanticEquals(b, uriMapping: uriMapping); |
| |
| if (!_compareUsageMap( |
| actual: calls, |
| expected: expected.calls, |
| expectedIsSubset: expectedIsSubset, |
| allowDeadCodeElimination: allowDeadCodeElimination, |
| definitionMatches: definitionMatches, |
| referenceMatches: (CallReference a, CallReference b) => |
| // ignore: invalid_use_of_visible_for_testing_member |
| a.semanticEquals( |
| b, |
| allowTearoffToStaticPromotion: allowTearoffToStaticPromotion, |
| allowMoreConstArguments: allowMoreConstArguments, |
| allowPromotionOfUnsupported: allowPromotionOfUnsupported, |
| uriMapping: uriMapping, |
| loadingUnitMapping: loadingUnitMapping, |
| ), |
| )) { |
| return false; |
| } |
| |
| if (!_compareUsageMap( |
| actual: instances, |
| expected: expected.instances, |
| expectedIsSubset: expectedIsSubset, |
| allowDeadCodeElimination: allowDeadCodeElimination, |
| definitionMatches: definitionMatches, |
| referenceMatches: (InstanceReference a, InstanceReference b) => |
| // ignore: invalid_use_of_visible_for_testing_member |
| a.semanticEquals( |
| b, |
| uriMapping: uriMapping, |
| loadingUnitMapping: loadingUnitMapping, |
| allowMoreConstArguments: allowMoreConstArguments, |
| allowPromotionOfUnsupported: allowPromotionOfUnsupported, |
| ), |
| )) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /// Returns true if [expected] is a semantic subset of [actual]. |
| static bool _compareUsageMap<R extends Reference>({ |
| required Map<Definition, List<R>> actual, |
| required Map<Definition, List<R>> expected, |
| required bool expectedIsSubset, |
| required bool allowDeadCodeElimination, |
| required bool Function(Definition, Definition) definitionMatches, |
| required bool Function(R, R) referenceMatches, |
| }) { |
| final actualUsages = actual.entries.toList(); |
| final expectedUsages = expected.entries.toList(); |
| |
| if (!expectedIsSubset && |
| !allowDeadCodeElimination && |
| actualUsages.length != expectedUsages.length) { |
| return false; |
| } |
| |
| final matchedActualIndices = <int>{}; |
| |
| for (final expectedUsage in expectedUsages) { |
| int? foundMatchIndex; |
| for (var i = 0; i < actualUsages.length; i++) { |
| if (matchedActualIndices.contains(i)) { |
| continue; |
| } |
| |
| final actualUsage = actualUsages[i]; |
| |
| if (definitionMatches(actualUsage.key, expectedUsage.key)) { |
| // Definitions match semantically. Now check the references. |
| // The list of references for this identifier must be an exact |
| // semantic match. |
| final referencesMatch = _matchReferences( |
| actual: actualUsage.value, |
| expected: expectedUsage.value, |
| allowDeadCodeElimination: allowDeadCodeElimination, |
| matches: referenceMatches, |
| ); |
| |
| if (referencesMatch) { |
| foundMatchIndex = i; |
| break; |
| } |
| } |
| } |
| |
| if (foundMatchIndex != null) { |
| matchedActualIndices.add(foundMatchIndex); |
| } else if (!allowDeadCodeElimination) { |
| // No match found for this expected usage, and DCE not allowed. |
| return false; |
| } |
| } |
| |
| // In one-to-one mode, all actual usages must have been matched. |
| if (!expectedIsSubset && |
| matchedActualIndices.length != actualUsages.length) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /// Tries to find a pairing for each [expected] item from the [actual] items. |
| /// |
| /// Each item from [actual] can be matched at most once. |
| static bool _matchReferences<R extends Reference>({ |
| required List<R> actual, |
| required List<R> expected, |
| required bool Function(R, R) matches, |
| required bool allowDeadCodeElimination, |
| }) { |
| if (!allowDeadCodeElimination && actual.length != expected.length) { |
| return false; |
| } |
| if (actual.length < expected.length) { |
| return false; |
| } |
| |
| final matchedActualIndices = <int>{}; |
| |
| for (final expectedItem in expected) { |
| int? foundMatchIndex; |
| for (var i = 0; i < actual.length; i++) { |
| if (matchedActualIndices.contains(i)) { |
| continue; |
| } |
| if (matches(actual[i], expectedItem)) { |
| foundMatchIndex = i; |
| break; |
| } |
| } |
| |
| if (foundMatchIndex != null) { |
| matchedActualIndices.add(foundMatchIndex); |
| } else { |
| return false; // No match for expectedItem. |
| } |
| } |
| |
| return true; |
| } |
| |
| /// Returns a new [Recordings] that only contains usages of definitions |
| /// filtered by the provided criteria. |
| /// |
| /// If [definitionPackageName] is provided, only usages of definitions |
| /// defined in that package are included. |
| Recordings filter({String? definitionPackageName}) { |
| bool belongsToPackage(Definition definition) { |
| if (definitionPackageName == null) return true; |
| final uri = definition.library; |
| return uri.startsWith('package:$definitionPackageName/'); |
| } |
| |
| final newCallsForDefinition = { |
| for (final entry in calls.entries) |
| if (belongsToPackage(entry.key)) |
| entry.key: [ |
| for (final call in entry.value) |
| call.filter(definitionPackageName: definitionPackageName), |
| ], |
| }; |
| |
| final newInstancesForDefinition = { |
| for (final entry in instances.entries) |
| if (belongsToPackage(entry.key)) |
| entry.key: [ |
| for (final instance in entry.value) |
| instance.filter(definitionPackageName: definitionPackageName), |
| ], |
| }; |
| |
| return Recordings( |
| metadata: metadata, |
| calls: newCallsForDefinition, |
| instances: newInstancesForDefinition, |
| ); |
| } |
| } |
| |
| extension MapifyIterableExtension<T> on Iterable<T> { |
| /// Transform list to map, faster than using list.indexOf |
| Map<T, int> get asMapToIndices { |
| var i = 0; |
| return {for (final element in this) element: i++}; |
| } |
| } |