compute method

  1. @override
Future<void> compute(
  1. ChangeBuilder builder
)
override

Computes the changes for this producer using builder.

This method should not modify fixKind.

Implementation

@override
Future<void> compute(ChangeBuilder builder) async {
  logInfo('Starting assist: Convert to StatelessComponent');
  final componentClass = node.thisOrAncestorOfType<ClassDeclaration>();
  final superclass = componentClass?.extendsClause?.superclass;
  logInfo(
    'Found component class: ${componentClass?.name.lexeme}, '
    'superclass: ${superclass?.name.lexeme}',
  );
  if (componentClass == null || superclass == null) {
    logError(
      ' No class declaration or superclass found at offset $selectionOffset',
    );
    return;
  }
  // useDeclaringConstructorsAst = true;
  final componentClassBody = componentClass.body;
  if (componentClassBody is! BlockClassBody) {
    logError(
      ' Component class body is not a '
      'block ${componentClassBody.runtimeType}',
    );
    return;
  }

  // Don't spam, activate only from the `class` keyword to the class body.
  if (selectionOffset < componentClass.classKeyword.offset ||
      selectionOffset > componentClassBody.leftBracket.end) {
    logError(
      ' Selection is outside the component class body',
    );
    return;
  }

  // Must be a StatefulComponent subclass.
  final componentClassFragment = componentClass.declaredFragment!;
  final componentClassElement = componentClassFragment.element;
  final superType = componentClassElement.supertype;
  if (superType == null || !superType.isExactlyStatefulComponentType) {
    logError(
      ' Component class is not a StatefulComponent subclass',
    );
    return;
  }

  final createStateMethod = _findCreateStateMethod(componentClass);
  if (createStateMethod == null) return;

  final stateClass = _findStateClass(componentClassElement);
  final stateClassElement = stateClass?.declaredFragment!.element;
  if (stateClass == null ||
      stateClassElement == null ||
      !Identifier.isPrivateName(stateClass.namePart.typeName.lexeme) ||
      !_isSameTypeParameters(componentClass, stateClass)) {
    logError(' State class is not valid');
    return;
  }

  final verifier = _StatelessVerifier();
  final fieldFinder = _FieldFinder();

  for (final member in stateClass.members2) {
    if (member is ConstructorDeclaration) {
      member.accept(fieldFinder);
    } else if (member is MethodDeclaration) {
      member.accept(verifier);
      if (!verifier.canBeStateless) {
        logError(
          ' State class cannot be converted to StatelessComponent',
        );
        return;
      }
    }
  }

  final usageVerifier = _StateUsageVisitor(
    componentClassElement,
    stateClassElement,
  );
  unit.visitChildren(usageVerifier);
  if (usageVerifier.used) {
    logError(
      ' State class is used in a way that prevents'
      ' conversion to StatelessComponent',
    );
    return;
  }

  final fieldsAssignedInConstructors =
      fieldFinder.fieldsAssignedInConstructors;

  // Prepare nodes to move.
  final nodesToMove = <ClassMember>[];
  final elementsToMove = <Element>{};
  for (final member in stateClass.members2) {
    if (member is FieldDeclaration) {
      if (member.isStatic) {
        logWarning(
          ' Static members cannot be moved to StatelessComponent '
          ' and will be skipped: '
          '${member.fields.variables.map((v) => v.name.lexeme).join(', ')}',
        );
        return;
      }
      for (final fieldNode in member.fields.variables) {
        final fieldElement =
            fieldNode.declaredFragment!.element as FieldElement;
        if (!fieldsAssignedInConstructors.contains(fieldElement)) {
          nodesToMove.add(member);
          elementsToMove.add(fieldElement);

          final getter = fieldElement.getter;
          if (getter != null) {
            elementsToMove.add(getter);
          }

          final setter = fieldElement.setter;
          if (setter != null) {
            elementsToMove.add(setter);
          }
        }
      }
    } else if (member is MethodDeclaration) {
      if (member.isStatic) {
        logWarning(
          ' Static members cannot be moved to StatelessComponent and will'
          ' be skipped: ${member.name.lexeme}',
        );
        return;
      }
      if (!_isDefaultOverride(member)) {
        nodesToMove.add(member);
        elementsToMove.add(member.declaredFragment!.element);
      }
    }
  }

  /// Return the code for the [movedNode], so that qualification of the
  /// references to the widget (`widget.` or static `MyComponentClass.`)
  /// is removed
  String rewriteComponentMemberReferences(AstNode movedNode) {
    final linesRange = utils.getLinesRange(range.node(movedNode));
    final text = utils.getRangeText(linesRange);

    // Remove `widget.` before references to the widget instance members.
    final visitor = _ReplacementEditBuilder(
      componentClassElement,
      elementsToMove,
      linesRange,
    );
    movedNode.accept(visitor);
    return SourceEdit.applySequence(text, visitor.edits.reversed.toList());
  }

  final statelessComponentClass = await getNoctermClass(
    sessionHelper,
    'StatelessComponent',
  );
  if (statelessComponentClass == null) {
    logError(' Could not find StatelessComponent class');
    return;
  }

  await builder.addDartFileEdit(file, (builder) {
    builder.addReplacement(range.node(superclass), (builder) {
      builder.writeReference(statelessComponentClass);
    });

    builder.addDeletion(range.deletionRange(stateClass));

    var createStateNextToEnd = createStateMethod.endToken.next!;
    createStateNextToEnd =
        createStateNextToEnd.precedingComments ?? createStateNextToEnd;
    final createStateRange = range.startOffsetEndOffset(
      utils.getLineContentStart(createStateMethod.offset),
      utils.getLineContentStart(createStateNextToEnd.offset),
    );

    final newLine =
        createStateNextToEnd.type != TokenType.CLOSE_CURLY_BRACKET;

    builder.addReplacement(createStateRange, (builder) {
      for (var i = 0; i < nodesToMove.length; i++) {
        final member = nodesToMove[i];
        final comments = member.beginToken.precedingComments;
        if (comments != null) {
          final offset = utils.getLineContentStart(comments.offset);
          final length = comments.end - offset;
          builder.writeln(utils.getText(offset, length));
        }

        final text = rewriteComponentMemberReferences(member);
        builder.write(text);
        if (newLine || i < nodesToMove.length - 1) {
          builder.writeln();
        }
      }
    });
  });
}