runWithReporter method

  1. @override
void runWithReporter(
  1. SaropaDiagnosticReporter reporter,
  2. SaropaContext context
)
override

Override this method to implement your lint rule.

Use context to register callbacks for AST node types:

context.addMethodInvocation((node) {
  if (condition) {
    reporter.atNode(node);
  }
});

Implementation

@override
void runWithReporter(
  SaropaDiagnosticReporter reporter,
  SaropaContext context,
) {
  context.addClassDeclaration((ClassDeclaration node) {
    // Check if extends State<T>
    final ExtendsClause? extendsClause = node.extendsClause;
    if (extendsClause == null) return;

    final NamedType superclass = extendsClause.superclass;
    final String superName = superclass.name.lexeme;

    if (superName != 'State') return;
    if (superclass.typeArguments == null) return;

    // Find Ticker fields
    final List<String> tickerFields = <String>[];
    for (final ClassMember member in node.bodyMembers) {
      if (member is FieldDeclaration) {
        final String? typeName = member.fields.type?.toSource();
        if (typeName != null &&
            (typeName == 'Ticker' || typeName == 'Ticker?')) {
          for (final VariableDeclaration variable
              in member.fields.variables) {
            tickerFields.add(variable.name.lexeme);
          }
        }
      }
    }

    if (tickerFields.isEmpty) return;

    // Find dispose method and check for stop calls
    String? disposeBody;
    for (final ClassMember member in node.bodyMembers) {
      if (member is MethodDeclaration && member.name.lexeme == 'dispose') {
        disposeBody = member.body.toSource();
        break;
      }
    }

    // Report tickers not stopped in dispose (RegExp built once per field)
    final stopRegexByField = <String, RegExp>{
      for (final String f in tickerFields)
        f: RegExp(
          '${RegExp.escape(f)}\\s*[?.]+'
          '\\s*(stop|dispose)\\s*\\(\\)',
        ),
    };
    for (final String fieldName in tickerFields) {
      final re = stopRegexByField[fieldName];
      if (re == null) continue;
      final bool isStopped = disposeBody != null && re.hasMatch(disposeBody);

      if (!isStopped) {
        for (final ClassMember member in node.bodyMembers) {
          if (member is FieldDeclaration) {
            for (final VariableDeclaration variable
                in member.fields.variables) {
              if (variable.name.lexeme == fieldName) {
                reporter.atNode(variable);
              }
            }
          }
        }
      }
    }
  });
}