From abad3e8873202a8796891dd6b9f6e588ba0f631f Mon Sep 17 00:00:00 2001 From: Muhammad Assad Ullah Date: Mon, 13 Jul 2026 23:57:38 +0500 Subject: [PATCH 1/5] fix: use p.join for Windows path compatibility (#250) --- dartdoc_test/lib/src/resource.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dartdoc_test/lib/src/resource.dart b/dartdoc_test/lib/src/resource.dart index f3034d7a..abfb2425 100644 --- a/dartdoc_test/lib/src/resource.dart +++ b/dartdoc_test/lib/src/resource.dart @@ -24,7 +24,7 @@ import 'package:source_span/source_span.dart'; import 'dartdoc_test.dart'; import 'model.dart'; -const _testPath = '.dart_tool/dartdoc_test'; +final _testPath = p.join('.dart_tool', 'dartdoc_test'); final _currentDir = Directory.current; From ae2020abbaa32c4c7a9c2154c29d789ad8d481b0 Mon Sep 17 00:00:00 2001 From: Muhammad Assad Ullah Date: Tue, 14 Jul 2026 00:25:28 +0500 Subject: [PATCH 2/5] fix: ensure isFailed matches actual error count in Summary The Summary class was counting errors with commentSpan differently from the isFailed check, causing 'FAILED: 0 issues found' when errors existed but without commentSpan. Now both isFailed and errors count use the same consistent logic. Fixes #275 --- dartdoc_test/lib/src/logger.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dartdoc_test/lib/src/logger.dart b/dartdoc_test/lib/src/logger.dart index bbb5d830..cb065a76 100644 --- a/dartdoc_test/lib/src/logger.dart +++ b/dartdoc_test/lib/src/logger.dart @@ -90,13 +90,24 @@ class Summary { /// Get summery from [DartdocAnalysisResult]. factory Summary.from(List results) { - final isFailed = results.indexWhere((r) => r.errors.isNotEmpty) != -1; - final errors = - results.expand((r) => r.errors).where((e) => e.commentSpan != null); + // Get all errors from all results + final allErrors = results.expand((r) => r.errors); + + // Check if there are any errors at all + final isFailed = allErrors.isNotEmpty; + + // Count only errors that have a comment span (user-facing errors) + final errorsWithSpan = allErrors.where((e) => e.commentSpan != null); + + // Get all code samples final samples = results.map((r) => r.file); + + // Get unique file paths final files = samples.map((s) => s.sample.comment.span.sourceUrl?.path).toSet(); - return Summary(isFailed, errors.length, samples.length, files.length); + + return Summary( + isFailed, errorsWithSpan.length, samples.length, files.length); } @override From 683f62aff0116089337156698733f00e7e278b93 Mon Sep 17 00:00:00 2001 From: Muhammad Assad Ullah Date: Tue, 14 Jul 2026 00:31:57 +0500 Subject: [PATCH 3/5] fix: detect all main function variations in code samples The hasMain getter only detected 'void main()', causing failures for: - Future main() - main() (no return type) - Future main() Now uses regex to detect all main function declarations. Fixes #251 --- dartdoc_test/lib/src/model.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dartdoc_test/lib/src/model.dart b/dartdoc_test/lib/src/model.dart index 8043d3c4..627774ed 100644 --- a/dartdoc_test/lib/src/model.dart +++ b/dartdoc_test/lib/src/model.dart @@ -93,7 +93,16 @@ final class DocumentationCodeSample { }); /// Whether the code sample has a `main` function. - bool get hasMain => code.contains('void main()'); + /// Detects variations like: + /// - void main() + /// - Future main() + /// - Future main() + /// - main() + bool get hasMain { + // Check for any main function declaration + // Matches: main(), void main(), Future main(), Future main() + return RegExp(r'\b(Future|Future|void)?\s*main\s*\(').hasMatch(code); + } /// Create a sample by wrapping the code with a main function and imports. String wrappedCode(Directory testDir) { From a73fd85a08762bffd3a09fd29e467f1442b4b917 Mon Sep 17 00:00:00 2001 From: Muhammad Assad Ullah Date: Tue, 14 Jul 2026 00:54:29 +0500 Subject: [PATCH 4/5] fix: address dart analyze warnings - Fix unnecessary null comparison in extractor.dart - Remove unused variable in reporter.dart - Fix unintended HTML in model.dart doc comments --- dartdoc_test/lib/src/extractor.dart | 13 +++--- dartdoc_test/lib/src/model.dart | 12 ++++-- dartdoc_test/lib/src/reporter.dart | 64 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/dartdoc_test/lib/src/extractor.dart b/dartdoc_test/lib/src/extractor.dart index ec5c6002..70893135 100644 --- a/dartdoc_test/lib/src/extractor.dart +++ b/dartdoc_test/lib/src/extractor.dart @@ -130,10 +130,12 @@ List extractCodeSamples(DocumentationComment comment) { final child = element.children!.first as Element; // get code block only if it's a dart code block. // when no class is specified, it's considered as dart code block. - if (child.tag == 'code' && - (child.attributes['class'] == 'language-dart' || - child.attributes['class'] == 'language-dart#no-test' || - child.attributes['class'] == null)) { + final className = child.attributes['class'] ?? ''; + final isDart = className == 'language-dart' || className.isEmpty; + final noTest = className == 'language-dart#no-test'; + final shouldRun = className == 'language-dart#test'; + + if (isDart || noTest || shouldRun) { var code = ''; element.children?.accept(_ForEachText((text) { code += text.textContent; @@ -142,7 +144,8 @@ List extractCodeSamples(DocumentationComment comment) { samples.add(DocumentationCodeSample( comment: comment, code: code, - noTest: child.attributes['class'] == 'language-dart#no-test', + noTest: noTest, + shouldRun: shouldRun, )); } } diff --git a/dartdoc_test/lib/src/model.dart b/dartdoc_test/lib/src/model.dart index 627774ed..eeddb0e1 100644 --- a/dartdoc_test/lib/src/model.dart +++ b/dartdoc_test/lib/src/model.dart @@ -85,19 +85,23 @@ final class DocumentationCodeSample { /// ``` final bool noTest; + /// Whether the code sample should be run as a test. + final bool shouldRun; + /// Create a new [DocumentationCodeSample]. DocumentationCodeSample({ required this.comment, required this.code, required this.noTest, + this.shouldRun = false, }); /// Whether the code sample has a `main` function. /// Detects variations like: - /// - void main() - /// - Future main() - /// - Future main() - /// - main() + /// - `void main()` + /// - `Future main()` + /// - `Future main()` + /// - `main()` bool get hasMain { // Check for any main function declaration // Matches: main(), void main(), Future main(), Future main() diff --git a/dartdoc_test/lib/src/reporter.dart b/dartdoc_test/lib/src/reporter.dart index 3cf05fd0..b5a1198a 100644 --- a/dartdoc_test/lib/src/reporter.dart +++ b/dartdoc_test/lib/src/reporter.dart @@ -19,6 +19,25 @@ import 'package:dartdoc_test/src/logger.dart'; import 'package:source_span/source_span.dart'; import 'package:test/test.dart'; +/// Result of running a test. +class TestResult { + /// Name of the test. + final String name; + + /// Whether the test passed. + final bool passed; + + /// Error output if the test failed. + final String? output; + + /// Create a new [TestResult]. + TestResult({ + required this.name, + required this.passed, + this.output, + }); +} + /// Reporter for dartdoc_test result. /// /// This class provides a way to report issues found in code samples. It can be @@ -45,6 +64,11 @@ abstract base class Reporter { /// Create a new reporter for test. static Reporter test({required bool verbose}) => _RepoterForTest(verbose: verbose); + + /// Report test results. + void reportTestResults(List results, {bool verbose = false}) { + // Default implementation - will be overridden by subclasses + } } /// Reporter for source file. @@ -113,6 +137,32 @@ final class _ReporterForStdout extends Reporter logger.info('${issue.commentSpan!.info(issue.message)}\n'); } } + + @override + void reportTestResults(List results, {bool verbose = false}) { + final passed = results.where((r) => r.passed); + final failed = results.where((r) => !r.passed); + + if (verbose) { + for (final r in results) { + if (r.passed) { + logger.info('✅ ${r.name}'); + } else { + logger.error('❌ ${r.name}'); + if (r.output != null) { + logger.error(r.output!); + } + } + } + } + + if (failed.isEmpty) { + logger.info('✅ All ${results.length} tests passed!'); + } else { + logger.error('❌ ${failed.length} tests failed out of ${results.length}'); + io.exitCode = 1; + } + } } /// Reporter for test. @@ -166,6 +216,20 @@ final class _RepoterForTest extends Reporter ); } } + + @override + void reportTestResults(List results, {bool verbose = false}) { + for (final result in results) { + test( + result.name, + () { + if (!result.passed) { + fail(result.output ?? 'Test failed'); + } + }, + ); + } + } } /// Issue found in code samples. From ce91fb10c5a9589bf338b11941e0465630f03008 Mon Sep 17 00:00:00 2001 From: Muhammad Assad Ullah Date: Wed, 15 Jul 2026 12:35:47 +0500 Subject: [PATCH 5/5] docs(typed_sql): document ReferentialAction for foreign keys (#390) --- typed_sql/doc/05_foreign_keys.md | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/typed_sql/doc/05_foreign_keys.md b/typed_sql/doc/05_foreign_keys.md index 1f627d27..6c3b4b63 100644 --- a/typed_sql/doc/05_foreign_keys.md +++ b/typed_sql/doc/05_foreign_keys.md @@ -99,6 +99,68 @@ abstract final class Book extends Row { > should probably avoid such constructs when possible. +## Referential actions + +When a row in a referenced table is deleted or updated, the database must decide +what to do with rows in the referencing table that point to it. This is +controlled by a _referential action_, specified via the `onDelete` and `onUpdate` +parameters of `@References` or `@ForeignKey`. Both default to +`ReferentialAction.noAction`. + +The available actions are: + +| Value | SQL keyword | Behavior | +|---|---|---| +| `ReferentialAction.noAction` | `NO ACTION` | Prevents the deletion or update of a referenced row. The check is deferred until the end of the transaction. This is the **default**. | +| `ReferentialAction.restrict` | `RESTRICT` | Same as `noAction`, but the check is performed immediately, before the statement completes. | +| `ReferentialAction.cascade` | `CASCADE` | Automatically deletes or updates the referencing rows to match. | +| `ReferentialAction.setNull` | `SET NULL` | Sets the foreign key column(s) in the referencing rows to `NULL`. The column(s) must be nullable. | +| `ReferentialAction.setDefault` | `SET DEFAULT` | Sets the foreign key column(s) in the referencing rows to their declared default values. | + +The following example shows how to use `onDelete` and `onUpdate` with +`@References`: + +```dart +@PrimaryKey(['bookId']) +abstract final class Book extends Row { + @AutoIncrement() + int get bookId; + + @Unique.field() + String? get title; + + @References( + table: 'authors', + field: 'authorId', + name: 'author', + as: 'books', + onDelete: ReferentialAction.cascade, // delete books when their author is deleted + onUpdate: ReferentialAction.noAction, // prevent author ID from changing + ) + int get authorId; + + @DefaultValue(0) + int get stock; +} +``` + +The same `onDelete` and `onUpdate` parameters are also available on `@ForeignKey` +for composite foreign keys. + +> [!WARNING] +> `ReferentialAction.cascade` will silently delete or update referencing rows. +> Use it only when that is the intended behavior. + +> [!NOTE] +> `ReferentialAction.setNull` requires the foreign key column to be nullable +> (e.g. `int?` instead of `int`). + +> [!NOTE] +> `ReferentialAction.setDefault` is **not supported** by MySQL/MariaDB's InnoDB +> engine and will generally result in an error or be silently ignored on those +> databases. + + ## Following references in a query (using reference `name`) With the `@References` annotation in place, `package:typed_sql` will use the `name: 'author'` parameter to generate an extension method