Plugin: Import assists for column nullability

This commit is contained in:
Simon Binder 2019-09-08 21:36:26 +02:00
parent d79b04193c
commit 1b7721a98f
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 226 additions and 10 deletions

View File

@ -6,7 +6,7 @@ plugin. To set up the plugin, run the following steps
1. Change the file `moor/tools/analyzer_plugin/pubspec.yaml` so that the `dependency_overrides`
section points to the location where you cloned this repository. This is needed because we
can't use relative paths for dependencies in analyzer plugins yet.
can't use relative paths for dependencies in analyzer plugins yet- see https://dartbug.com/35281
2. In VS Code, change `dart.additionalAnalyzerFileExtensions` to include `moor` files:
```json
{
@ -15,10 +15,14 @@ plugin. To set up the plugin, run the following steps
]
}
```
To diagnose errors with the plugin, turning on the diagnostics server by setting a
`dart.analyzerDiagnosticsPort` and enabling the instrumentation log via `dart.analyzerInstrumentationLogFile`
is recommended as well.
3. If you already had the project open, close and re-open VS Code. Otherwise, simply open this
project.
4. Type around in a `.moor` file - notice how you still don't get syntax highlighting because
VS Code required a static grammar for files and can't use a language server for that :(
4. Type around in a `.moor` file.
5. Notice how you don't see anything (https://github.com/Dart-Code/Dart-Code/issues/1981), but
at least the plugin output appears in the instrumentation log.
Debugging plugins is not fun. See the [docs](https://github.com/dart-lang/sdk/blob/master/pkg/analyzer_plugin/doc/tutorial/debugging.md)
on some general guidance, and good luck. Enabling the analyzer diagnostics server can help.

View File

@ -42,6 +42,8 @@ abstract class FileTask<R extends ParsedFile> {
final ErrorSink errors = ErrorSink();
String get path => backendTask.entrypoint.path;
FileTask(this.backendTask, this.session);
void reportError(MoorError error) => errors.report(error);

View File

@ -1,17 +1,20 @@
import 'package:analyzer/src/context/context_root.dart'; // ignore: implementation_imports
import 'package:analyzer/src/context/builder.dart'; // ignore: implementation_imports
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer_plugin/plugin/assist_mixin.dart';
import 'package:analyzer_plugin/plugin/completion_mixin.dart';
import 'package:analyzer_plugin/plugin/folding_mixin.dart';
import 'package:analyzer_plugin/plugin/highlights_mixin.dart';
import 'package:analyzer_plugin/plugin/outline_mixin.dart';
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/utilities/assist/assist.dart';
import 'package:analyzer_plugin/utilities/completion/completion_core.dart';
import 'package:analyzer_plugin/utilities/folding/folding.dart';
import 'package:analyzer_plugin/utilities/highlights/highlights.dart';
import 'package:analyzer_plugin/utilities/outline/outline.dart';
import 'package:moor_generator/src/backends/plugin/backend/file_tracker.dart';
import 'package:moor_generator/src/backends/plugin/services/assists/assist_service.dart';
import 'package:moor_generator/src/backends/plugin/services/autocomplete.dart';
import 'package:moor_generator/src/backends/plugin/services/errors.dart';
import 'package:moor_generator/src/backends/plugin/services/folding.dart';
@ -23,7 +26,12 @@ import 'backend/driver.dart';
import 'backend/logger.dart';
class MoorPlugin extends ServerPlugin
with OutlineMixin, HighlightsMixin, FoldingMixin, CompletionMixin {
with
OutlineMixin,
HighlightsMixin,
FoldingMixin,
CompletionMixin,
AssistsMixin {
MoorPlugin(ResourceProvider provider) : super(provider) {
setupLogger(this);
}
@ -118,7 +126,7 @@ class MoorPlugin extends ServerPlugin
@override
List<CompletionContributor> getCompletionContributors(String path) {
return [const MoorCompletingContributor()];
return const [MoorCompletingContributor()];
}
@override
@ -130,4 +138,20 @@ class MoorPlugin extends ServerPlugin
return MoorCompletionRequest(parameters.offset, resourceProvider, task);
}
@override
List<AssistContributor> getAssistContributors(String path) {
return const [AssistService()];
}
@override
Future<AssistRequest> getAssistRequest(
plugin.EditGetAssistsParams parameters) async {
final path = parameters.file;
final driver = _moorDriverForPath(path);
final task = await driver.parseMoorFile(path);
return MoorAssistRequest(
task, parameters.length, parameters.offset, resourceProvider);
}
}

View File

@ -0,0 +1,47 @@
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/utilities/assist/assist.dart';
import 'package:moor_generator/src/backends/plugin/services/requests.dart';
import 'package:sqlparser/sqlparser.dart';
part 'column_nullability.dart';
class AssistService implements AssistContributor {
const AssistService();
final _nullability = const ColumnNullability();
@override
void computeAssists(AssistRequest request, AssistCollector collector) {
final moorRequest = request as MoorAssistRequest;
final parseResult = moorRequest.task.lastResult.parseResult;
final relevantNodes =
parseResult.findNodesAtPosition(request.offset, length: request.length);
for (var node in relevantNodes.expand((node) => node.selfAndParents)) {
_handleNode(collector, node, moorRequest.task.path);
}
}
void _handleNode(AssistCollector collector, AstNode node, String path) {
if (node is ColumnDefinition) {
_nullability.contribute(collector, node, path);
}
}
}
abstract class _AssistOnNodeContributor<T extends AstNode> {
const _AssistOnNodeContributor();
void contribute(AssistCollector collector, T node, String path);
}
class AssistId {
final String id;
final int priority;
const AssistId._(this.id, this.priority);
static const makeNullable = AssistId._('make_column_nullable', 100);
static const makeNotNull = AssistId._('make_column_not_nullable', 10);
}

View File

@ -0,0 +1,44 @@
part of 'assist_service.dart';
class ColumnNullability extends _AssistOnNodeContributor<ColumnDefinition> {
const ColumnNullability();
@override
void contribute(
AssistCollector collector, ColumnDefinition node, String path) {
final notNull = node.findConstraint<NotNull>();
if (notNull == null) {
// there is no not-null constraint on this column, suggest to add one at
// the end of the definition
final end = node.lastPosition;
final id = AssistId.makeNotNull;
collector.addAssist(PrioritizedSourceChange(
id.priority,
SourceChange('Add a NOT NULL constraint', id: id.id, edits: [
SourceFileEdit(
path,
-1,
edits: [
SourceEdit(end, 0, ' NOT NULL'),
],
)
]),
));
} else {
// suggest to remove the NOT NULL constraint, e.g. to make this column
// nullable
final id = AssistId.makeNullable;
collector.addAssist(PrioritizedSourceChange(
id.priority,
SourceChange('Make this column nullable', id: id.id, edits: [
SourceFileEdit(path, -1, edits: [
SourceEdit(notNull.firstPosition, notNull.lastPosition, '')
])
]),
));
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer_plugin/utilities/assist/assist.dart';
import 'package:analyzer_plugin/utilities/completion/completion_core.dart';
import 'package:analyzer_plugin/utilities/folding/folding.dart';
import 'package:analyzer_plugin/utilities/highlights/highlights.dart';
@ -32,3 +33,18 @@ class MoorCompletionRequest extends CompletionRequest {
MoorCompletionRequest(this.offset, this.resourceProvider, this.task);
}
class MoorAssistRequest extends AssistRequest {
final MoorTask task;
@override
final int length;
@override
final int offset;
@override
final ResourceProvider resourceProvider;
MoorAssistRequest(this.task, this.length, this.offset, this.resourceProvider);
}

View File

@ -56,7 +56,10 @@ abstract class AstNode {
/// The last position that belongs to node, exclusive. Not set for all nodes.
int get lastPosition => last.span.end.offset;
FileSpan get span => first.span.expand(last.span);
FileSpan get span {
if (first == null || last == null) return null;
return first.span.expand(last.span);
}
/// Sets the [AstNode.first] and [AstNode.last] property in one go.
void setSpan(Token first, Token last) {
@ -74,6 +77,12 @@ abstract class AstNode {
}
}
/// Returns an iterable containing `this` node and all [parents].
Iterable<AstNode> get selfAndParents sync* {
yield this;
yield* parents;
}
/// Recursively returns all descendants of this node, e.g. its children, their
/// children and so on. The tree will be pre-order traversed.
Iterable<AstNode> get allDescendants sync* {

View File

@ -21,6 +21,15 @@ class ColumnDefinition extends AstNode {
bool contentEquals(ColumnDefinition other) {
return other.columnName == columnName && other.typeName == typeName;
}
/// Finds a constraint of type [T], or null, if none is set.
T findConstraint<T extends ColumnConstraint>() {
final typedConstraints = constraints.whereType<T>().iterator;
if (typedConstraints.moveNext()) {
return typedConstraints.current;
}
return null;
}
}
/// https://www.sqlite.org/syntax/column-constraint.html
@ -77,6 +86,9 @@ enum ConflictClause { rollback, abort, fail, ignore, replace }
class NotNull extends ColumnConstraint {
final ConflictClause onConflict;
Token not;
Token $null;
NotNull(String name, {this.onConflict}) : super(name);
@override

View File

@ -1,3 +1,5 @@
import 'dart:collection';
import 'package:sqlparser/src/analysis/analysis.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/autocomplete/engine.dart';
@ -87,7 +89,6 @@ class SqlEngine {
/// [registerTable] before calling this method.
AnalysisContext analyzeParsed(ParseResult result) {
final node = result.rootNode;
const SetParentVisitor().startAtRoot(node);
final context = AnalysisContext(node, result.sql);
final scope = _constructRootScope();
@ -132,5 +133,39 @@ class ParseResult {
final AutoCompleteEngine autoCompleteEngine;
ParseResult._(this.rootNode, this.tokens, this.errors, this.sql,
this.autoCompleteEngine);
this.autoCompleteEngine) {
const SetParentVisitor().startAtRoot(rootNode);
}
/// Attempts to find the most relevant (bottom-most in the AST) nodes that
/// intersects with the source range from [offset] to [offset] + [length].
List<AstNode> findNodesAtPosition(int offset, {int length = 0}) {
if (tokens.isEmpty || rootNode == null) return const [];
final candidates = <AstNode>{};
final unchecked = Queue<AstNode>();
unchecked.add(rootNode);
while (unchecked.isNotEmpty) {
final node = unchecked.removeFirst();
final span = node.span;
final start = span.start.offset;
final end = span.end.offset;
final hasIntersection = !(end < offset || start > offset + length);
if (hasIntersection) {
// this node matches. As we want to find the bottom-most node in the AST
// that matches, this means that the parent is no longer a candidate.
candidates.add(node);
candidates.remove(node.parent);
// assume that the span of a node is a superset of the span of any
// child, so each child could potentially be interesting.
unchecked.addAll(node.childNodes);
}
}
return candidates.toList();
}
}

View File

@ -129,10 +129,14 @@ mixin SchemaParser on ParserBase {
..setSpan(first, _previous);
}
if (_matchOne(TokenType.not)) {
final notToken = _previous;
final nullToken =
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
return NotNull(resolvedName, onConflict: _conflictClauseOrNull())
..setSpan(first, _previous);
..setSpan(first, _previous)
..not = notToken
..$null = nullToken;
}
if (_matchOne(TokenType.unique)) {
return UniqueColumn(resolvedName, _conflictClauseOrNull())

View File

@ -0,0 +1,19 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
test('finds the most relevant node', () {
final engine = SqlEngine();
final result = engine.parse('SELECT * FROM tbl;');
// | this is offset 8
// | this is offset 17
final mostRelevantAtStar = result.findNodesAtPosition(8);
expect(mostRelevantAtStar.length, 1);
expect(mostRelevantAtStar.single, const TypeMatcher<StarResultColumn>());
final mostRelevantAtTbl = result.findNodesAtPosition(17, length: 2);
expect(mostRelevantAtTbl.length, 1);
expect(mostRelevantAtTbl.single, const TypeMatcher<TableReference>());
});
}