drift/sqlparser/lib/src/analysis/steps/linting_visitor.dart

102 lines
3.0 KiB
Dart

part of '../analysis.dart';
/// Visitor that runs after all other steps ran and reports more complex lints
/// on an sql statement.
class LintingVisitor extends RecursiveVisitor<void, void> {
final EngineOptions options;
final AnalysisContext context;
LintingVisitor(this.options, this.context);
@override
void visitInvocation(SqlInvocation e, void arg) {
final lowercaseCall = e.name.toLowerCase();
if (options.addedFunctions.containsKey(lowercaseCall)) {
options.addedFunctions[lowercaseCall].reportErrors(e, context);
}
visitChildren(e, arg);
}
@override
void visitTuple(Tuple e, void arg) {
if (!e.usedAsRowValue) return;
bool isRowValue(Expression expr) => expr is Tuple || expr is SubQuery;
var parent = e.parent;
var isAllowed = false;
if (parent is WhenComponent && e == parent.when) {
// look at the surrounding case expression
parent = parent.parent;
}
if (parent is BinaryExpression) {
// Source: https://www.sqlite.org/rowvalue.html#syntax
const allowedTokens = [
TokenType.less,
TokenType.lessEqual,
TokenType.more,
TokenType.moreEqual,
TokenType.equal,
TokenType.doubleEqual,
TokenType.lessMore,
TokenType.$is,
];
if (allowedTokens.contains(parent.operator.type)) {
isAllowed = true;
}
} else if (parent is BetweenExpression) {
// Allowed if all value are row values or subqueries
isAllowed = !parent.childNodes.any((e) => !isRowValue(e));
} else if (parent is CaseExpression) {
// Allowed if we have something to compare against and all comparisons
// are row values
if (parent.base == null) {
isAllowed = false;
} else {
final comparisons = <Expression>[
parent.base,
for (final branch in parent.whens) branch.when
];
isAllowed = !comparisons.any((e) => !isRowValue(e));
}
} else if (parent is InExpression) {
// In expressions are tricky. The rhs can always be a row value, but the
// lhs can only be a row value if the rhs is a subquery
isAllowed = e == parent.inside || parent.inside is SubQuery;
}
if (!isAllowed) {
context.reportError(AnalysisError(
type: AnalysisErrorType.rowValueMisuse,
relevantNode: e,
message: 'Row values can only be used as expressions in comparisons',
));
}
}
@override
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
final expectedColumns = e.resolvedColumns.length;
for (final tuple in e.values) {
final elementsInTuple = tuple.expressions.length;
if (elementsInTuple != expectedColumns) {
context.reportError(AnalysisError(
type: AnalysisErrorType.valuesSelectCountMismatch,
relevantNode: tuple,
message: 'The surrounding VALUES clause has $expectedColumns '
'columns, but this tuple only has $elementsInTuple',
));
}
}
visitChildren(e, arg);
}
}