visitClassDeclaration method

  1. @override
void visitClassDeclaration(
  1. ClassDeclaration node
)
override

Implementation

@override
void visitClassDeclaration(ClassDeclaration node) {
  final classBody = node.body;
  if (classBody is! BlockClassBody) {
    super.visitClassDeclaration(node);
    return;
  }

  bool isMobXStore = false;

  // Detect via `extends Store` — the pattern used by abstract base classes
  // in code-generated MobX (e.g. `abstract class _CartStore with Store`).
  final extendsClause = node.extendsClause;
  final withClause = node.withClause;
  if (extendsClause != null &&
      extendsClause.superclass.name.lexeme == 'Store') {
    isMobXStore = true;
  }
  if (!isMobXStore && withClause != null) {
    if (withClause.mixinTypes.any((t) => t.name.lexeme == 'Store')) {
      isMobXStore = true;
    }
  }

  // Detect via @observable / @computed field annotations.
  if (!isMobXStore) {
    for (final member in classBody.members) {
      if (member is FieldDeclaration) {
        for (final metadata in member.metadata) {
          final name = metadata.name.toSource();
          if (name == 'observable' || name == 'computed') {
            isMobXStore = true;
            break;
          }
        }
      }
      if (isMobXStore) break;
    }
  }

  if (isMobXStore) {
    final className = node.namePart.typeName.lexeme;
    final stateFields = <FieldInfo>[];
    final methods = <MethodInfo>[];

    bool isFamilyCandidate = false;
    for (final member in classBody.members) {
      if (member is FieldDeclaration) {
        bool hasObservable = false;
        bool hasComputed = false;
        for (final metadata in member.metadata) {
          final name = metadata.name.toSource();
          if (name == 'observable') hasObservable = true;
          if (name == 'computed') hasComputed = true;
        }
        if (hasObservable || hasComputed) {
          for (final variable in member.fields.variables) {
            // @computed fields are getters in MobX but stored as late fields;
            // mark them with a `// computed` initializer hint so the
            // transformer can emit a derived Provider instead of a state field.
            stateFields.add(
              FieldInfo(
                rawName: variable.name.lexeme,
                type: member.fields.type?.toSource() ?? 'dynamic',
                initializer: hasComputed
                    ? (variable.initializer?.toSource() ?? '/* computed */')
                    : variable.initializer?.toSource(),
              ),
            );
          }
        }
      } else if (member is ConstructorDeclaration) {
        for (final param in member.parameters.parameters) {
          final paramName = param.name?.lexeme ?? '';
          if (paramName != 'key') {
            isFamilyCandidate = true;
            break;
          }
        }
      } else if (member is MethodDeclaration) {
        bool isAction = false;
        for (final metadata in member.metadata) {
          if (metadata.name.toSource() == 'action') {
            isAction = true;
            break;
          }
        }
        // We capture all methods, but mark if they are actions (callsNotifyListeners equivalent)
        methods.add(buildMethodInfo(member, callsNotifyListeners: isAction));
      }
    }

    nodes.add(
      LogicUnitNode(
        name: className,
        stateFields: stateFields,
        methods: methods,
        isNotifier: true,
        notifierType: detectNotifierType(methods),
        isFamilyCandidate: isFamilyCandidate,
        filePath: filePath,
        offset: node.offset,
        length: node.length,
      ),
    );
  }
  super.visitClassDeclaration(node);
}