mirror of https://github.com/AMT-Cheif/drift.git
Static analysis for moor_ffi functions
This commit is contained in:
parent
e536761295
commit
990755f170
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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)')
|
||||
]);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue