Skip to content

Commit 3a97943

Browse files
committed
add linter
1 parent b3f5628 commit 3a97943

22 files changed

+940
-29
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ docs/docs/*.wasm
3737
docs/docs/*.css
3838
docs/docs/examples/**
3939
docs/web/robots.txt
40+
41+
# Linting
42+
custom_lint.log

docs/analysis_options.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
include: package:lints/recommended.yaml
22

33
analyzer:
4+
plugins:
5+
- custom_lint
46
language:
57
strict-casts: true
68
exclude:
79
- "**/*.g.dart"
10+
11+
custom_lint:
12+
debug: true
13+
# Optional, will cause custom_lint to log its internal debug information
14+
verbose: true

docs/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies:
1717
sqlite3: ^2.0.0
1818

1919
# Used in examples
20-
rxdart: ^0.27.3
20+
rxdart: ^0.28.0
2121
yaml: ^3.1.1
2222
drift_dev: any
2323
test: ^1.18.0
@@ -46,6 +46,7 @@ dev_dependencies:
4646
hosted: https://pub-simonbinder-eu.fsn1.your-objectstorage.com
4747
version: ^0.0.15
4848
sass_builder: ^2.2.1
49+
custom_lint: ^0.6.7
4950

5051
dependency_overrides:
5152
drift_flutter:

drift_dev/lib/drift_dev.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import 'package:custom_lint_builder/custom_lint_builder.dart';
2+
import 'package:drift_dev/src/lints/custom_lint_plugin.dart';
3+
4+
/// This function is automaticly recognized by custom_lint to include this drift_dev package as a linter
5+
PluginBase createPlugin() {
6+
return DriftLinter();
7+
}

drift_dev/lib/src/analysis/driver/error.dart

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,49 @@ import 'package:source_gen/source_gen.dart';
44
import 'package:source_span/source_span.dart';
55
import 'package:sqlparser/sqlparser.dart' as sql;
66

7+
enum DriftAnalysisErrorLevel { warning, error }
8+
79
class DriftAnalysisError {
810
final SourceSpan? span;
911
final String message;
12+
final DriftAnalysisErrorLevel level;
1013

11-
DriftAnalysisError(this.span, this.message);
14+
DriftAnalysisError(this.span, this.message,
15+
{this.level = DriftAnalysisErrorLevel.error});
1216

1317
factory DriftAnalysisError.forDartElement(
14-
dart.Element element, String message) {
18+
dart.Element element, String message,
19+
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
1520
return DriftAnalysisError(
1621
spanForElement(element),
1722
message,
23+
level: level,
1824
);
1925
}
2026

2127
factory DriftAnalysisError.inDartAst(
22-
dart.Element element, dart.SyntacticEntity entity, String message) {
23-
return DriftAnalysisError(dartAstSpan(element, entity), message);
28+
dart.Element element, dart.SyntacticEntity entity, String message,
29+
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
30+
return DriftAnalysisError(dartAstSpan(element, entity), message,
31+
level: level);
2432
}
2533

2634
factory DriftAnalysisError.inDriftFile(
27-
sql.SyntacticEntity sql, String message) {
28-
return DriftAnalysisError(sql.span, message);
35+
sql.SyntacticEntity sql, String message,
36+
{DriftAnalysisErrorLevel level = DriftAnalysisErrorLevel.error}) {
37+
return DriftAnalysisError(sql.span, message, level: level);
2938
}
3039

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

39-
return DriftAnalysisError(error.span, message);
49+
return DriftAnalysisError(error.span, message, level: level);
4050
}
4151

4252
@override

drift_dev/lib/src/analysis/resolver/dart/column.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ class ColumnParser {
509509
customConstraints: foundCustomConstraint,
510510
referenceName: _readReferenceName(element),
511511
),
512+
element: element,
512513
referencesColumnInSameTable: referencesColumnInSameTable,
513514
);
514515
}
@@ -661,6 +662,7 @@ class ColumnParser {
661662

662663
class PendingColumnInformation {
663664
final DriftColumn column;
665+
final Element element;
664666

665667
/// If the returned column references another column in the same table, its
666668
/// [ForeignKeyReference] is still unresolved when the local column resolver
@@ -670,5 +672,6 @@ class PendingColumnInformation {
670672
/// this column in that case.
671673
final String? referencesColumnInSameTable;
672674

673-
PendingColumnInformation(this.column, {this.referencesColumnInSameTable});
675+
PendingColumnInformation(this.column,
676+
{this.referencesColumnInSameTable, required this.element});
674677
}

drift_dev/lib/src/analysis/resolver/dart/table.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
4242
} else {
4343
for (final constraint in column.column.constraints) {
4444
if (constraint is ForeignKeyReference) {
45+
if (column.column.sqlType.builtin !=
46+
constraint.otherColumn.sqlType.builtin ||
47+
column.column.typeConverter?.dartType !=
48+
constraint.otherColumn.typeConverter?.dartType) {
49+
reportError(DriftAnalysisError.forDartElement(column.element,
50+
"This column references a column whose type doesn't match this one. The generated managers will ignore this relation",
51+
level: DriftAnalysisErrorLevel.warning));
52+
}
4553
references.add(constraint.otherColumn.owner);
4654
}
4755
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:custom_lint_builder/custom_lint_builder.dart';
2+
import 'package:drift_dev/src/lints/drift_backend_error_lint.dart';
3+
import 'package:drift_dev/src/lints/non_null_insert_with_ignore_lint.dart';
4+
import 'package:drift_dev/src/lints/offset_without_limit_lint.dart';
5+
import 'package:drift_dev/src/lints/unawaited_futures_in_transaction_lint.dart';
6+
import 'package:meta/meta.dart';
7+
8+
@internal
9+
class DriftLinter extends PluginBase {
10+
@override
11+
List<LintRule> getLintRules(CustomLintConfigs configs) => [
12+
unawaitedFuturesInMigration,
13+
unawaitedFuturesInTransaction,
14+
OffsetWithoutLimit(),
15+
DriftBuildErrors(),
16+
NonNullInsertWithIgnore()
17+
];
18+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import 'dart:io';
2+
3+
import 'package:analyzer/dart/analysis/results.dart';
4+
import 'package:analyzer/dart/analysis/session.dart';
5+
import 'package:analyzer/dart/ast/ast.dart';
6+
import 'package:analyzer/dart/element/element.dart';
7+
import 'package:analyzer/error/error.dart' hide LintCode;
8+
import 'package:analyzer/error/listener.dart';
9+
import 'package:custom_lint_builder/custom_lint_builder.dart';
10+
import 'package:drift_dev/src/analysis/backend.dart';
11+
import 'package:drift_dev/src/analysis/driver/error.dart';
12+
import 'package:drift_dev/src/analysis/options.dart';
13+
import 'package:logging/logging.dart';
14+
15+
import '../analysis/driver/driver.dart';
16+
17+
final columnBuilderChecker =
18+
TypeChecker.fromName('DriftDatabase', packageName: 'drift');
19+
20+
class DriftBuildErrors extends DartLintRule {
21+
DriftBuildErrors() : super(code: _errorCode);
22+
23+
static const _errorCode = LintCode(
24+
name: 'drift_build_errors',
25+
problemMessage: '{0}',
26+
errorSeverity: ErrorSeverity.ERROR,
27+
);
28+
LintCode get _warningCode => LintCode(
29+
name: _errorCode.name,
30+
problemMessage: _errorCode.problemMessage,
31+
errorSeverity: ErrorSeverity.WARNING);
32+
33+
@override
34+
void run(CustomLintResolver resolver, ErrorReporter reporter,
35+
CustomLintContext context) async {
36+
final unit = await resolver.getResolvedUnitResult();
37+
final backend = CustomLintBackend(unit.session);
38+
final driver = DriftAnalysisDriver(backend, const DriftOptions.defaults());
39+
40+
final file = await driver.fullyAnalyze(unit.uri);
41+
for (final error in file.allErrors) {
42+
if (error.span case final span?) {
43+
// ignore: deprecated_member_use
44+
reporter.reportErrorForSpan(
45+
error.level == DriftAnalysisErrorLevel.warning
46+
? _warningCode
47+
: _errorCode,
48+
span,
49+
[error.message.trim()]);
50+
}
51+
}
52+
}
53+
}
54+
55+
class CustomLintBackend extends DriftBackend {
56+
@override
57+
final Logger log = Logger('drift_dev.CustomLintBackend');
58+
final AnalysisSession session;
59+
60+
CustomLintBackend(this.session);
61+
62+
@override
63+
bool get canReadDart => true;
64+
65+
@override
66+
Future<AstNode?> loadElementDeclaration(Element element) async {
67+
final library = element.library;
68+
if (library == null) return null;
69+
70+
final info = await library.session.getResolvedLibraryByElement(library);
71+
if (info is ResolvedLibraryResult) {
72+
return info.getElementDeclaration(element)?.node;
73+
} else {
74+
return null;
75+
}
76+
}
77+
78+
@override
79+
Future<String> readAsString(Uri uri) async {
80+
final file = session.getFile(uri.path);
81+
82+
if (file is FileResult) {
83+
return file.content;
84+
}
85+
86+
throw FileSystemException('Not a file result: $file');
87+
}
88+
89+
@override
90+
Future<LibraryElement> readDart(Uri uri) async {
91+
final result = await session.getLibraryByUri(uri.toString());
92+
if (result is LibraryElementResult) {
93+
return result.element;
94+
}
95+
96+
throw NotALibraryException(uri);
97+
}
98+
99+
@override
100+
Future<Expression> resolveExpression(
101+
Uri context, String dartExpression, Iterable<String> imports) {
102+
throw CannotReadExpressionException('Not supported at the moment');
103+
}
104+
105+
@override
106+
Future<Element?> resolveTopLevelElement(
107+
Uri context, String reference, Iterable<Uri> imports) {
108+
throw UnimplementedError();
109+
}
110+
111+
@override
112+
Uri resolveUri(Uri base, String uriString) => base.resolve(uriString);
113+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/error/error.dart' hide LintCode;
3+
import 'package:analyzer/error/listener.dart';
4+
import 'package:custom_lint_builder/custom_lint_builder.dart';
5+
6+
final managerTypeChecker =
7+
TypeChecker.fromName('BaseTableManager', packageName: 'drift');
8+
final insertStatementChecker =
9+
TypeChecker.fromName('InsertStatement', packageName: 'drift');
10+
final insertOrIgnoreChecker =
11+
TypeChecker.fromName('InsertMode', packageName: 'drift');
12+
13+
class NonNullInsertWithIgnore extends DartLintRule {
14+
NonNullInsertWithIgnore() : super(code: _code);
15+
16+
static const _code = LintCode(
17+
name: 'non_null_insert_with_ignore',
18+
problemMessage:
19+
'`insertReturning` and `createReturning` will throw an exception if a row isn\'t actually inserted. Use `createReturningOrNull` or `insertReturningOrNull` if you want to ignore conflicts.',
20+
errorSeverity: ErrorSeverity.WARNING,
21+
);
22+
23+
@override
24+
void run(CustomLintResolver resolver, ErrorReporter reporter,
25+
CustomLintContext context) async {
26+
context.registry.addMethodInvocation(
27+
(node) {
28+
if (node.argumentList.arguments.isEmpty) return;
29+
switch (node.function) {
30+
case SimpleIdentifier func:
31+
if (func.name == "insertReturning" ||
32+
func.name == "createReturning") {
33+
switch (func.parent) {
34+
case MethodInvocation func:
35+
final targetType = func.realTarget?.staticType;
36+
if (targetType != null) {
37+
if (managerTypeChecker.isSuperTypeOf(targetType) ||
38+
insertStatementChecker.isExactlyType(targetType)) {
39+
final namedArgs = func.argumentList.arguments
40+
.whereType<NamedExpression>();
41+
for (final arg in namedArgs) {
42+
if (arg.name.label.name == "mode") {
43+
switch (arg.expression) {
44+
case PrefixedIdentifier mode:
45+
if (mode.identifier.name == "insertOrIgnore") {
46+
print("Found insertOrIgnore");
47+
reporter.atNode(node, _code);
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
},
58+
);
59+
}
60+
}

0 commit comments

Comments
 (0)