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