runWithReporter method
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,
) {
final realtimeChannelRegex = RegExp(r'\bRealtimeChannel\b');
final removeChannelRegex = RegExp(r'\bremoveChannel\b');
RegExp unsubscribeRegex(String name) =>
RegExp(RegExp.escape(name) + r'\s*[?.]?\s*\.\s*unsubscribe\s*\(');
context.addClassDeclaration((ClassDeclaration node) {
final ExtendsClause? extendsClause = node.extendsClause;
if (extendsClause == null) return;
final String superName = extendsClause.superclass.name.lexeme;
if (superName != 'State') return;
// Find RealtimeChannel fields
final List<String> channelNames = <String>[];
for (final ClassMember member in node.bodyMembers) {
if (member is FieldDeclaration) {
final String? typeName = member.fields.type?.toSource();
if (typeName != null && realtimeChannelRegex.hasMatch(typeName)) {
for (final VariableDeclaration variable
in member.fields.variables) {
channelNames.add(variable.name.lexeme);
}
}
}
}
if (channelNames.isEmpty) return;
// Find dispose method
String? disposeBody;
for (final ClassMember member in node.bodyMembers) {
if (member is MethodDeclaration && member.name.lexeme == 'dispose') {
disposeBody = member.body.toSource();
break;
}
}
// Check if channels are unsubscribed
for (final String name in channelNames) {
final bool isUnsubscribed =
disposeBody != null &&
(unsubscribeRegex(name).hasMatch(disposeBody) ||
removeChannelRegex.hasMatch(disposeBody));
if (!isUnsubscribed) {
for (final ClassMember member in node.bodyMembers) {
if (member is FieldDeclaration) {
for (final VariableDeclaration variable
in member.fields.variables) {
if (variable.name.lexeme == name) {
reporter.atNode(variable);
}
}
}
}
}
}
});
}