Static analysis for moor_ffi functions

This commit is contained in:
Simon Binder 2020-02-19 12:25:40 +01:00
parent e536761295
commit 990755f170
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
7 changed files with 229 additions and 13 deletions

View File

@ -0,0 +1,57 @@
import 'package:sqlparser/sqlparser.dart';
class MoorFfiExtension implements Extension {
const MoorFfiExtension();
@override
void register(SqlEngine engine) {
engine.registerFunctionHandler(const _MoorFfiFunctions());
}
}
class _MoorFfiFunctions with ArgumentCountLinter implements FunctionHandler {
const _MoorFfiFunctions();
static const Set<String> _unaryFunctions = {
'sqrt',
'sin',
'cos',
'tan',
'asin',
'acos',
'atan'
};
@override
Set<String> get functionNames => const {'pow', ..._unaryFunctions};
@override
int argumentCountFor(String function) {
if (_unaryFunctions.contains(function)) {
return 1;
} else if (function == 'pow') {
return 2;
}
// ignore: avoid_returning_null
return null;
}
@override
ResolveResult inferArgumentType(
AnalysisContext context, SqlInvocation call, Expression argument) {
return const ResolveResult(
ResolvedType(type: BasicType.real, nullable: false));
}
@override
ResolveResult inferReturnType(AnalysisContext context, SqlInvocation call,
List<Typeable> expandedArgs) {
return const ResolveResult(
ResolvedType(type: BasicType.real, nullable: true));
}
@override
void reportErrors(SqlInvocation call, AnalysisContext context) {
reportMismatches(call, context);
}
}

View File

@ -94,4 +94,9 @@ enum SqlModule {
/// Enables support for the fts5 module and its functions when parsing sql
/// queries.
fts5,
/// Enables support for mathematical functions only available in `moor_ffi`.
// note: We're ignoring the warning because we can't change the json key
// ignore: constant_identifier_names
moor_ffi,
}

View File

@ -18,7 +18,7 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'generate_connect_constructor',
'use_experimental_inference',
'sqlite_modules',
'eagerly_load_dart_ast',
'eagerly_load_dart_ast'
]);
final val = MoorOptions(
generateFromJsonStringConstructor: $checkedConvert(
@ -70,6 +70,7 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'use_column_name_as_json_key_when_defined_in_moor_file',
'generateConnectConstructor': 'generate_connect_constructor',
'useExperimentalInference': 'use_experimental_inference',
'eagerlyLoadDartAst': 'eagerly_load_dart_ast',
'modules': 'sqlite_modules'
});
}
@ -109,4 +110,5 @@ T _$enumDecodeNullable<T>(
const _$SqlModuleEnumMap = {
SqlModule.json1: 'json1',
SqlModule.fts5: 'fts5',
SqlModule.moor_ffi: 'moor_ffi',
};

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/moor/moor_ffi_extension.dart';
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:moor_generator/src/analyzer/runner/task.dart';
import 'package:moor_generator/src/backends/backend.dart';
@ -41,6 +42,7 @@ class MoorSession {
enabledExtensions: [
if (options.hasModule(SqlModule.fts5)) const Fts5Extension(),
if (options.hasModule(SqlModule.json1)) const Json1Extension(),
if (options.hasModule(SqlModule.moor_ffi)) const MoorFfiExtension(),
],
enableExperimentalTypeInference: options.useExperimentalInference,
);

View File

@ -0,0 +1,115 @@
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/moor/moor_ffi_extension.dart';
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:test/test.dart';
import '../utils.dart';
void main() {
SqlEngine engine;
setUp(() {
engine = SqlEngine(
EngineOptions(enabledExtensions: const [MoorFfiExtension()]),
);
});
group('reports errors', () {
test('when pow is called with 3 arguments', () {
final result = engine.analyze('SELECT pow(1, 2, 3);');
expect(result.errors, [
const TypeMatcher<AnalysisError>()
.having(
(source) => source.message,
'message',
allOf(contains('2'), contains('3'), contains('pow expects')),
)
.having(
(source) => source.span.text,
'span.text',
'pow(1, 2, 3)',
),
]);
});
test('when an unary function is called with 2 arguments', () {
final result = engine.analyze('SELECT sin(1, 2);');
expect(result.errors, [
const TypeMatcher<AnalysisError>()
.having(
(source) => source.message,
'message',
allOf(contains('2'), contains('1'), contains('sin expects')),
)
.having(
(source) => source.span.text,
'span.text',
'sin(1, 2)',
),
]);
});
});
test('infers return type', () {
final result = engine.analyze('SELECT pow(2.5, 3);');
final stmt = result.root as SelectStatement;
expect(stmt.resolvedColumns.map(result.typeOf), [
const ResolveResult(ResolvedType(type: BasicType.real, nullable: true))
]);
});
test('infers argument type', () {
final result = engine.analyze('SELECT pow(2.5, ?);');
final variable = result.root.allDescendants.whereType<Variable>().first;
expect(
result.typeOf(variable),
const ResolveResult(ResolvedType(type: BasicType.real, nullable: false)),
);
});
test('integration tests with moor files and experimental inference',
() async {
final state = TestState.withContent(
const {
'foo|lib/a.moor': '''
CREATE TABLE numbers (foo REAL NOT NULL);
query: SELECT pow(oid, foo) FROM numbers;
''',
'foo|lib/b.moor': '''
import 'a.moor';
wrongArgs: SELECT sin(oid, foo) FROM numbers;
'''
},
options: const MoorOptions(
useExperimentalInference: true,
modules: [SqlModule.moor_ffi],
),
);
final fileA = await state.analyze('package:foo/a.moor');
expect(fileA.errors.errors, isEmpty);
final resultA = fileA.currentResult as ParsedMoorFile;
final queryInA = resultA.resolvedQueries.single as SqlSelectQuery;
expect(
queryInA.resultSet.columns.single,
const TypeMatcher<ResultColumn>()
.having((e) => e.type, 'type', ColumnType.real),
);
final fileB = await state.analyze('package:foo/b.moor');
expect(fileB.errors.errors, [
const TypeMatcher<ErrorInMoorFile>()
.having((e) => e.span.text, 'span.text', 'sin(oid, foo)')
]);
});
}

View File

@ -53,7 +53,7 @@ class _Fts5Table extends Table {
}
/// Provides type inference and lints for
class _Fts5Functions implements FunctionHandler {
class _Fts5Functions with ArgumentCountLinter implements FunctionHandler {
const _Fts5Functions();
@override
@ -112,6 +112,15 @@ class _Fts5Functions implements FunctionHandler {
return const ResolveResult.unknown();
}
@override
int argumentCountFor(String function) {
return const {
'bm25': 1,
'highlight': 4,
'snippet': 6,
}[function];
}
@override
void reportErrors(SqlInvocation call, AnalysisContext context) {
// it doesn't make sense to call fts5 functions with a star parameter
@ -125,19 +134,10 @@ class _Fts5Functions implements FunctionHandler {
}
final args = (call.parameters as ExprFunctionParameters).parameters;
final expectedArgCount = const {
'bm25': 1,
'highlight': 4,
'snippet': 6,
}[call.name.toLowerCase()];
final expectedArgCount = argumentCountFor(call.name.toLowerCase());
if (expectedArgCount != args.length) {
context.reportError(AnalysisError(
relevantNode: call,
message: '${call.name} expects $expectedArgCount arguments, '
'got ${args.length}.',
type: AnalysisErrorType.other,
));
reportArgumentCountMismatch(call, context, expectedArgCount, args.length);
return;
}

View File

@ -47,6 +47,41 @@ abstract class FunctionHandler {
void reportErrors(SqlInvocation call, AnalysisContext context) {}
}
/// Should be mixed on on [FunctionHandler] implementations only.
mixin ArgumentCountLinter {
/// Returns the amount of arguments expected for [function] (lowercase).
///
/// If the function is unknown, or if the result would be ambiguous, returns
/// null.
int /*?*/ argumentCountFor(String function);
int actualArgumentCount(SqlInvocation call) {
return call.expandParameters().length;
}
void reportMismatches(SqlInvocation call, AnalysisContext context) {
final expectedArgs = argumentCountFor(call.name.toLowerCase());
if (expectedArgs != null) {
final actualArgs = actualArgumentCount(call);
if (actualArgs != expectedArgs) {
reportArgumentCountMismatch(call, context, expectedArgs, actualArgs);
}
}
}
void reportArgumentCountMismatch(
SqlInvocation call, AnalysisContext context, int expected, int actual) {
context.reportError(AnalysisError(
relevantNode: call,
message: '${call.name} expects $expected arguments, '
'got $actual.',
type: AnalysisErrorType.other,
));
}
}
/// Interface for a handler which can resolve the result set of a table-valued
/// function.
abstract class TableValuedFunctionHandler {