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.addBinaryExpression((BinaryExpression node) {
    // Check for string + operator
    if (node.operator.type != TokenType.PLUS) return;

    // Check if we're inside a loop
    if (!_isInsideLoop(node)) return;

    // Only flag a genuine accumulator: `s = s + x` (or `s = x + s`) where the
    // assignment target is one of the `+` operands and is read back each
    // iteration. A per-element transform — `.map((e) => e + suffix)`, a fresh
    // per-iteration local, a `RegExp(r'\s*' + escaped)` argument — produces a
    // NEW string each pass (O(n) total, StringBuffer inapplicable), not the
    // O(n^2) accumulation this rule targets, and has no such accumulator.
    final AstNode? parent = node.parent;
    if (parent is! AssignmentExpression) return;
    if (parent.rightHandSide != node) return;
    if (parent.operator.type != TokenType.EQ) return;
    final String targetSource = parent.leftHandSide.toSource();
    if (targetSource != node.leftOperand.toSource() &&
        targetSource != node.rightOperand.toSource()) {
      return;
    }

    // Check if operands look like strings
    final String source = node.toSource();
    if (_looksLikeStringOperation(source)) {
      reporter.atNode(node);
    }
  });

  context.addAssignmentExpression((AssignmentExpression node) {
    // Check for += operator
    if (node.operator.type != TokenType.PLUS_EQ) return;

    // Check if we're inside a loop
    if (!_isInsideLoop(node)) return;

    // Require the accumulator to ACTUALLY be a String. The previous
    // variable-name heuristic ("result"/"output"/"buffer"/"message") flagged
    // numeric accumulators like `total += count` or `resultMap[k] += n` as
    // O(n^2) string concat — a false positive, since numeric `+=` is O(1) and
    // StringBuffer is inapplicable. Only `String += ...` allocates a new
    // String per iteration, which is the O(n^2) pattern this rule targets.
    //
    // Use `readType` (the type read back from the target before the compound
    // op) rather than `leftHandSide.staticType`: an assignment target is in a
    // WRITE context, so its `staticType` is often null, whereas `readType`
    // carries the resolved String type of the accumulator being read each pass.
    final DartType? targetType = node.readType;
    if (targetType == null || !targetType.isDartCoreString) return;

    reporter.atNode(node);
  });
}