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,
) {
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);
}
}
}
}
}
}
});
}