mirror of https://github.com/AMT-Cheif/drift.git
Plugin: Import assists for column nullability
This commit is contained in:
parent
d79b04193c
commit
1b7721a98f
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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, '')
|
||||
])
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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* {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,10 +129,14 @@ mixin SchemaParser on ParserBase {
|
|||
..setSpan(first, _previous);
|
||||
}
|
||||
if (_matchOne(TokenType.not)) {
|
||||
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
|
||||
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())
|
||||
|
|
|
@ -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>());
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue