diff --git a/analysis_options.yaml b/analysis_options.yaml index bb75690b..1793a810 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -28,7 +28,6 @@ linter: - await_only_futures - camel_case_types - cancel_subscriptions - - cascade_invocations - comment_references - constant_identifier_names - curly_braces_in_flow_control_structures @@ -83,4 +82,4 @@ linter: - unnecessary_this - unrelated_type_equality_checks - use_rethrow_when_possible - - valid_regexps + - valid_regexps \ No newline at end of file diff --git a/extras/integration_tests/tests/analysis_options.yaml b/extras/integration_tests/tests/analysis_options.yaml new file mode 100644 index 00000000..e649ddc0 --- /dev/null +++ b/extras/integration_tests/tests/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + plugins: + - moor \ No newline at end of file diff --git a/extras/integration_tests/tests/lib/database/test.moor b/extras/integration_tests/tests/lib/database/test.moor new file mode 100644 index 00000000..e2d524e3 --- /dev/null +++ b/extras/integration_tests/tests/lib/database/test.moor @@ -0,0 +1,3 @@ +CREATE TABLE test ( + id INT NOT NULL PRIMARY AUTOINCREMENT +) \ No newline at end of file diff --git a/moor/tool/analyzer_plugin/bin/plugin.dart b/moor/tools/analyzer_plugin/bin/plugin.dart similarity index 100% rename from moor/tool/analyzer_plugin/bin/plugin.dart rename to moor/tools/analyzer_plugin/bin/plugin.dart diff --git a/moor/tool/analyzer_plugin/pubspec.yaml b/moor/tools/analyzer_plugin/pubspec.yaml similarity index 52% rename from moor/tool/analyzer_plugin/pubspec.yaml rename to moor/tools/analyzer_plugin/pubspec.yaml index a3dacbcb..e66dd028 100644 --- a/moor/tool/analyzer_plugin/pubspec.yaml +++ b/moor/tools/analyzer_plugin/pubspec.yaml @@ -3,8 +3,10 @@ version: 1.0.0 description: This pubspec is a part of moor and determines the version of the moor analyzer to load dependencies: - moor_generator: ^1.6.0 - -dependency_overrides: moor_generator: - path: ../../../moor_generator \ No newline at end of file + +#dependency_overrides: +# moor_generator: +# path: /home/simon/IdeaProjects/moor/moor_generator +# sqlparser: +# path: /home/simon/IdeaProjects/moor/sqlparser \ No newline at end of file diff --git a/moor_generator/lib/src/parser/moor/moor_analyzer.dart b/moor_generator/lib/src/parser/moor/moor_analyzer.dart index 342c27b4..c0a93e7c 100644 --- a/moor_generator/lib/src/parser/moor/moor_analyzer.dart +++ b/moor_generator/lib/src/parser/moor/moor_analyzer.dart @@ -11,7 +11,9 @@ class MoorAnalyzer { MoorAnalyzer(this.content); Future analyze() { - final results = SqlEngine().parseMultiple(content); + final engine = SqlEngine(); + final tokens = engine.tokenize(content); + final results = SqlEngine().parseMultiple(tokens, content); final createdTables = []; final errors = []; diff --git a/moor_generator/lib/src/plugin/analyzer/highlights/request.dart b/moor_generator/lib/src/plugin/analyzer/highlights/request.dart new file mode 100644 index 00000000..65f9fbee --- /dev/null +++ b/moor_generator/lib/src/plugin/analyzer/highlights/request.dart @@ -0,0 +1,14 @@ +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer_plugin/utilities/highlights/highlights.dart'; + +import '../results.dart'; + +class MoorHighlightingRequest extends HighlightsRequest { + @override + final String path; + @override + final ResourceProvider resourceProvider; + final MoorAnalysisResults parsedFile; + + MoorHighlightingRequest(this.parsedFile, this.path, this.resourceProvider); +} diff --git a/moor_generator/lib/src/plugin/analyzer/highlights/sql_highlighter.dart b/moor_generator/lib/src/plugin/analyzer/highlights/sql_highlighter.dart new file mode 100644 index 00000000..4eb3f0b1 --- /dev/null +++ b/moor_generator/lib/src/plugin/analyzer/highlights/sql_highlighter.dart @@ -0,0 +1,82 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/utilities/highlights/highlights.dart'; +import 'package:moor_generator/src/plugin/analyzer/highlights/request.dart'; +import 'package:sqlparser/sqlparser.dart'; + +const _notBuiltIn = { + TokenType.numberLiteral, + TokenType.stringLiteral, + TokenType.identifier, + TokenType.leftParen, + TokenType.rightParen, + TokenType.comma, + TokenType.star, + TokenType.less, + TokenType.lessEqual, + TokenType.lessMore, + TokenType.equal, + TokenType.more, + TokenType.moreEqual, + TokenType.shiftRight, + TokenType.shiftLeft, + TokenType.exclamationEqual, + TokenType.plus, + TokenType.minus, +}; + +class SqlHighlighter implements HighlightsContributor { + const SqlHighlighter(); + + @override + void computeHighlights( + HighlightsRequest request, HighlightsCollector collector) { + if (request is! MoorHighlightingRequest) { + return; + } + + final typedRequest = request as MoorHighlightingRequest; + final visitor = _HighlightingVisitor(collector); + + for (var stmt in typedRequest.parsedFile.statements) { + stmt.accept(visitor); + } + + for (var token in typedRequest.parsedFile.sqlTokens) { + if (!_notBuiltIn.contains(token.type)) { + final start = token.span.start.offset; + final length = token.span.length; + collector.addRegion(start, length, HighlightRegionType.BUILT_IN); + } + } + } +} + +class _HighlightingVisitor extends RecursiveVisitor { + final HighlightsCollector collector; + + _HighlightingVisitor(this.collector); + + void _contribute(AstNode node, HighlightRegionType type) { + final offset = node.firstPosition; + final length = node.lastPosition - offset; + collector.addRegion(offset, length, type); + } + + @override + void visitReference(Reference e) { + _contribute(e, HighlightRegionType.INSTANCE_FIELD_REFERENCE); + } + + @override + void visitLiteral(Literal e) { + if (e is NullLiteral) { + _contribute(e, HighlightRegionType.BUILT_IN); + } else if (e is NumericLiteral) { + _contribute(e, HighlightRegionType.LITERAL_INTEGER); + } else if (e is StringLiteral) { + _contribute(e, HighlightRegionType.LITERAL_STRING); + } else if (e is BooleanLiteral) { + _contribute(e, HighlightRegionType.LITERAL_BOOLEAN); + } + } +} diff --git a/moor_generator/lib/src/plugin/analyzer/moor_analyzer.dart b/moor_generator/lib/src/plugin/analyzer/moor_analyzer.dart new file mode 100644 index 00000000..a5456bfd --- /dev/null +++ b/moor_generator/lib/src/plugin/analyzer/moor_analyzer.dart @@ -0,0 +1,15 @@ +import 'package:analyzer/file_system/file_system.dart'; +import 'package:moor_generator/src/plugin/analyzer/results.dart'; +import 'package:sqlparser/sqlparser.dart'; + +class MoorAnalyzer { + Future analyze(File file) async { + final content = file.readAsStringSync(); + final sqlEngine = SqlEngine(); + + final tokens = sqlEngine.tokenize(content); + final stmts = sqlEngine.parseMultiple(tokens, content); + + return MoorAnalysisResults(stmts.map((r) => r.rootNode).toList(), tokens); + } +} diff --git a/moor_generator/lib/src/plugin/analyzer/results.dart b/moor_generator/lib/src/plugin/analyzer/results.dart new file mode 100644 index 00000000..9aa4cc2b --- /dev/null +++ b/moor_generator/lib/src/plugin/analyzer/results.dart @@ -0,0 +1,8 @@ +import 'package:sqlparser/sqlparser.dart'; + +class MoorAnalysisResults { + final List statements; + final List sqlTokens; + + MoorAnalysisResults(this.statements, this.sqlTokens); +} diff --git a/moor_generator/lib/src/plugin/driver.dart b/moor_generator/lib/src/plugin/driver.dart index b32e09f6..3f16e40d 100644 --- a/moor_generator/lib/src/plugin/driver.dart +++ b/moor_generator/lib/src/plugin/driver.dart @@ -1,40 +1,79 @@ // ignore_for_file: implementation_imports +import 'dart:async'; + +import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/src/dart/analysis/driver.dart'; +import 'package:moor_generator/src/plugin/state/file_tracker.dart'; + +import 'analyzer/moor_analyzer.dart'; +import 'analyzer/results.dart'; class MoorDriver implements AnalysisDriverGeneric { - final _addedFiles = {}; + final FileTracker _tracker; + final AnalysisDriverScheduler _scheduler; + final MoorAnalyzer _analyzer; + final ResourceProvider _resources; + + MoorDriver(this._tracker, this._scheduler, this._analyzer, this._resources) { + _scheduler.add(this); + } bool _ownsFile(String path) => path.endsWith('.moor'); @override void addFile(String path) { if (_ownsFile(path)) { - _addedFiles.add(path); - handleFileChanged(path); + _tracker.addFile(path); } } @override - void dispose() {} + void dispose() { + _scheduler.remove(this); + } void handleFileChanged(String path) { - if (_ownsFile(path)) {} + if (_ownsFile(path)) { + _tracker.handleContentChanged(path); + _scheduler.notify(this); + } } @override - bool get hasFilesToAnalyze => null; + bool get hasFilesToAnalyze => _tracker.hasWork; @override - Future performWork() { - // TODO: implement performWork - return null; + Future performWork() async { + final completer = Completer(); + + if (_tracker.hasWork) { + _tracker.work((path) { + try { + return _resolveMoorFile(path); + } finally { + completer.complete(); + } + }); + + await completer.future; + } + } + + Future _resolveMoorFile(String path) { + return _analyzer.analyze(_resources.getFile(path)); } @override set priorityFiles(List priorityPaths) { - // We don't support this ATM + _tracker.setPriorityFiles(priorityPaths); } @override + // todo ask the tracker about the top-priority file. AnalysisDriverPriority get workPriority => AnalysisDriverPriority.general; + + Future parseMoorFile(String path) { + _scheduler.notify(this); + return _tracker.results(path); + } } diff --git a/moor_generator/lib/src/plugin/plugin.dart b/moor_generator/lib/src/plugin/plugin.dart index 5a6505bb..3d6eacda 100644 --- a/moor_generator/lib/src/plugin/plugin.dart +++ b/moor_generator/lib/src/plugin/plugin.dart @@ -1,24 +1,66 @@ import 'package:analyzer/file_system/file_system.dart'; -// ignore: implementation_imports -import 'package:analyzer/src/dart/analysis/driver.dart'; +import 'package:analyzer_plugin/plugin/highlights_mixin.dart'; import 'package:analyzer_plugin/plugin/plugin.dart'; +import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:analyzer_plugin/utilities/highlights/highlights.dart'; +import 'package:moor_generator/src/plugin/state/file_tracker.dart'; -class MoorPlugin extends ServerPlugin { +import 'analyzer/highlights/request.dart'; +import 'analyzer/highlights/sql_highlighter.dart'; +import 'analyzer/moor_analyzer.dart'; +import 'driver.dart'; + +class MoorPlugin extends ServerPlugin with HighlightsMixin { MoorPlugin(ResourceProvider provider) : super(provider); @override - final List fileGlobsToAnalyze = const ['**/*.moor']; + final List fileGlobsToAnalyze = const ['*.moor']; @override final String name = 'Moor plugin'; @override - final String version = '0.0.1'; + // docs say that this should a version of _this_ plugin, but they lie. this + // version will be used to determine compatibility with the analyzer + final String version = '2.0.0-alpha.0'; @override final String contactInfo = 'Create an issue at https://github.com/simolus3/moor/'; @override - AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot) { - return null; + MoorDriver createAnalysisDriver(ContextRoot contextRoot) { + final tracker = FileTracker(); + final analyzer = MoorAnalyzer(); + return MoorDriver( + tracker, analysisDriverScheduler, analyzer, resourceProvider); + } + + @override + void contentChanged(String path) { + _moorDriverForPath(path)?.handleFileChanged(path); + } + + MoorDriver _moorDriverForPath(String path) { + final driver = super.driverForPath(path); + + if (driver is! MoorDriver) return null; + return driver as MoorDriver; + } + + @override + List getHighlightsContributors(String path) { + return const [SqlHighlighter()]; + } + + @override + Future getHighlightsRequest(String path) async { + final driver = _moorDriverForPath(path); + if (driver == null) { + throw RequestFailure( + RequestErrorFactory.pluginError('Not driver set for path', null)); + } + + final parsed = await driver.parseMoorFile(path); + + return MoorHighlightingRequest(parsed, path, resourceProvider); } } diff --git a/moor_generator/lib/src/plugin/state/file_tracker.dart b/moor_generator/lib/src/plugin/state/file_tracker.dart index bc19c46a..a77fdc58 100644 --- a/moor_generator/lib/src/plugin/state/file_tracker.dart +++ b/moor_generator/lib/src/plugin/state/file_tracker.dart @@ -1,5 +1,128 @@ -/// Keeps track of files that need to be analyzed by the moor generator. -class FileTracker {} +import 'dart:async'; -/// A `.moor` file added to the plugin. -class _TrackedFile {} +import 'package:collection/collection.dart'; +import 'package:moor_generator/src/plugin/analyzer/results.dart'; + +/// Keeps track of files that need to be analyzed by the moor plugin. +class FileTracker { + PriorityQueue _pendingWork; + final Map _trackedFiles = {}; + final Set _currentPriority = {}; + + FileTracker() { + _pendingWork = PriorityQueue(_compareByPriority); + } + + int _compareByPriority(TrackedFile a, TrackedFile b) { + final aPriority = a.currentPriority?.index ?? 0; + final bPriority = b.currentPriority?.index ?? 0; + return aPriority.compareTo(bPriority); + } + + void _updateFile(TrackedFile file, Function(TrackedFile) update) { + _pendingWork.remove(file); + update(file); + _pendingWork.add(file); + } + + void _putInQueue(TrackedFile file) { + _updateFile(file, (f) { + // no action needed, insert with current priority. + }); + } + + bool get hasWork => _pendingWork.isNotEmpty; + + TrackedFile addFile(String path) { + return _trackedFiles.putIfAbsent(path, () { + final tracked = TrackedFile(path); + _pendingWork.add(tracked); + return tracked; + }); + } + + void handleContentChanged(String path) { + _putInQueue(addFile(path)); + } + + void setPriorityFiles(List priority) { + // remove prioritized flag from existing files + for (var file in _currentPriority) { + _updateFile(file, (f) => f._prioritized = false); + } + _currentPriority + ..clear() + ..addAll(priority.map(addFile)) + ..forEach((file) { + _updateFile(file, (f) => f._prioritized = true); + }); + } + + void notifyFileChanged(String path) { + final tracked = addFile(path); + tracked._currentResult = null; + _putInQueue(tracked); + } + + Future results(String path) async { + final tracked = addFile(path); + + if (tracked._currentResult != null) { + return tracked._currentResult; + } else { + final completer = Completer(); + tracked._waiting.add(completer); + return completer.future; + } + } + + void work(Future Function(String path) worker) { + if (_pendingWork.isNotEmpty) { + final unit = _pendingWork.removeFirst(); + + worker(unit.path).then((result) { + for (var completer in unit._waiting) { + completer.complete(result); + } + unit._waiting.clear(); + }, onError: (e, StackTrace s) { + for (var completer in unit._waiting) { + completer.completeError(e, s); + } + unit._waiting.clear(); + }); + } + } +} + +enum FileType { moor, unknown } + +enum FilePriority { ignore, regular, interactive } + +const Map _defaultPrio = { + FileType.moor: FilePriority.regular, + FileType.unknown: FilePriority.ignore, +}; + +class TrackedFile { + final String path; + final FileType type; + + /// Whether this file has been given an elevated priority, for instance + /// because the user is currently typing in it. + bool _prioritized; + MoorAnalysisResults _currentResult; + final List> _waiting = []; + + FilePriority get currentPriority => + _prioritized ? FilePriority.interactive : defaultPriority; + + TrackedFile._(this.path, this.type); + + factory TrackedFile(String path) { + final type = path.endsWith('.moor') ? FileType.moor : FileType.unknown; + return TrackedFile._(path, type); + } + + FilePriority get defaultPriority => _defaultPrio[type]; +} diff --git a/moor_generator/pubspec.yaml b/moor_generator/pubspec.yaml index bc1e16d4..31a48e79 100644 --- a/moor_generator/pubspec.yaml +++ b/moor_generator/pubspec.yaml @@ -14,8 +14,9 @@ environment: dependencies: analyzer: '>=0.36.0 <0.38.0' analyzer_plugin: + collection: ^1.14.0 recase: ^2.0.1 - built_value: '>=6.3.0 <7.0.0' + built_value: ^6.3.0 source_gen: ^0.9.4 source_span: ^1.5.5 build: ^1.1.0 @@ -28,7 +29,7 @@ dev_dependencies: test_api: ^0.2.0 test_core: ^0.2.0 build_runner: '>=1.1.0 <1.6.0' - built_value_generator: '>=6.3.0 <7.0.0' + built_value_generator: ^6.3.0 build_test: '>=0.10.0 <0.11.0' dependency_overrides: diff --git a/sqlparser/lib/sqlparser.dart b/sqlparser/lib/sqlparser.dart index 21cd4a51..5de3d55a 100644 --- a/sqlparser/lib/sqlparser.dart +++ b/sqlparser/lib/sqlparser.dart @@ -5,4 +5,4 @@ export 'src/analysis/analysis.dart'; export 'src/ast/ast.dart'; export 'src/engine/sql_engine.dart'; export 'src/reader/parser/parser.dart' show ParsingError; -export 'src/reader/tokenizer/token.dart' show CumulatedTokenizerException; +export 'src/reader/tokenizer/token.dart' hide keywords; diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index bdda0f2c..6b2211cf 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -44,14 +44,13 @@ class SqlEngine { final parser = Parser(tokens); final stmt = parser.statement(); - return ParseResult._(stmt, parser.errors, sql); + return ParseResult._(stmt, tokens, parser.errors, sql); } /// Parses multiple sql statements, separated by a semicolon. All /// [ParseResult] entries will have the same [ParseResult.errors], but the /// [ParseResult.sql] will only refer to the substring creating a statement. - List parseMultiple(String sql) { - final tokens = tokenize(sql); + List parseMultiple(List tokens, String sql) { final parser = Parser(tokens); final stmts = parser.statements(); @@ -61,7 +60,7 @@ class SqlEngine { final last = statement.lastPosition; final source = sql.substring(first, last); - return ParseResult._(statement, parser.errors, source); + return ParseResult._(statement, tokens, parser.errors, source); }).toList(); } @@ -113,6 +112,9 @@ class ParseResult { /// The topmost node in the sql AST that was parsed. final AstNode rootNode; + /// The tokens that were scanned in the source file + final List tokens; + /// A list of all errors that occurred during parsing. [ParsingError.toString] /// returns a helpful description of what went wrong, along with the position /// where the error occurred. @@ -121,5 +123,5 @@ class ParseResult { /// The sql source that created the AST at [rootNode]. final String sql; - ParseResult._(this.rootNode, this.errors, this.sql); + ParseResult._(this.rootNode, this.tokens, this.errors, this.sql); }