Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linter for drift #3281

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ docs/.cache
docs/docs/*.js
docs/docs/*.wasm
docs/docs/examples/**
docs/web/robots.txt
docs/web/robots.txt

# Linting
custom_lint.log
7 changes: 7 additions & 0 deletions drift_dev/lib/drift_dev.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:drift_dev/src/lints/custom_lint_plugin.dart';

/// This function is automaticly recognized by custom_lint to include this drift_dev package as a linter
PluginBase createPlugin() {
return DriftLinter();
}
30 changes: 18 additions & 12 deletions drift_dev/lib/src/analysis/driver/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,45 @@ import 'package:source_gen/source_gen.dart';
import 'package:source_span/source_span.dart';
import 'package:sqlparser/sqlparser.dart' as sql;

enum DriftAnalysisErrorLevel { warning, error }

class DriftAnalysisError {
final SourceSpan? span;
final String message;
final DriftAnalysisErrorLevel level;

DriftAnalysisError(this.span, this.message);
DriftAnalysisError(this.span, this.message,
{this.level = DriftAnalysisErrorLevel.error});

factory DriftAnalysisError.forDartElement(
dart.Element element, String message) {
return DriftAnalysisError(
spanForElement(element),
message,
);
dart.Element element, String message,
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
return DriftAnalysisError(spanForElement(element), message, level: level);
}

factory DriftAnalysisError.inDartAst(
dart.Element element, dart.SyntacticEntity entity, String message) {
return DriftAnalysisError(dartAstSpan(element, entity), message);
dart.Element element, dart.SyntacticEntity entity, String message,
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
return DriftAnalysisError(dartAstSpan(element, entity), message,
level: level);
}

factory DriftAnalysisError.inDriftFile(
sql.SyntacticEntity sql, String message) {
return DriftAnalysisError(sql.span, message);
sql.SyntacticEntity sql, String message,
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
return DriftAnalysisError(sql.span, message, level: level);
}

factory DriftAnalysisError.fromSqlError(sql.AnalysisError error) {
factory DriftAnalysisError.fromSqlError(sql.AnalysisError error,
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
var message = error.message ?? '';
if (error.type == sql.AnalysisErrorType.notSupportedInDesiredVersion) {
message =
'$message\nNote: You can change the assumed sqlite version with build '
'options. See https://drift.simonbinder.eu/options/#assumed-sql-environment for details!';
}

return DriftAnalysisError(error.span, message);
return DriftAnalysisError(error.span, message, level: level);
}

@override
Expand Down
6 changes: 5 additions & 1 deletion drift_dev/lib/src/analysis/resolver/dart/column.dart
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ class ColumnParser {
customConstraints: foundCustomConstraint,
referenceName: _readReferenceName(element),
),
element: element,
referencesColumnInSameTable: referencesColumnInSameTable,
);
}
Expand Down Expand Up @@ -665,5 +666,8 @@ class PendingColumnInformation {
/// this column in that case.
final String? referencesColumnInSameTable;

PendingColumnInformation(this.column, {this.referencesColumnInSameTable});
final Element element;

PendingColumnInformation(this.column,
{this.referencesColumnInSameTable, required this.element});
}
8 changes: 8 additions & 0 deletions drift_dev/lib/src/analysis/resolver/dart/table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
} else {
for (final constraint in column.column.constraints) {
if (constraint is ForeignKeyReference) {
if (column.column.sqlType.builtin !=
constraint.otherColumn.sqlType.builtin ||
column.column.typeConverter?.dartType !=
constraint.otherColumn.typeConverter?.dartType) {
reportError(DriftAnalysisError.forDartElement(column.element,
"This column references a column whose type doesn't match this one. The generated managers will ignore this relation",
level: DriftAnalysisErrorLevel.warning));
}
references.add(constraint.otherColumn.owner);
}
}
Expand Down
16 changes: 16 additions & 0 deletions drift_dev/lib/src/lints/custom_lint_plugin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:drift_dev/src/lints/offset_without_limit.dart';
import 'package:drift_dev/src/lints/drift_backend_errors.dart';
import 'package:drift_dev/src/lints/unawaited_futures.dart';
import 'package:meta/meta.dart';

@internal
class DriftLinter extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) => [
UnawaitedFuturesInTransaction(),
UnawaitedFuturesInMigration(),
DriftBuildErrors(),
OffsetWithoutLimit()
];
}
113 changes: 113 additions & 0 deletions drift_dev/lib/src/lints/drift_backend_errors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'dart:io';

import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart' hide LintCode;
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:drift_dev/src/analysis/backend.dart';
import 'package:drift_dev/src/analysis/driver/error.dart';
import 'package:drift_dev/src/analysis/options.dart';
import 'package:logging/logging.dart';

import '../analysis/driver/driver.dart';

final columnBuilderChecker =
TypeChecker.fromName('DriftDatabase', packageName: 'drift');

class DriftBuildErrors extends DartLintRule {
DriftBuildErrors() : super(code: _code);

static const _code = LintCode(
name: 'drift_build_errors',
problemMessage: '{0}',
errorSeverity: ErrorSeverity.ERROR,
);

LintCode get _codeAsWarning => LintCode(
name: _code.name,
problemMessage: _code.problemMessage,
errorSeverity: ErrorSeverity.WARNING);

@override
void run(CustomLintResolver resolver, ErrorReporter reporter,
CustomLintContext context) async {
final unit = await resolver.getResolvedUnitResult();
final backend = CustomLintBackend(unit.session);
final driver = DriftAnalysisDriver(backend, const DriftOptions.defaults());

final file = await driver.fullyAnalyze(unit.uri);
for (final error in file.allErrors) {
if (error.span case final span?) {
reporter.reportErrorForSpan(
error.level == DriftAnalysisErrorLevel.warning
? _codeAsWarning
: _code,
span,
[error.message.trim()]);
}
}
}
}

class CustomLintBackend extends DriftBackend {
@override
final Logger log = Logger('drift_dev.CustomLintBackend');
final AnalysisSession session;

CustomLintBackend(this.session);

@override
bool get canReadDart => true;

@override
Future<AstNode?> loadElementDeclaration(Element element) async {
final library = element.library;
if (library == null) return null;

final info = await library.session.getResolvedLibraryByElement(library);
if (info is ResolvedLibraryResult) {
return info.getElementDeclaration(element)?.node;
} else {
return null;
}
}

@override
Future<String> readAsString(Uri uri) async {
final file = session.getFile(uri.path);

if (file is FileResult) {
return file.content;
}

throw FileSystemException('Not a file result: $file');
}

@override
Future<LibraryElement> readDart(Uri uri) async {
final result = await session.getLibraryByUri(uri.toString());
if (result is LibraryElementResult) {
return result.element;
}

throw NotALibraryException(uri);
}

@override
Future<Expression> resolveExpression(
Uri context, String dartExpression, Iterable<String> imports) {
throw CannotReadExpressionException('Not supported at the moment');
}

@override
Future<Element?> resolveTopLevelElement(
Uri context, String reference, Iterable<Uri> imports) {
throw UnimplementedError();
}

@override
Uri resolveUri(Uri base, String uriString) => base.resolve(uriString);
}
51 changes: 51 additions & 0 deletions drift_dev/lib/src/lints/offset_without_limit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart' hide LintCode;
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';

final managerTypeChecker =
TypeChecker.fromName('BaseTableManager', packageName: 'drift');

class OffsetWithoutLimit extends DartLintRule {
OffsetWithoutLimit() : super(code: _code);

static const _code = LintCode(
name: 'offset_without_limit',
problemMessage: 'Using offset without a limit will result in a ',
errorSeverity: ErrorSeverity.ERROR,
);

@override
void run(CustomLintResolver resolver, ErrorReporter reporter,
CustomLintContext context) async {
context.registry.addMethodInvocation(
(node) {
if (node.argumentList.arguments.isEmpty) return;
final func = _typeCheck<SimpleIdentifier>(node.function);

if (func?.name == "get" || func?.name == "watch") {
final target = _typeCheck<PrefixedIdentifier>(node.target);
final managerGetter =
_typeCheck<PropertyAccessorElement>(target?.staticElement);
if (managerGetter != null) {
if (managerTypeChecker.isSuperTypeOf(managerGetter.returnType)) {
final namedArgs =
node.argumentList.arguments.whereType<NamedExpression>();
if (namedArgs
.every((element) => element.name.label.name != "limit") &&
namedArgs
.any((element) => element.name.label.name == "offset")) {
reporter.atNode(node, _code);
}
}
}
}
},
);
}
}

T? _typeCheck<T>(i) {
return i is T ? i : null;
}
Loading
Loading