Skip to content
Open
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
13 changes: 8 additions & 5 deletions dartdoc_test/lib/src/extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ List<DocumentationCodeSample> 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;
Expand All @@ -142,7 +144,8 @@ List<DocumentationCodeSample> extractCodeSamples(DocumentationComment comment) {
samples.add(DocumentationCodeSample(
comment: comment,
code: code,
noTest: child.attributes['class'] == 'language-dart#no-test',
noTest: noTest,
shouldRun: shouldRun,
));
}
}
Expand Down
19 changes: 15 additions & 4 deletions dartdoc_test/lib/src/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,24 @@ class Summary {

/// Get summery from [DartdocAnalysisResult].
factory Summary.from(List<DartdocAnalysisResult> 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
Expand Down
15 changes: 14 additions & 1 deletion dartdoc_test/lib/src/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,28 @@ 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.
bool get hasMain => code.contains('void main()');
/// Detects variations like:
/// - `void main()`
/// - `Future<void> main()`
/// - `Future main()`
/// - `main()`
bool get hasMain {
// Check for any main function declaration
// Matches: main(), void main(), Future<void> main(), Future main()
return RegExp(r'\b(Future<void>|Future|void)?\s*main\s*\(').hasMatch(code);
}

/// Create a sample by wrapping the code with a main function and imports.
String wrappedCode(Directory testDir) {
Expand Down
64 changes: 64 additions & 0 deletions dartdoc_test/lib/src/reporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<TestResult> results, {bool verbose = false}) {
// Default implementation - will be overridden by subclasses
}
}

/// Reporter for source file.
Expand Down Expand Up @@ -113,6 +137,32 @@ final class _ReporterForStdout extends Reporter
logger.info('${issue.commentSpan!.info(issue.message)}\n');
}
}

@override
void reportTestResults(List<TestResult> 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.
Expand Down Expand Up @@ -166,6 +216,20 @@ final class _RepoterForTest extends Reporter
);
}
}

@override
void reportTestResults(List<TestResult> 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.
Expand Down
2 changes: 1 addition & 1 deletion dartdoc_test/lib/src/resource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
62 changes: 62 additions & 0 deletions typed_sql/doc/05_foreign_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down