Antinna / equatable_macro

Apache License 2.0
0 stars 0 forks source link

New Approach, works with `sealed` classes #1

Closed mg3994 closed 1 month ago

mg3994 commented 2 months 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 2 months ago

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

mg3994 commented 2 months ago

We are continuing with the older approach as it is future-proof with the upcoming support for sealed classes. However, there are a few key concerns and issues that need to be addressed:

Covariant Usage: The covariant keyword is facing issues.

Same Augment Library Imports: covariant makes same augment library imports with alias prefix

Private Class () Usage: The private class starts with/denoted by `` are facing issues.

mg3994 commented 2 months ago

This Source code show above is just a preview of development template only, it will not work

Manishmg3994 commented 1 month ago

covariant is enough for now , Dart Macros team is improving That unnecessary import , stay tuned for future , Closing it for now Approved changements are only

Ankit3994 commented 1 month ago

With new dart sdk version sealed classes are behaving correctly for augmented code in IDE without error ,may face error during build, dart team is working on that no worry