blob: 2aa7b8ec336cb00dd44f91b61c6da1a68721611d [file] [edit]
// Copyright (c) 2023, 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.
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:checks/checks.dart';
import 'package:checks_codegen/src/builder.dart';
import 'package:test/scaffolding.dart';
void main() {
group('ChecksBuilder', () {
late Builder builder;
late TestReaderWriter readerWriter;
setUpAll(() async {
readerWriter = TestReaderWriter(rootPackage: 'a');
await readerWriter.testing.loadIsolateSources();
});
setUp(() async {
builder = checksBuilder(null);
});
test('can build', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
abstract class Foo {
final Bar barField;
int get intField;
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<_i3.Bar> get barField => '
"has('barField', (v) => v.barField);",
" _i1.Subject<int> get intField => has('intField', (v) => "
'v.intField);',
'}',
]);
});
test('can build with an export', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
export 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
abstract class Foo {
final Bar barField;
int get intField;
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<_i3.Bar> get barField => '
"has('barField', (v) => v.barField);",
' _i1.Subject<int> get intField => '
"has('intField', (v) => v.intField);",
'}',
]);
});
test('fails if the annotation is not on an import or export', () async {
final result = await testBuilder(builder, {
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
import 'some_test.checks.dart';
@CheckExtensions([Foo])
void main() {
}
''',
'a|test/foo.dart': '''
abstract class Foo {
int get intField;
}
''',
}, readerWriter: readerWriter);
check(result.errors).any(
.it()..contains(
'must annotate an import or export of some_test.checks.dart',
),
);
});
test(
'fails if the annotation is not an import or export to the generated file',
() async {
final result = await testBuilder(builder, {
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'wrong_test.checks.dart';
void main() {
}
''',
'a|test/foo.dart': '''
abstract class Foo {
int get intField;
}
''',
}, readerWriter: readerWriter);
check(result.errors).any(
.it()..contains(
'must annotate an import or export of some_test.checks.dart',
),
);
},
);
test('names the field when a field type fails to resolve', () async {
final result = await testBuilder(builder, {
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
abstract class Foo {
int get intField;
List<Missing> get unresolved;
}
''',
}, readerWriter: readerWriter);
check(result.errors).any(
.it()
..contains('Failed to resolve the type of `Foo.unresolved`.')
..contains('List<Missing> get unresolved;'),
);
});
test(
'includes function typed fields and omits methods and setters',
() async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
abstract class Foo {
int get intField;
final void Function() callback;
final Bar Function(Bar, [Bar?]) positional;
final void Function({required Bar bar, int? count})? named;
void method();
set setterOnly(int value);
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
" _i1.Subject<int> get intField => has('intField', (v) => "
'v.intField);',
' _i1.Subject<void Function()> get callback =>',
" has('callback', (v) => v.callback);",
' _i1.Subject<_i3.Bar Function(_i3.Bar, [_i3.Bar?])> get positional =>',
" has('positional', (v) => v.positional);",
' _i1.Subject<void Function({required _i3.Bar bar, int? count})?> '
'get named =>',
" has('named', (v) => v.named);",
'}',
])
..not(.it()..contains('method'))
..not(.it()..contains('setterOnly'));
},
);
test('uses bounds for type parameters of the class', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
abstract class Foo<T extends Bar, S> {
final T bounded;
final S? unbounded;
final List<T> list;
final void Function(T) callback;
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<_i3.Bar> get bounded => '
"has('bounded', (v) => v.bounded);",
' _i1.Subject<dynamic> get unbounded => '
"has('unbounded', (v) => v.unbounded);",
' _i1.Subject<List<_i3.Bar>> get list => '
"has('list', (v) => v.list);",
' _i1.Subject<void Function(_i3.Bar)> get callback =>',
" has('callback', (v) => v.callback);",
'}',
]);
});
test('supports record, generic function, and Never types', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
abstract class Foo {
final (Bar, {Bar? named}) record;
final T Function<T extends Bar>(T) generic;
final Never never;
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<(_i3.Bar, {_i3.Bar? named})> get record =>',
" has('record', (v) => v.record);",
' _i1.Subject<T Function<T extends _i3.Bar>(T)> get generic =>',
" has('generic', (v) => v.generic);",
" _i1.Subject<Never> get never => has('never', (v) => v.never);",
'}',
]);
});
test('imports type arguments of field types', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'bar.dart';
import 'baz.dart';
abstract class Foo {
final Map<Bar, List<Baz?>> nested;
final List<void Function()> callbacks;
}
''',
'a|test/bar.dart': '''
abstract class Bar {}
''',
'a|test/baz.dart': '''
abstract class Baz {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'bar.dart' as _i3;",
"import 'baz.dart' as _i4;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<Map<_i3.Bar, List<_i4.Baz?>>> get nested =>',
" has('nested', (v) => v.nested);",
' _i1.Subject<List<void Function()>> get callbacks =>',
" has('callbacks', (v) => v.callbacks);",
'}',
]);
});
test('imports through the imports of the declaring library', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'dart:io';
import 'package:b/b.dart';
abstract class Foo {
final HttpClient client;
final List<Bar> bars;
}
''',
'b|lib/b.dart': '''
export 'src/bar.dart';
''',
'b|lib/src/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..containsInOrder([
"import 'dart:io' as _i3;",
"import 'package:b/b.dart' as _i4;",
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<_i3.HttpClient> get client => '
"has('client', (v) => v.client);",
' _i1.Subject<List<_i4.Bar>> get bars => '
"has('bars', (v) => v.bars);",
'}',
])
..not(.it()..contains('dart:_http'))
..not(.it()..contains('src/bar.dart'));
});
test('prefers libraries outside of src/', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'package:b/src/bar.dart';
import 'baz.dart';
abstract class Foo {
final Bar bar;
final Baz baz;
}
''',
'a|test/baz.dart': '''
import 'package:b/b.dart';
abstract class Baz {}
''',
'b|lib/b.dart': '''
export 'src/bar.dart';
''',
'b|lib/src/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..contains("import 'package:b/b.dart' as _i")
..not(.it()..contains('src/bar.dart'));
});
test('falls back to importing from src/', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'package:b/src/bar.dart';
abstract class Foo {
final Bar bar;
}
''',
'b|lib/src/bar.dart': '''
abstract class Bar {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput).contains("import 'package:b/src/bar.dart' as _i");
});
test(
'imports types from private SDK libraries through public SDK libraries',
() async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'status.dart';
abstract class Foo {
final status = makeStatus();
}
''',
// `HttpStatus` is declared in `dart:_internal`, which does not
// import `dart:io`, and neither does any library declaring a type
// used by `Foo`.
'a|test/status.dart': '''
import 'dart:io';
HttpStatus? makeStatus() => null;
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..contains("import 'dart:io' as _i")
..contains('.HttpStatus?> get status')
..not(.it()..contains('dart:_internal'));
},
);
test('fails for types only exported by private SDK libraries', () async {
final result = await testBuilder(builder, {
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'dart:_internal';
abstract class Foo {
EfficientLengthIterable<int> get iterable;
}
''',
}, readerWriter: readerWriter);
check(result.errors).any(
.it()..contains(
'Cannot import `EfficientLengthIterable`. '
'It is declared in dart:_internal',
),
);
});
test('omits private fields and getters', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
abstract class Foo {
int get intField;
final int _privateField;
_Private get _privateGetter;
}
abstract class _Private {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
" _i1.Subject<int> get intField => has('intField', (v) => "
'v.intField);',
'}',
])
..not(.it()..contains('_private'))
..not(.it()..contains('_Private'));
});
test('omits public fields whose types reference private types', () async {
await testBuilder(
builder,
{
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
import 'foo.dart';
@CheckExtensions([Foo])
import 'some_test.checks.dart';
''',
'a|test/foo.dart': '''
import 'other.dart';
typedef _PublicAlias = int;
abstract class Foo<T extends _Private> {
_PublicAlias get aliasedPublic;
_Private get direct;
List<_Private> get typeArgument;
Map<String, List<Other?>> get fromOther;
_Private Function() get fnReturn;
void Function(_Private) get fnParam;
S Function<S extends _Private>(S) get fnBound;
(_Private, int) get positionalRecord;
({_Private named}) get namedRecord;
T get bounded;
}
abstract class _Private {}
''',
'a|test/other.dart': '''
typedef Other = _OtherPrivate;
abstract class _OtherPrivate {}
''',
},
readerWriter: readerWriter,
flattenOutput: true,
);
final checksOutput = readerWriter.testing.readString(
AssetId('a', 'test/some_test.checks.dart'),
);
check(checksOutput)
..containsInOrder([
"import 'package:checks/checks.dart';",
"import 'package:checks/context.dart' as _i1;",
"import 'foo.dart' as _i2;",
'extension FooChecks on _i1.Subject<_i2.Foo> {',
' _i1.Subject<int> get aliasedPublic =>',
" has('aliasedPublic', (v) => v.aliasedPublic);",
'}',
])
..not(.it()..contains('_Private'))
..not(.it()..contains('_OtherPrivate'))
..not(.it()..contains('other.dart'))
..not(.it()..contains('direct'))
..not(.it()..contains('typeArgument'))
..not(.it()..contains('fromOther'))
..not(.it()..contains('fnReturn'))
..not(.it()..contains('fnParam'))
..not(.it()..contains('fnBound'))
..not(.it()..contains('positionalRecord'))
..not(.it()..contains('namedRecord'))
..not(.it()..contains('bounded'));
});
test('fails if a target type is private', () async {
final result = await testBuilder(builder, {
'a|test/some_test.dart': '''
import 'package:checks_codegen/checks_codegen.dart';
@CheckExtensions([_Private])
import 'some_test.checks.dart';
abstract class _Private {
int get intField;
}
''',
}, readerWriter: readerWriter);
check(result.errors).any(
.it()
..contains(
'Private types cannot be used for checks extensions: `_Private`',
)
..contains('abstract class _Private {'),
);
});
});
}