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.addConditionalExpression((ConditionalExpression node) {
// Check for pattern: condition ? Colors.red/error : something
final String conditionSource = node.condition.toSource().toLowerCase();
final String thenSource = node.thenExpression.toSource().toLowerCase();
final String elseSource = node.elseExpression.toSource().toLowerCase();
// cspell:ignore haserror iserror isvalid
// Check if this is an error-related condition
if (!conditionSource.contains('error') &&
!conditionSource.contains('invalid') &&
!conditionSource.contains('haserror') &&
!conditionSource.contains('iserror') &&
!conditionSource.contains('isvalid')) {
return;
}
// Check if using error colors - use patterns to avoid false positives
// like 'thread', 'spread', 'shredded' matching 'red'
if (!_errorColorPattern.hasMatch(thenSource) &&
!_errorColorPattern.hasMatch(elseSource)) {
return;
}
// Require at least one branch to be Color-typed. The `\.error\b` alternative
// in the regex matches ANY dotted identifier ending in error/Error —
// including a log-severity enum value like `DebugLevels.Error` or a String
// label — none of which is a UI color. A staticType==Color gate drops those
// non-color ternaries (the accessibility concern only applies when the
// distinction is actually carried by color).
if (!_isColorTyped(node.thenExpression.staticType) &&
!_isColorTyped(node.elseExpression.staticType)) {
return;
}
// Check if this is in a color-only context (no icon nearby)
// Look for Icon, errorText, or helperText in surrounding context
AstNode? current = node.parent;
int depth = 0;
bool hasNonColorIndicator = false;
while (current != null && depth < 10) {
final String nodeSource = current.toSource();
final bool hasIconOrText = _nonColorIndicatorPatterns
.sublist(0, 5)
.any((re) => re.hasMatch(nodeSource));
final bool hasDecorationAndError =
_nonColorIndicatorPatterns[5].hasMatch(nodeSource) &&
_nonColorIndicatorPatterns[6].hasMatch(nodeSource);
if (hasIconOrText || hasDecorationAndError) {
hasNonColorIndicator = true;
break;
}
current = current.parent;
depth++;
}
if (!hasNonColorIndicator) {
reporter.atNode(node);
}
});
}