blob: fbce8157912494a4fea73dd703fa1721d2fbe245 [file] [log] [blame]
// Copyright (c) 2024, 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:dartdoc/src/markdown_processor.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'dartdoc_test_base.dart';
import 'src/test_descriptor_utils.dart' as d;
import 'src/utils.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(ExtensionMethodsTest);
defineReflectiveTests(ExtensionMethodsExportTest);
});
}
@reflectiveTest
class ExtensionMethodsTest extends DartdocTestBase {
@override
String get libraryName => 'extension_methods';
void test_applicability_extensionOnClass_couldApplyToSameClass() async {
var library = await bootPackageWithLibrary('''
class C {}
extension Ex on C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void test_applicability_extensionOnClass_couldApplyToDifferentClass() async {
var library = await bootPackageWithLibrary('''
class C {}
extension Ex on int {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isFalse);
}
void test_applicability_extensionOnClass_couldApplyToSubclass() async {
var library = await bootPackageWithLibrary('''
class C {}
class D extends C {}
extension Ex on C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('D')), isTrue);
}
void test_applicability_extensionOnClass_couldApplyToInstantiation() async {
var library = await bootPackageWithLibrary('''
class C<T> {}
extension Ex on C<int> {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void
test_applicability_extensionOnClass_couldApplyToSubclassInstantiation() async {
var library = await bootPackageWithLibrary('''
class C<T> {}
class D<T> extends C<T> {}
extension Ex on C<int> {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('D')), isTrue);
}
void
test_applicability_extensionOnClass_couldApplyToSubclassInstantiation2() async {
var library = await bootPackageWithLibrary('''
class C<T> {}
class D extends C<String> {}
extension Ex on C<int> {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('D')), isFalse);
}
void test_applicability_extensionOnObject_couldApplyToClass() async {
var library = await bootPackageWithLibrary('''
extension Ex on Object {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void test_applicability_extensionOnObject_couldApplyToMixin() async {
var library = await bootPackageWithLibrary('''
extension Ex on Object {}
mixin M {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.mixins.named('M')), isTrue);
}
void test_applicability_extensionOnDynamic_couldApplyToClass() async {
var library = await bootPackageWithLibrary('''
extension Ex on dynamic {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void test_applicability_extensionOnDynamic_alwaysApplies() async {
var library = await bootPackageWithLibrary('''
extension Ex on dynamic {}
''');
expect(library.extensions.named('Ex').alwaysApplies, isTrue);
}
void test_applicability_extensionOnVoid_couldApplyToClass() async {
var library = await bootPackageWithLibrary('''
extension Ex on void {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void test_applicability_extensionOnNull_couldApplyToClass() async {
var library = await bootPackageWithLibrary('''
extension Ex on Null {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isFalse);
}
void test_applicability_extensionOnTypeVariable_couldApplyToClass() async {
var library = await bootPackageWithLibrary('''
extension Ex<T> on T {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isTrue);
}
void test_applicability_extensionOnTypeVariable_alwaysApplies() async {
var library = await bootPackageWithLibrary('''
extension Ex<T> on T {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.alwaysApplies, isTrue);
}
void
test_applicability_extensionOnTypeVariableWithBound_couldApplyToSubtypeOfBound() async {
var library = await bootPackageWithLibrary('''
class C {}
class D extends C {}
extension Ex<T extends C> on T {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('D')), isTrue);
}
void
test_applicability_extensionOnTypeVariableWithBound_couldApplyToUnrelated() async {
var library = await bootPackageWithLibrary('''
extension Ex<T extends num> on T {}
class C {}
''');
var ex = library.extensions.named('Ex');
expect(ex.couldApplyTo(library.classes.named('C')), isFalse);
}
void test_referenceToExtension() async {
var library = await bootPackageWithLibrary('''
extension Ex on int {}
/// Text [Ex].
var f() {}
''');
var f = library.functions.named('f');
expect(f.fullyQualifiedName, 'extension_methods.f');
expect(
f.documentationAsHtml,
contains('<a href="$linkPrefix/Ex.html">Ex</a>'),
);
}
void test_referenceToMissingElement_onExtensionMember() async {
var library = await bootPackageWithLibrary('''
extension E on int {
/// Text [NotFound] text.
int get f => 7;
}
''');
var f = library.extensions.first.instanceFields.first;
expect(f.fullyQualifiedName, 'extension_methods.E.f');
// We are primarily testing that dartdoc does not crash when trying to
// resolve an unknown reference, from the position of an extension member.
expect(
f.documentationAsHtml,
contains('<p>Text <code>NotFound</code> text.</p>'),
);
}
void test_referenceToExtensionMethod() async {
var library = await bootPackageWithLibrary('''
extension Ex on int {
void m() {}
}
/// Text [Ex.m].
var f() {}
''');
var f = library.functions.named('f');
expect(f.fullyQualifiedName, 'extension_methods.f');
expect(
f.documentationAsHtml,
contains('<a href="$linkPrefix/Ex/m.html">Ex.m</a>'),
);
}
void test_referenceToExtensionGetter() async {
var library = await bootPackageWithLibrary('''
extension Ex on int {
bool get b => true;
}
/// Text [Ex.b].
var f() {}
''');
var f = library.functions.named('f');
expect(f.fullyQualifiedName, 'extension_methods.f');
expect(
f.documentationAsHtml,
contains('<a href="$linkPrefix/Ex/b.html">Ex.b</a>'),
);
}
void test_referenceToExtensionSetter() async {
var library = await bootPackageWithLibrary('''
extension Ex on int {
set b(int value) {}
}
/// Text [Ex.b].
var f() {}
''');
var f = library.functions.named('f');
expect(f.fullyQualifiedName, 'extension_methods.f');
expect(
f.documentationAsHtml,
contains('<a href="$linkPrefix/Ex/b.html">Ex.b</a>'),
);
}
// TODO(srawlins): Test everything else about extensions.
}
@reflectiveTest
class ExtensionMethodsExportTest extends DartdocTestBase {
@override
String get libraryName => 'extension_methods';
late Package package;
/// Verifies that comment reference text, [referenceText] attached to the
/// function, `f`, is resolved to [expected], and links to [href].
void expectReferenceValidFromF(
String referenceText, ModelElement expected, String href) {
var fFunction = package.functions.named('f');
var reference = getMatchingLinkElement(referenceText, fFunction)
.commentReferable as ModelElement;
expect(identical(reference.canonicalModelElement, expected), isTrue,
reason: '$expected (${expected.hashCode}) is not '
'${reference.canonicalModelElement} '
'(${reference.canonicalModelElement.hashCode})');
expect(expected.isCanonical, isTrue);
expect(expected.href, endsWith(href));
}
/// Sets up a package with three files:
///
/// * a private file containing a class, `C`, an extension on that class, `E`,
/// `E`, and a method in that extension, `m`.
/// * A public file that exports some members of the private file. The content
/// of this file is specified with [exportingLibraryContent].
/// * Another public file which is completely unrelated to the first two (no
/// imports or exports linking them), which contains a function, `f`.
Future<void> setupWith(String exportingLibraryContent) async {
var packageGraph = await bootPackageFromFiles([
d.dir('lib', [
d.dir('src', [
d.file('lib.dart', '''
class C {}
extension Ex on C {
void m() {}
}
'''),
]),
d.file('one.dart', exportingLibraryContent),
d.file('two.dart', '''
/// Comment.
var f() {}
'''),
])
]);
package = packageGraph.defaultPackage;
}
void test_reexportWithShow() async {
await setupWith("export 'src/lib.dart' show C, Ex;");
var ex = package.extensions.named('Ex');
var m = ex.instanceMethods.named('m');
expectReferenceValidFromF('Ex', ex, '%one/Ex.html');
expectReferenceValidFromF('Ex.m', m, '%one/Ex/m.html');
}
void test_reexportWithHide() async {
await setupWith("export 'src/lib.dart' hide A;");
var ex = package.extensions.named('Ex');
var m = ex.instanceMethods.named('m');
expectReferenceValidFromF('Ex', ex, '%one/Ex.html');
expectReferenceValidFromF('Ex.m', m, '%one/Ex/m.html');
}
void test_reexportFull() async {
await setupWith("export 'src/lib.dart';");
var ex = package.extensions.named('Ex');
var m = ex.instanceMethods.named('m');
expectReferenceValidFromF('Ex', ex, '%one/Ex.html');
expectReferenceValidFromF('Ex.m', m, '%one/Ex/m.html');
}
}