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.addMethodInvocation((MethodInvocation node) {
final String methodName = node.methodName.name;
if (methodName != 'toIso8601String' &&
!_dateTimeStorageMethodPatterns.any((p) => p.hasMatch(methodName))) {
return;
}
final Expression? target = node.target;
if (target == null) return;
// Check if called on DateTime
final DartType? type = target.staticType;
if (type == null) return;
final String typeName = type.getDisplayString();
if (!typeName.startsWith('DateTime')) return;
// Check if already UTC. Structural (not text-based), so
// `DateTime.now().toUtc().toLocal()` is correctly NOT treated as
// UTC even though the substring `.toUtc()` appears earlier in the
// expression — only the OUTERMOST form (a bare `.toUtc()` call, or a
// `DateTime.utc(...)` constructor) counts.
if (_expressionIsUtc(target)) {
return;
}
// Check if inside a storage-related context.
//
// Scope the scan to the IMMEDIATE enclosing statement only. Walking every
// ancestor ran the storage regexes against each ancestor's full
// toSource(), which grows to the whole method/class as we ascend — so any
// unrelated `save(` / `insert(` / `.set...(` elsewhere in the enclosing
// scope marked a toIso8601String() used for a UI label as "storage
// context" (also quadratic). The storage call that actually receives this
// serialized DateTime is always within the same enclosing statement.
AstNode? current = node.parent;
while (current != null) {
final String source = current.toSource();
for (final pattern in _storagePatterns) {
if (pattern.hasMatch(source)) {
// The receiver's own source never mentions UTC for a bare
// identifier (e.g. `utcNow`) even when the value was normalized
// upstream, at a local variable's initializer. Resolve one hop
// back before reporting. This resolution walks the compilation
// unit, so it only runs once a storage-context match makes
// reporting otherwise imminent, not on every toIso8601String()
// call.
if (target is SimpleIdentifier && _isUpstreamUtc(target)) {
return;
}
reporter.atNode(node);
return;
}
}
// Stop once we have scanned the enclosing statement; ancestors beyond it
// belong to sibling statements and are not this value's storage target.
if (current is Statement) return;
current = current.parent;
}
});
}