import 'package:json_annotation/json_annotation.dart'; import 'package:sqlparser/sqlparser.dart'; import 'package:path/path.dart' show url; import '../utils/string_escaper.dart'; import 'backend.dart'; part '../generated/analysis/preprocess_drift.g.dart'; @JsonSerializable(constructor: '_') class DriftPreprocessorResult { /// A map from inline Dart lexemes used in a `.drift` file to the name of /// fields in a file generated to help analyze them. /// /// Public APIs in the `analyzer` (or `build_resolvers`) packages only support /// resolving full Dart files. In a `.drift` file however, it is possible to /// write in-line Dart expressions in SQL, for instance to declare a type /// converter with `MAPPED BY const MyTypeConverter()`. /// To enable drift's analyzer to see the `const MyTypeConverter()` expression /// in a meaningful way, a preprocess step generates a hidden Dart source file /// storing these expressions as text: /// /// ```dart /// var expr_1 = const MyTypeConverter(); /// ``` /// /// This map contains the lexemes of Dart expressions (like /// `const MyTypeConverter()`) as keys and maps to the name of fields to use /// (here, `expr_1`). final Map inlineDartExpressionsToHelperField; /// The names of all tables and views declared in this `.drift` file. /// /// Having this information available helps drift's analyzer in a future /// steps. When a table or view name is encountered in another `.drift` file, /// knowing where that table is likely to be defined helps doing analysis in /// the right order. final List declaredTablesAndViews; final List imports; DriftPreprocessorResult._(this.inlineDartExpressionsToHelperField, this.declaredTablesAndViews, this.imports); factory DriftPreprocessorResult.fromJson(Map json) => _$DriftPreprocessorResultFromJson(json); Map toJson() => _$DriftPreprocessorResultToJson(this); } /// Processes `.drift` files to extract all embedded Dart snippets. /// /// To analyze drift files in the build system, we extract these snippets into /// a standalone Dart file so that we're able to analyze them with the resolvers /// provided by the build system. class DriftPreprocessor { final DriftPreprocessorResult result; final String temporaryDartFile; DriftPreprocessor._(this.result, this.temporaryDartFile); static Iterable _imports(AstNode node, Uri ownUri) { return node.allDescendants .whereType() .map((stmt) => ownUri.resolve(stmt.importedFile)); } static Future analyze( DriftBackend backend, Uri uri) async { final contents = await backend.readAsString(uri); final engine = SqlEngine(EngineOptions( useDriftExtensions: true, version: SqliteVersion.current)); final parsedInput = engine.parseDriftFile(contents); final directImports = _imports(parsedInput.rootNode, uri).toList(); // Generate a hidden Dart helper file if this drift file uses inline Dart // expressions. final dartLexemes = parsedInput.tokens .whereType() .map((token) => token.dartCode) .toList(); var dartHelperFile = ''; final codeToField = {}; if (dartLexemes.isNotEmpty) { // Imports in drift files are transitive, so we need to find all // transitive Dart sources to import into the generated helper file. final seenFiles = {uri}; final queue = [...directImports]; while (queue.isNotEmpty) { final foundImport = queue.removeLast(); if (!seenFiles.contains(foundImport)) { seenFiles.add(foundImport); final extension = url.extension(foundImport.path); if (extension == '.moor' || extension == '.drift') { ParseResult parsed; try { parsed = engine .parseDriftFile(await backend.readAsString(foundImport)); } catch (e, s) { // Not being able to read or parse this file isn't critical, we'll // just ignore the imports it contributes. // The main analysis step will definitely warn about the import // not existing or parse errors afterwards, so there's no need to // warn twice. backend.log.fine('Could not read or parse $foundImport', e, s); continue; } _imports(parsed.rootNode, foundImport) .where((importedId) => !seenFiles.contains(importedId) && !queue.contains(importedId)) .forEach(queue.add); } } } final importedDartFiles = seenFiles.where((uri) => url.extension(uri.path) == '.dart'); // to analyze the expressions, generate a fake Dart file that declares each // expression in a `var`, we can then read the static type when resolving // file later. final dartBuffer = StringBuffer(); for (final import in importedDartFiles) { final importUri = import.toString(); dartBuffer.writeln('import ${asDartLiteral(importUri)};'); } for (var i = 0; i < dartLexemes.length; i++) { final name = 'expr_$i'; dartBuffer.writeln('var $name = ${dartLexemes[i]};'); codeToField[dartLexemes[i]] = name; } dartHelperFile = dartBuffer.toString(); } final declaredTablesAndViews = []; for (final entry in parsedInput.rootNode.childNodes) { if (entry is CreateTableStatement) { declaredTablesAndViews.add(entry.tableName); } else if (entry is CreateViewStatement) { declaredTablesAndViews.add(entry.viewName); } } final result = DriftPreprocessorResult._( codeToField, declaredTablesAndViews, directImports.toList(), ); return DriftPreprocessor._(result, dartHelperFile); } }