Antinna / equatable_macro

Apache License 2.0
0 stars 0 forks source link

New Approach, works with `sealed` classes #1

Open mg3994 opened 5 days ago

mg3994 commented 5 days ago

New Idea

New Update Soon

pubspec.yaml


dependencies:
_macros:
sdk: dart
> analysis_options.yaml
```yaml
include: package:lints/recommended.yaml
analyzer:
  enable-experiment:
    - macros

macro.dart

export 'package:_macros/src/api.dart';

equatable_macro_base.dart

// equatable_macro_base.dart
// import 'package:macros/macros.dart'; // removing this as we will directly export that api and import that here , to avoid error
import 'macro.dart'; //try this instead
import 'dart:async';

/// The `Equatable` class is a Dart macro used for automatically implementing equality
/// and hashCode methods for classes. This macro is particularly useful for reducing boilerplate
/// code and ensuring consistent equality checks across your Dart classes.
///
/// The `Equatable` macro implements both `ClassDeclarationsMacro` and `ClassDefinitionMacro`
/// to provide functionality for modifying class declarations and definitions.
///
/// It applies the `@Equatable` annotation to a class, generating the necessary code
/// for equality comparison based on the fields of the class.
///
/// The `stringify` parameter controls whether a `toString` method is generated for the class:
/// - If `stringify` is `true`, the generated `toString` method will include the values of
///   the fields in its output.
/// - If `stringify` is `false` (the default), the `toString` method will not include field values.
///
/// This macro is part of the `equatable_macro` package and provides a clean and efficient
/// way to make classes equatable without manually overriding `==` and `hashCode` methods.
///
/// ## Usage
/// To use the `Equatable` macro, annotate your class with `@Equatable()` as follows:
///
/// ```dart
/// import 'package:equatable_macro/equatable_macro.dart';
///
/// @Equatable(stringify: true) // Set stringify to true to include field values in toString
/// class Example {
///   final int id;
///   final String name;
///
///   const Example(this.id, this.name);
/// }
/// ```
///
/// ## Parameters
/// - `stringify` (optional, defaults to `false`): If `true`, the generated `toString` method
///   will include the values of the fields in its output. If `false`, the `toString` method
///   will not include field values.
///
/// ## Implementation
/// The `Equatable` macro modifies the class definition to include the following:
/// - An overridden `==` operator that compares instances based on their field values.
/// - An overridden `hashCode` getter that computes a hash code based on the field values.
/// - An optional `toString` method if `stringify` is set to `true`. The `toString` method
///   will return a string representation of the class including field values.
///
/// ## Example
/// ```dart
/// import 'package:equatable_macro/equatable_macro.dart';
///
/// @Equatable(stringify: true)
/// class Example {
///   final int id;
///   final String name;
///
///   const Example(this.id, this.name);
/// }
///
/// void main() {
///   var a = Example(1, 'a');
///   var b = Example(1, 'a');
///   var c = Example(2, 'b');
///
///   print(a == b); // true
///   print(a == c); // false
///   print(a.toString()); // Example(id: 1, name: a)
/// }
/// ```
///
/// This macro is intended to simplify the process of making Dart classes equatable
/// and improve code maintainability by automating the implementation of equality
/// checks and hash code computations. The `stringify` parameter provides flexibility
/// for including or excluding field values in the `toString` representation.

macro class Equatable implements ClassDeclarationsMacro {
  /// Indicates whether the `toString` method should include field values.
  /// Defaults to `false`.
  final bool? _stringify;

  /// Creates an instance of the `Equatable` macro.
  ///
  /// - `stringify`: If `true`, the generated `toString` method will include field values.
  ///   If `false`, the `toString` method will not include field values. Defaults to `false`.
  const Equatable({ bool? stringify, }) : _stringify = stringify;
  const Equatable.unstringify() : _stringify = false;
   const Equatable.stringify() : _stringify = true;
  ///! For Future Release
  bool get stringify {
    return _stringify  ?? _defaultstringify;
  }
  ///! For Future Release
  static  bool _defaultstringify = false;
  ///! For Future Release
  set stringify(bool stringify) { 
    _defaultstringify=stringify;
    stringify = stringify;}

  MethodDeclaration? equality(List<MethodDeclaration> methods ,String op) {
    return
     methods.firstWhereOrNull(
      (m) => m.identifier.name == op,
    );

  }

  @override
  FutureOr<void> buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder)async {

    final boolean = await  builder.resolveIdentifier(Uri.parse('dart:core'), 'bool');
      final integer = await builder.resolveIdentifier(Uri.parse('dart:core'), 'int');
      final string = await builder.resolveIdentifier(Uri.parse('dart:core'), 'String');
    final defaultClassMethodHashCode = await builder.methodsOf(clazz).then((value)=>value.where((elements)=>(elements.isGetter && elements.identifier.name == "hashCode" ))) ; 
    final defaultClassMethodEqualEqual = await builder.methodsOf(clazz).then((value)=>value.where((elements)=>(elements.isOperator && elements.identifier.name == "=="))) ; 
    final defaultClassMethodToString = await builder.methodsOf(clazz).then((value)=>value.where((elements)=>( elements.identifier.name == "toString"))) ; 

    if (defaultClassMethodHashCode.isNotEmpty ) { 
    for (var element in defaultClassMethodHashCode ) {
                  builder.report(  Diagnostic( DiagnosticMessage(
              'A default `${element.identifier.name}` method was created with `@Equatable()` Macro',
              target: element.asDiagnosticTarget,
            ),
            Severity.error,

          correctionMessage:' * Remove This `${element.identifier.name}` method  *'));

    }

    }

    if (defaultClassMethodEqualEqual.isNotEmpty ) { 
    for (var element in defaultClassMethodEqualEqual ) {
                 builder.report(  Diagnostic( DiagnosticMessage(
              'The name `${element.identifier.name}` is already defined and it is created by `@Equatable()` Macro',
              target: element.asDiagnosticTarget,
            ),
            Severity.error,

          correctionMessage:' * Remove This `${element.identifier.name}` method or Try renaming the declarations*'));

    }

    }

    if (defaultClassMethodToString.isNotEmpty && stringify) { 
    for (var element in defaultClassMethodToString ) {
                 builder.report(  Diagnostic( DiagnosticMessage(
              'The name `${element.identifier.name}` is already defined and it is created by `@Equatable()` Macro',
              target: element.asDiagnosticTarget,
            ),
            Severity.error,

          correctionMessage:' * Remove This `${element.identifier.name}` method or Try renaming the declarations*'));

    }

    }

    //
     final methods = await builder.methodsOf(clazz);
     final object    = await builder.resolveIdentifier(Uri.parse('dart:core'), 'Object');  
var allFields = <FieldDeclaration>[];
 superclassOf(ClassDeclaration clazz) async {  final superclassType = clazz.superclass != null
      ? await builder.typeDeclarationOf( clazz.superclass!.identifier): null;
        return superclassType is ClassDeclaration ? superclassType : null;
       }

allFields.addAll(await builder.fieldsOf(clazz));
 var superclass = await superclassOf(clazz);

while (superclass != null && superclass.identifier.name != 'Object') {
  allFields.addAll(await builder.fieldsOf(superclass));
  superclass = await superclassOf(superclass);
}
allFields = allFields..removeWhere((f) => f.hasStatic); //!   modify in future
final fields       = allFields;
final fieldNames = fields.map((f) => f.identifier.name); 
final lastField = fieldNames.lastOrNull; //error here modify it as nullable
final toStringFields = fields.map((field) {
      return '${field.type.isNullable ? 'if (${field.identifier.name} != null)' : ''} "${field.identifier.name}: \${${field.identifier.name}.toString()}", ';
    });

    return builder.declareInLibrary(DeclarationCode.fromParts([
       'augment ',
        if (clazz.hasSealed) 'sealed ',
        'class ${clazz.identifier.name}',
        // if (clazz.superclass case final superClass?) ...[
        //   ' extends ',
        //   superClass.code,
        // ],
        ' {\n'

      ,'  external ', boolean, ' operator==(', ' covariant ', '${clazz.identifier.name}' ' other);\n',
    '  external ', integer, ' get hashCode;\n', //if nothing in fields then hash code will be 0 , that's not good, change full logic
    if (stringify) ...['  external ', string, ' toString();\n',
       '  augment ', string, ' toString()' '=> "',
          clazz.identifier.name,
          '(\${[',
          ...toStringFields,
          '].join(", ")})',
          '";\n',
    ],

    if (fields.isEmpty || lastField == null) ...[
      '  augment ', boolean, ' operator==(', ' covariant ', '${clazz.identifier.name}' ' other)'
      ' => '
      'other != null ;\n'],

    if(lastField != null)  ...[ '  augment ', boolean, ' operator==(', ' covariant ', '${clazz.identifier.name}' ' other)'
      ' => ',
      for (final field in fieldNames)

            ...[ 'other.$field == $field', if (field != lastField) ' && '], //that will give error here
          ';\n',

      ],
       '  augment ', integer, ' get hashCode => ', 
       object,
          '.hashAll',
          '([',
          'runtimeType, ',
          fieldNames.join(', '),
          ']',
          ');\n',
          '}'

    ]));
  }

}

extension IterableExtensionX<T> on Iterable<T> {
  /// The first element satisfying [test], or `null` if there are none.
  T? firstWhereOrNull(bool Function(T element) test) {
    for (var element in this) {
      if (test(element)) return element;
    }
    return null;
  }

}
Todo

Features:

Manishmg3994 commented 19 hours ago

@mg3994 Approved, 👍 but refactor it , also don't turn code in smaller chunks , in future API may change