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.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;
    }
  });
}