diff --git a/.gitignore b/.gitignore index 10bfc93ae..d54aed87b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ docs/docs/*.wasm docs/docs/*.css docs/docs/examples/** docs/web/robots.txt + +# Linting +custom_lint.log diff --git a/docs/docs/setup.md b/docs/docs/setup.md index 264868751..32ad92483 100644 --- a/docs/docs/setup.md +++ b/docs/docs/setup.md @@ -34,12 +34,13 @@ adding a package to open database on the respective platform. dev_dependencies: drift_dev: ^{{ versions.drift_dev }} build_runner: ^{{ versions.build_runner }} + custom_lint: ^{{ versions.custom_lint }} ``` Alternatively, you can achieve the same result using the following command: ``` - dart pub add drift drift_flutter dev:drift_dev dev:build_runner + dart pub add drift drift_flutter dev:drift_dev dev:build_runner dev:custom_lint ``` Please note that `drift_flutter` depends on `sqlite3_flutter_libs`, which includes a compiled @@ -62,12 +63,13 @@ adding a package to open database on the respective platform. dev_dependencies: drift_dev: ^{{ versions.drift_dev }} build_runner: ^{{ versions.build_runner }} + custom_lint: ^{{ versions.custom_lint }} ``` Alternatively, you can achieve the same result using the following command: ``` - dart pub add drift sqlite3 dev:drift_dev dev:build_runner + dart pub add drift sqlite3 dev:drift_dev dev:build_runner dev:custom_lint ``` === "Dart (Postgres)" @@ -81,12 +83,13 @@ adding a package to open database on the respective platform. dev_dependencies: drift_dev: ^{{ versions.drift_dev }} build_runner: ^{{ versions.build_runner }} + custom_lint: ^{{ versions.custom_lint }} ``` Alternatively, you can achieve the same result using the following command: ``` - dart pub add drift postgres drift_postgres dev:drift_dev dev:build_runner + dart pub add drift postgres drift_postgres dev:drift_dev dev:build_runner dev:custom_lint ``` Drift only generates code for sqlite3 by default. So, also create a `build.yaml` @@ -105,6 +108,16 @@ adding a package to open database on the respective platform. # - sqlite ``` +## Linter setup + +Drift comes with a built-in linter that helps you write better code. It checks for common mistakes +and enforces best practices. To enable it, add the following configuration to your `analysis_options.yaml`: + +```yaml title="analysis_options.yaml" +analyzer: + plugins: + - custom_lint +``` ## Database class diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml index 300f32d08..3dc9b26a7 100644 --- a/docs/pubspec.yaml +++ b/docs/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: sqlite3: ^2.0.0 # Used in examples - rxdart: ^0.27.3 + rxdart: ^0.28.0 yaml: ^3.1.1 drift_dev: any test: ^1.18.0 diff --git a/drift_dev/lib/drift_dev.dart b/drift_dev/lib/drift_dev.dart new file mode 100644 index 000000000..9c5ed397f --- /dev/null +++ b/drift_dev/lib/drift_dev.dart @@ -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(); +} diff --git a/drift_dev/lib/src/analysis/driver/error.dart b/drift_dev/lib/src/analysis/driver/error.dart index 9a1026d96..36ac29378 100644 --- a/drift_dev/lib/src/analysis/driver/error.dart +++ b/drift_dev/lib/src/analysis/driver/error.dart @@ -4,31 +4,41 @@ 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) { + 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 = @@ -36,7 +46,7 @@ class DriftAnalysisError { '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 diff --git a/drift_dev/lib/src/analysis/resolver/dart/column.dart b/drift_dev/lib/src/analysis/resolver/dart/column.dart index bceec1351..30e5c5caa 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/column.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/column.dart @@ -509,6 +509,7 @@ class ColumnParser { customConstraints: foundCustomConstraint, referenceName: _readReferenceName(element), ), + element: element, referencesColumnInSameTable: referencesColumnInSameTable, ); } @@ -661,6 +662,7 @@ class ColumnParser { class PendingColumnInformation { final DriftColumn column; + final Element element; /// If the returned column references another column in the same table, its /// [ForeignKeyReference] is still unresolved when the local column resolver @@ -670,5 +672,6 @@ class PendingColumnInformation { /// this column in that case. final String? referencesColumnInSameTable; - PendingColumnInformation(this.column, {this.referencesColumnInSameTable}); + PendingColumnInformation(this.column, + {this.referencesColumnInSameTable, required this.element}); } diff --git a/drift_dev/lib/src/analysis/resolver/dart/table.dart b/drift_dev/lib/src/analysis/resolver/dart/table.dart index 956a2bdcc..fe94603d9 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/table.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/table.dart @@ -42,6 +42,14 @@ class DartTableResolver extends LocalElementResolver { } 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); } } diff --git a/drift_dev/lib/src/lints/custom_lint_plugin.dart b/drift_dev/lib/src/lints/custom_lint_plugin.dart new file mode 100644 index 000000000..50666690c --- /dev/null +++ b/drift_dev/lib/src/lints/custom_lint_plugin.dart @@ -0,0 +1,18 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:drift_dev/src/lints/drift_backend_error_lint.dart'; +import 'package:drift_dev/src/lints/non_null_insert_with_ignore_lint.dart'; +import 'package:drift_dev/src/lints/offset_without_limit_lint.dart'; +import 'package:drift_dev/src/lints/unawaited_futures_in_transaction_lint.dart'; +import 'package:meta/meta.dart'; + +@internal +class DriftLinter extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + unawaitedFuturesInMigration, + unawaitedFuturesInTransaction, + OffsetWithoutLimit(), + DriftBuildErrors(), + NonNullInsertWithIgnore() + ]; +} diff --git a/drift_dev/lib/src/lints/drift_backend_error_lint.dart b/drift_dev/lib/src/lints/drift_backend_error_lint.dart new file mode 100644 index 000000000..931cbaeff --- /dev/null +++ b/drift_dev/lib/src/lints/drift_backend_error_lint.dart @@ -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: _errorCode); + + static const _errorCode = LintCode( + name: 'drift_build_errors', + problemMessage: '{0}', + errorSeverity: ErrorSeverity.ERROR, + ); + LintCode get _warningCode => LintCode( + name: _errorCode.name, + problemMessage: _errorCode.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?) { + // ignore: deprecated_member_use + reporter.reportErrorForSpan( + error.level == DriftAnalysisErrorLevel.warning + ? _warningCode + : _errorCode, + 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 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 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 readDart(Uri uri) async { + final result = await session.getLibraryByUri(uri.toString()); + if (result is LibraryElementResult) { + return result.element; + } + + throw NotALibraryException(uri); + } + + @override + Future resolveExpression( + Uri context, String dartExpression, Iterable imports) { + throw CannotReadExpressionException('Not supported at the moment'); + } + + @override + Future resolveTopLevelElement( + Uri context, String reference, Iterable imports) { + throw UnimplementedError(); + } + + @override + Uri resolveUri(Uri base, String uriString) => base.resolve(uriString); +} diff --git a/drift_dev/lib/src/lints/non_null_insert_with_ignore_lint.dart b/drift_dev/lib/src/lints/non_null_insert_with_ignore_lint.dart new file mode 100644 index 000000000..63c3d5978 --- /dev/null +++ b/drift_dev/lib/src/lints/non_null_insert_with_ignore_lint.dart @@ -0,0 +1,60 @@ +import 'package:analyzer/dart/ast/ast.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'); +final insertStatementChecker = + TypeChecker.fromName('InsertStatement', packageName: 'drift'); +final insertOrIgnoreChecker = + TypeChecker.fromName('InsertMode', packageName: 'drift'); + +class NonNullInsertWithIgnore extends DartLintRule { + NonNullInsertWithIgnore() : super(code: _code); + + static const _code = LintCode( + name: 'non_null_insert_with_ignore', + problemMessage: + '`insertReturning` and `createReturning` will throw an exception if a row isn\'t actually inserted. Use `createReturningOrNull` or `insertReturningOrNull` if you want to ignore conflicts.', + errorSeverity: ErrorSeverity.WARNING, + ); + + @override + void run(CustomLintResolver resolver, ErrorReporter reporter, + CustomLintContext context) async { + context.registry.addMethodInvocation( + (node) { + if (node.argumentList.arguments.isEmpty) return; + switch (node.function) { + case SimpleIdentifier func: + if (func.name == "insertReturning" || + func.name == "createReturning") { + switch (func.parent) { + case MethodInvocation func: + final targetType = func.realTarget?.staticType; + if (targetType != null) { + if (managerTypeChecker.isSuperTypeOf(targetType) || + insertStatementChecker.isExactlyType(targetType)) { + final namedArgs = func.argumentList.arguments + .whereType(); + for (final arg in namedArgs) { + if (arg.name.label.name == "mode") { + switch (arg.expression) { + case PrefixedIdentifier mode: + if (mode.identifier.name == "insertOrIgnore") { + print("Found insertOrIgnore"); + reporter.atNode(node, _code); + } + } + } + } + } + } + } + } + } + }, + ); + } +} diff --git a/drift_dev/lib/src/lints/offset_without_limit_lint.dart b/drift_dev/lib/src/lints/offset_without_limit_lint.dart new file mode 100644 index 000000000..d97f5c68d --- /dev/null +++ b/drift_dev/lib/src/lints/offset_without_limit_lint.dart @@ -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 doesnt have any effect.', + 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(node.function); + + if (func?.name == "get" || func?.name == "watch") { + final target = _typeCheck(node.target); + final managerGetter = + _typeCheck(target?.staticElement); + if (managerGetter != null) { + if (managerTypeChecker.isSuperTypeOf(managerGetter.returnType)) { + final namedArgs = + node.argumentList.arguments.whereType(); + if (namedArgs + .every((element) => element.name.label.name != "limit") && + namedArgs + .any((element) => element.name.label.name == "offset")) { + reporter.atNode(node, _code); + } + } + } + } + }, + ); + } +} + +T? _typeCheck(i) { + return i is T ? i : null; +} diff --git a/drift_dev/lib/src/lints/unawaited_futures_in_transaction_lint.dart b/drift_dev/lib/src/lints/unawaited_futures_in_transaction_lint.dart new file mode 100644 index 000000000..d978cc842 --- /dev/null +++ b/drift_dev/lib/src/lints/unawaited_futures_in_transaction_lint.dart @@ -0,0 +1,196 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// A lint rule that reports unawaited futures if an additional check returns true. +class _GenericUnawaitedFutureRule extends DartLintRule { + _GenericUnawaitedFutureRule( + {required super.code, required this.additionalCheck}); + + /// An unwaited future will only be reported if the [additionalCheck] returns true. + final bool Function(AstNode) additionalCheck; + + @override + void run(CustomLintResolver resolver, ErrorReporter reporter, + CustomLintContext context) { + context.registry.addExpressionStatement((node) { + node.accept( + _Visitor(this, reporter, code, additionalCheck: additionalCheck)); + }); + context.registry.addCascadeExpression((node) { + node.accept( + _Visitor(this, reporter, code, additionalCheck: additionalCheck)); + }); + context.registry.addInterpolationExpression((node) { + node.accept( + _Visitor(this, reporter, code, additionalCheck: additionalCheck)); + }); + } +} + +final _databaseConnectionUserChecker = + TypeChecker.fromName('DatabaseConnectionUser', packageName: 'drift'); +final _migrationStrategyChecker = + TypeChecker.fromName('MigrationStrategy', packageName: 'drift'); + +/// A lint which reports unawaited futures in a transaction. +final unawaitedFuturesInTransaction = _GenericUnawaitedFutureRule( + code: LintCode( + name: 'unawaited_futures_in_transaction', + problemMessage: + 'All futures in a transaction should be awaited to ensure that all operations are completed before the transaction is closed.', + errorSeverity: ErrorSeverity.ERROR, + ), + additionalCheck: (node) { + return node.thisOrAncestorMatching( + (method) { + if (method is! MethodInvocation) return false; + final methodElement = method.methodName.staticElement; + if (methodElement is! MethodElement || + methodElement.name != 'transaction') { + return false; + } + // ignore: deprecated_member_use + final enclosingElement = methodElement.enclosingElement; + if (enclosingElement is! ClassElement || + !_databaseConnectionUserChecker.isExactly(enclosingElement)) { + return false; + } + return true; + }, + ) != + null; + }); + +/// A lint which reports unawaited futures in a migration. +final unawaitedFuturesInMigration = _GenericUnawaitedFutureRule( + code: LintCode( + name: 'unawaited_futures_in_migration', + problemMessage: + 'All futures in a migrations should be awaited to ensure that all operations are completed before the other opperations are performed.', + errorSeverity: ErrorSeverity.ERROR, + ), + additionalCheck: (node) { + return node.thisOrAncestorMatching((node) => + (node is InstanceCreationExpression && + node.staticType != null && + _migrationStrategyChecker.isExactlyType(node.staticType!))) != + null; + }); + +// Additional code copied from the original unawaited_futures.dart lint +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// Source: https://github.com/dart-lang/sdk/blob/main/pkg/linter/lib/src/rules/unawaited_futures.dart + +/// A visitor which will report any future that is not awaited. +/// If an [additionalCheck] is provided, it will be used to check if the future should be reported +class _Visitor extends SimpleAstVisitor { + final LintRule rule; + final ErrorReporter reporter; + final LintCode code; + final bool Function(AstNode node) additionalCheck; + + _Visitor(this.rule, this.reporter, this.code, + {required this.additionalCheck}); + + @override + void visitCascadeExpression(CascadeExpression node) { + var sections = node.cascadeSections; + for (var i = 0; i < sections.length; i++) { + _visit(sections[i]); + } + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + var expr = node.expression; + if (expr is AssignmentExpression) return; + + var type = expr.staticType; + if (type == null) { + return; + } + if (type.implementsInterface('Future', 'dart.async')) { + // Ignore a couple of special known cases. + if (_isFutureDelayedInstanceCreationWithComputation(expr) || + _isMapPutIfAbsentInvocation(expr)) { + return; + } + + if (_isEnclosedInAsyncFunctionBody(node) && additionalCheck(node)) { + // Future expression statement that isn't awaited in an async function: + // while this is legal, it's a very frequent sign of an error. + + reporter.atNode(node, code); + } + } + } + + @override + void visitInterpolationExpression(InterpolationExpression node) { + _visit(node.expression); + } + + bool _isEnclosedInAsyncFunctionBody(AstNode node) { + var enclosingFunctionBody = node.thisOrAncestorOfType(); + return enclosingFunctionBody?.isAsynchronous ?? false; + } + + /// Detects `Future.delayed(duration, [computation])` creations with a + /// computation. + bool _isFutureDelayedInstanceCreationWithComputation(Expression expr) => + expr is InstanceCreationExpression && + (expr.staticType?.isDartAsyncFuture ?? false) && + expr.constructorName.name?.name == 'delayed' && + expr.argumentList.arguments.length == 2; + + bool _isMapClass(Element? e) => + e is ClassElement && e.name == 'Map' && e.library.name == 'dart.core'; + + /// Detects Map.putIfAbsent invocations. + bool _isMapPutIfAbsentInvocation(Expression expr) => + expr is MethodInvocation && + expr.methodName.name == 'putIfAbsent' && + // ignore: deprecated_member_use + _isMapClass(expr.methodName.staticElement?.enclosingElement); + + void _visit(Expression expr) { + if ((expr.staticType?.isDartAsyncFuture ?? false) && + _isEnclosedInAsyncFunctionBody(expr) && + expr is! AssignmentExpression && + additionalCheck(expr)) { + reporter.atNode(expr, code); + } + } +} + +/// The above code snippet depends on some extensions which are copied from +/// https://github.com/dart-lang/sdk/blob/main/pkg/linter/lib/src/extensions.dart +/// The extensions are copied here below: +extension on DartType? { + bool implementsInterface(String interface, String library) { + var self = this; + if (self is! InterfaceType) { + return false; + } + bool predicate(InterfaceType i) => i.isSameAs(interface, library); + var element = self.element; + return predicate(self) || + !element.isSynthetic && element.allSupertypes.any(predicate); + } + + /// Returns whether `this` is the same element as [interface], declared in + /// [library]. + bool isSameAs(String? interface, String? library) { + var self = this; + return self is InterfaceType && + self.element.name == interface && + self.element.library.name == library; + } +} diff --git a/drift_dev/lib/src/writer/manager/table_manager_writer.dart b/drift_dev/lib/src/writer/manager/table_manager_writer.dart index 2b68ff57f..96c071f6c 100644 --- a/drift_dev/lib/src/writer/manager/table_manager_writer.dart +++ b/drift_dev/lib/src/writer/manager/table_manager_writer.dart @@ -117,16 +117,7 @@ class _TableManagerWriter { final currentType = typeForColumn(relation.currentColumn); final referencedType = typeForColumn(relation.referencedColumn); - if (currentType != referencedType) { - print( - "\"${relation.currentTable.baseDartName}.${relation.currentColumn.nameInSql}\" has a type of \"$currentType\"" - " and \"${relation.referencedTable.baseDartName}.${relation.referencedColumn.nameInSql}\" has a type of \"$referencedType\"." - " Filters, orderings and reference getters for this relation wont be generated." - " The Manager API can only generate filters and orderings for relations where the types are exactly the same." - " If you aren't using the Manager API, you can ignore this message."); - return false; - } - return true; + return currentType == referencedType; }).toList(); final columnFilters = []; diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index 460f2b401..2949f1006 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -53,6 +53,9 @@ dependencies: source_gen: ">=0.9.4 <2.0.0" string_scanner: ^1.1.1 + # Linting + custom_lint_builder: ^0.6.7 + dev_dependencies: lints: ^4.0.0 checked_yaml: ^2.0.1 diff --git a/drift_dev/test/lint/lint_test.dart b/drift_dev/test/lint/lint_test.dart new file mode 100644 index 000000000..594069fbd --- /dev/null +++ b/drift_dev/test/lint/lint_test.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:path/path.dart' as p; + +void main() { + // Test the linter_test.dart file + test('linter', () async { + final workingDir = p.join(p.current, 'test/lint/test_pkg'); + expect( + await Process.run('dart', + ['run', 'custom_lint', '--fatal-infos', '--fatal-warnings'], + workingDirectory: workingDir) + .then((v) => v.exitCode), + 0); + }); +} diff --git a/drift_dev/test/lint/test_pkg/analysis_options.yaml b/drift_dev/test/lint/test_pkg/analysis_options.yaml new file mode 100644 index 000000000..909bbd25b --- /dev/null +++ b/drift_dev/test/lint/test_pkg/analysis_options.yaml @@ -0,0 +1,11 @@ +include: package:lints/recommended.yaml + +analyzer: + plugins: + - custom_lint + language: + strict-casts: true + +custom_lint: + debug: true + verbose: true diff --git a/drift_dev/test/lint/test_pkg/lib/db.dart b/drift_dev/test/lint/test_pkg/lib/db.dart new file mode 100644 index 000000000..36b045fa5 --- /dev/null +++ b/drift_dev/test/lint/test_pkg/lib/db.dart @@ -0,0 +1,68 @@ +import 'package:drift/drift.dart'; + +part 'db.g.dart'; + +class Users extends Table { + late final id = integer().autoIncrement()(); + late final name = text()(); + // expect_lint: drift_build_errors + late final age = integer(); + // expect_lint: drift_build_errors + late final group = int64().references(Groups, #id)(); +} + +class Groups extends Table { + late final id = integer().autoIncrement()(); + late final name = text()(); +} + +class BrokenTable extends Table { + // expect_lint: drift_build_errors + IntColumn get unknownRef => integer().customConstraint('CHECK foo > 10')(); +} + +@DriftDatabase(tables: [Users]) +class TestDatabase extends _$TestDatabase { + TestDatabase(super.e); + + @override + int get schemaVersion => 1; + + a() async { + transaction( + () async { + // expect_lint: unawaited_futures_in_transaction + into(users) + .insert(UsersCompanion.insert(name: 'name', group: BigInt.from(1))); + await into(users) + .insert(UsersCompanion.insert(name: 'name', group: BigInt.from(1))); + }, + ); + // expect_lint: non_null_insert_with_ignore + await into(users).insertReturning( + UsersCompanion.insert(name: 'name', group: BigInt.from(1)), + mode: InsertMode.insertOrIgnore); + // expect_lint: non_null_insert_with_ignore + await managers.users.createReturning( + (o) => o(name: "hi", group: BigInt.from(1)), + mode: InsertMode.insertOrIgnore); + await into(users).insertReturningOrNull( + UsersCompanion.insert(name: 'name', group: BigInt.from(1)), + mode: InsertMode.insertOrIgnore); + await managers.users.createReturningOrNull( + (o) => o(name: "hi", group: BigInt.from(1)), + mode: InsertMode.insertOrIgnore); + await into(users).insertReturning( + UsersCompanion.insert(name: 'name', group: BigInt.from(1))); + await managers.users + .createReturning((o) => o(name: "hi", group: BigInt.from(1))); + } + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + // expect_lint: unawaited_futures_in_migration + m.createTable(users); + }, + ); +} diff --git a/drift_dev/test/lint/test_pkg/lib/db.g.dart b/drift_dev/test/lint/test_pkg/lib/db.g.dart new file mode 100644 index 000000000..e036e8be7 --- /dev/null +++ b/drift_dev/test/lint/test_pkg/lib/db.g.dart @@ -0,0 +1,650 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'db.dart'; + +// ignore_for_file: type=lint +class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Group map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Group( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + ); + } + + @override + $GroupsTable createAlias(String alias) { + return $GroupsTable(attachedDatabase, alias); + } +} + +class Group extends DataClass implements Insertable { + final int id; + final String name; + const Group({required this.id, required this.name}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + id: Value(id), + name: Value(name), + ); + } + + factory Group.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Group( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + }; + } + + Group copyWith({int? id, String? name}) => Group( + id: id ?? this.id, + name: name ?? this.name, + ); + Group copyWithCompanion(GroupsCompanion data) { + return Group( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + ); + } + + @override + String toString() { + return (StringBuffer('Group(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Group && other.id == this.id && other.name == this.name); +} + +class GroupsCompanion extends UpdateCompanion { + final Value id; + final Value name; + const GroupsCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + }); + GroupsCompanion.insert({ + this.id = const Value.absent(), + required String name, + }) : name = Value(name); + static Insertable custom({ + Expression? id, + Expression? name, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + }); + } + + GroupsCompanion copyWith({Value? id, Value? name}) { + return GroupsCompanion( + id: id ?? this.id, + name: name ?? this.name, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } +} + +class $UsersTable extends Users with TableInfo<$UsersTable, User> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $UsersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _groupMeta = const VerificationMeta('group'); + @override + late final GeneratedColumn group = GeneratedColumn( + 'group', aliasedName, false, + type: DriftSqlType.bigInt, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES "groups" (id)')); + @override + List get $columns => [id, name, group]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'users'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('group')) { + context.handle( + _groupMeta, group.isAcceptableOrUnknown(data['group']!, _groupMeta)); + } else if (isInserting) { + context.missing(_groupMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + User map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return User( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + group: attachedDatabase.typeMapping + .read(DriftSqlType.bigInt, data['${effectivePrefix}group'])!, + ); + } + + @override + $UsersTable createAlias(String alias) { + return $UsersTable(attachedDatabase, alias); + } +} + +class User extends DataClass implements Insertable { + final int id; + final String name; + final BigInt group; + const User({required this.id, required this.name, required this.group}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['group'] = Variable(group); + return map; + } + + UsersCompanion toCompanion(bool nullToAbsent) { + return UsersCompanion( + id: Value(id), + name: Value(name), + group: Value(group), + ); + } + + factory User.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return User( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + group: serializer.fromJson(json['group']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'group': serializer.toJson(group), + }; + } + + User copyWith({int? id, String? name, BigInt? group}) => User( + id: id ?? this.id, + name: name ?? this.name, + group: group ?? this.group, + ); + User copyWithCompanion(UsersCompanion data) { + return User( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + group: data.group.present ? data.group.value : this.group, + ); + } + + @override + String toString() { + return (StringBuffer('User(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('group: $group') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, group); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is User && + other.id == this.id && + other.name == this.name && + other.group == this.group); +} + +class UsersCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value group; + const UsersCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.group = const Value.absent(), + }); + UsersCompanion.insert({ + this.id = const Value.absent(), + required String name, + required BigInt group, + }) : name = Value(name), + group = Value(group); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? group, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (group != null) 'group': group, + }); + } + + UsersCompanion copyWith( + {Value? id, Value? name, Value? group}) { + return UsersCompanion( + id: id ?? this.id, + name: name ?? this.name, + group: group ?? this.group, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (group.present) { + map['group'] = Variable(group.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UsersCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('group: $group') + ..write(')')) + .toString(); + } +} + +abstract class _$TestDatabase extends GeneratedDatabase { + _$TestDatabase(QueryExecutor e) : super(e); + $TestDatabaseManager get managers => $TestDatabaseManager(this); + late final $GroupsTable groups = $GroupsTable(this); + late final $UsersTable users = $UsersTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [groups, users]; +} + +typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ + Value id, + required String name, +}); +typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ + Value id, + Value name, +}); + +class $$GroupsTableFilterComposer + extends Composer<_$TestDatabase, $GroupsTable> { + $$GroupsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); +} + +class $$GroupsTableOrderingComposer + extends Composer<_$TestDatabase, $GroupsTable> { + $$GroupsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); +} + +class $$GroupsTableAnnotationComposer + extends Composer<_$TestDatabase, $GroupsTable> { + $$GroupsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); +} + +class $$GroupsTableTableManager extends RootTableManager< + _$TestDatabase, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, BaseReferences<_$TestDatabase, $GroupsTable, Group>), + Group, + PrefetchHooks Function()> { + $$GroupsTableTableManager(_$TestDatabase db, $GroupsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + }) => + GroupsCompanion( + id: id, + name: name, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String name, + }) => + GroupsCompanion.insert( + id: id, + name: name, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< + _$TestDatabase, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, BaseReferences<_$TestDatabase, $GroupsTable, Group>), + Group, + PrefetchHooks Function()>; +typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({ + Value id, + required String name, + required BigInt group, +}); +typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({ + Value id, + Value name, + Value group, +}); + +class $$UsersTableFilterComposer extends Composer<_$TestDatabase, $UsersTable> { + $$UsersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); +} + +class $$UsersTableOrderingComposer + extends Composer<_$TestDatabase, $UsersTable> { + $$UsersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); +} + +class $$UsersTableAnnotationComposer + extends Composer<_$TestDatabase, $UsersTable> { + $$UsersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); +} + +class $$UsersTableTableManager extends RootTableManager< + _$TestDatabase, + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, BaseReferences<_$TestDatabase, $UsersTable, User>), + User, + PrefetchHooks Function()> { + $$UsersTableTableManager(_$TestDatabase db, $UsersTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$UsersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$UsersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$UsersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value group = const Value.absent(), + }) => + UsersCompanion( + id: id, + name: name, + group: group, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String name, + required BigInt group, + }) => + UsersCompanion.insert( + id: id, + name: name, + group: group, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$UsersTableProcessedTableManager = ProcessedTableManager< + _$TestDatabase, + $UsersTable, + User, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (User, BaseReferences<_$TestDatabase, $UsersTable, User>), + User, + PrefetchHooks Function()>; + +class $TestDatabaseManager { + final _$TestDatabase _db; + $TestDatabaseManager(this._db); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db, _db.groups); + $$UsersTableTableManager get users => + $$UsersTableTableManager(_db, _db.users); +} diff --git a/drift_dev/test/lint/test_pkg/pubspec.yaml b/drift_dev/test/lint/test_pkg/pubspec.yaml new file mode 100644 index 000000000..3a752f10d --- /dev/null +++ b/drift_dev/test/lint/test_pkg/pubspec.yaml @@ -0,0 +1,25 @@ +name: test_pkg +description: A starting point for Dart libraries or applications. +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.5.3 + +dependencies: + drift: + build_runner: ^2.4.13 + +dev_dependencies: + lints: ^4.0.0 + test: ^1.24.0 + drift_dev: + custom_lint: ^0.6.7 + +dependency_overrides: + drift: + path: ../../../../drift + drift_dev: + path: ../../../../drift_dev + sqlparser: + path: ../../../../sqlparser diff --git a/melos.yaml b/melos.yaml index 4956da69d..ac3977c6d 100644 --- a/melos.yaml +++ b/melos.yaml @@ -6,6 +6,7 @@ packages: - drift - drift_sqflite - drift_dev + - drift_dev/test/lint/test_pkg - drift_flutter - sqlparser - examples/* diff --git a/pubspec.lock b/pubspec.lock index c9e9a6eb9..e698ebd07 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" collection: dependency: transitive description: @@ -77,10 +85,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" glob: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -141,10 +157,10 @@ packages: dependency: "direct dev" description: name: melos - sha256: "96e64bbade5712c3f010137e195bca9f1b351fac34ab1f322af492ae34032067" + sha256: a3f06ed871e0348cb99909ad5ddf5f8b53cc61d894c302b5417d2db1ee7ec381 url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "6.1.0" meta: dependency: transitive description: @@ -189,10 +205,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" prompts: dependency: transitive description: @@ -213,10 +229,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: b06600619c8c219065a548f8f7c192b3e080beff95488ed692780f48f69c0625 + sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.4.0" pubspec: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a91f29019..9a4b5e0a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,4 +5,4 @@ environment: sdk: ">=3.3.0 <4.0.0" dev_dependencies: - melos: ^3.0.0 + melos: ^6.1.0