Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
- fix: package name resolving for types referenced via a package-reference ('package:some_package/some_entrypoint.dart)
- fix: private top level methods where treated as part of the public API (resulting in all types used there treated as part of the public API as well)
- fix: don't print color control sequences if no terminal is attached (e.g. when piping the output to a file)
- fix: carry over entry points of type aliases to the aliased type (especially important for private types)
- fix: naming of unary operators was wrong (unaryunary...)

## Version 0.22.0
- fix: Fixes an issue if we have to deal with two types with the same name
Expand Down
48 changes: 48 additions & 0 deletions lib/src/analyze/package_api_analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ class PackageApiAnalyzer {

_mergeSuperTypes(collectedInterfaces);

// carry over entry points from typedefs to their aliased types
_carryOverTypedefEntryPoints(collectedInterfaces);

// extract package declarations
for (final classId in collectedInterfaces.keys) {
final entry = collectedInterfaces[classId]!;
Expand Down Expand Up @@ -524,6 +527,51 @@ class PackageApiAnalyzer {
}
return mergedSuperTypeIds;
}

/// Carries over entry points from typedefs to their aliased types.
///
/// When a typedef aliases a private type, the private type should have
/// the same entry points as the typedef to properly track breaking changes.
void _carryOverTypedefEntryPoints(
Map<int?, _InterfaceCollectionResult> collectedInterfaces) {
// First, collect all typedefs with their entry points
final typedefsByAliasedType =
<String, List<InternalTypeAliasDeclaration>>{};

for (final entry in collectedInterfaces.values) {
for (final typedef in entry.typeAliasDeclarations) {
if (typedef.entryPoints?.isNotEmpty == true) {
final aliasedTypeName = typedef.aliasedTypeName;
if (!typedefsByAliasedType.containsKey(aliasedTypeName)) {
typedefsByAliasedType[aliasedTypeName] = [];
}
typedefsByAliasedType[aliasedTypeName]!.add(typedef);
}
}
}

// Then, find matching interface declarations and carry over entry points
for (final entry in collectedInterfaces.values) {
for (final interface in entry.interfaceDeclarations) {
final typedefs = typedefsByAliasedType[interface.name];
if (typedefs != null) {
// Collect all entry points from all typedefs that alias this type
final entryPointsToAdd = <String>{};
for (final typedef in typedefs) {
if (typedef.entryPoints != null) {
entryPointsToAdd.addAll(typedef.entryPoints!);
}
}

// Add the entry points to the aliased type
if (entryPointsToAdd.isNotEmpty) {
_addEntryPoints<InternalInterfaceDeclaration>(
entry.interfaceDeclarations, interface.id, entryPointsToAdd);
}
}
}
}
}
}

class _InterfaceCollectionResult {
Expand Down
14 changes: 11 additions & 3 deletions lib/src/model/internal/internal_executable_declaration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ class InternalExecutableDeclaration implements InternalDeclaration {
// This is a fix for the only method overloading in Dart the `-` operator
name: (executableElement is MethodElement2 &&
executableElement.isOperator)
? (executableElement.formalParameters.isEmpty
? () {
final rawName =
executableElement.name3 ?? executableElement.displayName;
final prefix = executableElement.formalParameters.isEmpty
? 'unary'
: 'binary') +
(executableElement.name3 ?? executableElement.displayName)
: 'binary';
// If the raw name already starts with the prefix then don't add it again
if (rawName.startsWith(prefix)) {
return rawName;
}
return prefix + rawName;
}()
: executableElement.name3 ?? executableElement.displayName,
namespace: namespace,
isDeprecated: executableElement.metadata2.hasDeprecated,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:dart_apitool/api_tool.dart';
import 'package:test/test.dart';

void main() {
group('Private types exclusion', () {
late PackageApiAnalyzer packageAnalyzer;
late PackageApi packageApi;
setUp(() async {
packageAnalyzer = PackageApiAnalyzer(
packagePath: 'test/test_packages/missing_export/package_a',
);
packageApi = await packageAnalyzer.analyze();
});

test('should not include private class in extracted API', () {
final privateClassDeclarations = packageApi.interfaceDeclarations
.where((id) => id.name == '_PrivateClass');
expect(privateClassDeclarations, isEmpty,
reason:
'Private class _PrivateClass should not be included in public API');
});

test('should not include private enum in extracted API', () {
final privateEnumDeclarations = packageApi.interfaceDeclarations
.where((id) => id.name == '_PrivateEnum');
expect(privateEnumDeclarations, isEmpty,
reason:
'Private enum _PrivateEnum should not be included in public API');
});

test('should not include private property in extracted API', () {
final privatePropertyDeclarations = packageApi.fieldDeclarations
.where((fd) => fd.name == '_privateProperty');
expect(privatePropertyDeclarations, isEmpty,
reason:
'Private property _privateProperty should not be included in public API');
});

test('should not include private function in extracted API', () {
final privateFunctionDeclarations = packageApi.executableDeclarations
.where((ed) => ed.name == '_privateFunction');
expect(privateFunctionDeclarations, isEmpty,
reason:
'Private function _privateFunction should not be included in public API');
});

test('should not include any declarations from private_types.dart', () {
// Check that no declarations have their relative path pointing to private_types.dart
final allDeclarations = [
...packageApi.interfaceDeclarations,
...packageApi.executableDeclarations,
...packageApi.fieldDeclarations,
...packageApi.typeAliasDeclarations,
];

final privateTypesDeclarations = allDeclarations
.where((d) => d.relativePath.endsWith('private_types.dart'));

expect(privateTypesDeclarations, isEmpty,
reason:
'No declarations from private_types.dart should be included in public API');
});

test('should verify that public types are still extracted', () {
// Verify that the test package still extracts legitimate public API elements
final classADeclarations =
packageApi.interfaceDeclarations.where((id) => id.name == 'ClassA');
expect(classADeclarations, isNotEmpty,
reason: 'Public class ClassA should be included in API');

// Check for ClassC as a typedef instead of interface
final classCTypeAliases =
packageApi.typeAliasDeclarations.where((tad) => tad.name == 'ClassC');
expect(classCTypeAliases, isNotEmpty,
reason: 'Public typedef ClassC should be included in API');
});
});
}
24 changes: 24 additions & 0 deletions test/integration_tests/analyze/typedef_handling_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:dart_apitool/api_tool.dart';
import 'package:test/test.dart';

void main() {
group('Typedefs', () {
late PackageApiAnalyzer packageWithTypedefExportAnalyzer;
late PackageApi packageWithTypedefExport;
setUp(() async {
packageWithTypedefExportAnalyzer = PackageApiAnalyzer(
packagePath: 'test/test_packages/missing_export/package_a',
);
packageWithTypedefExport =
await packageWithTypedefExportAnalyzer.analyze();
});

test('carry over the entrypoint to the aliased type', () {
final privateClassCExports = packageWithTypedefExport
.interfaceDeclarations
.where((id) => id.name == '_PrivateClassC');
expect(privateClassCExports, isNotEmpty);
expect(privateClassCExports.single.entryPoints?.isNotEmpty, isTrue);
});
});
}
35 changes: 35 additions & 0 deletions test/integration_tests/diff/test_package_operators_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,40 @@ void main() {
);
});
});

group('reproducing GitHub issue #225', () {
test('unary operator name should not show "unary" twice', () async {
// Create a simple test case to check the operator naming
final api = await packageAWithRecord.analyze();

// Find the ClassA interface
final classAInterface = api.interfaceDeclarations
.firstWhere((interface) => interface.name == 'ClassA');

// Look for minus operators (binary and unary)
final minusOperators = classAInterface.executableDeclarations
.where((method) => method.name.contains('-'))
.toList();

// Should have both binary and unary minus operators
expect(minusOperators.length, equals(2));

// Find the specific operators
final binaryMinusOperator =
minusOperators.firstWhere((op) => op.name == 'binary-');
final unaryMinusOperator =
minusOperators.firstWhere((op) => op.name == 'unary-');

// Verify the names are correct (not duplicated)
expect(binaryMinusOperator.name, equals('binary-'));
expect(unaryMinusOperator.name, equals('unary-'));

// Check if any of them has duplicated text
for (final op in minusOperators) {
expect(op.name, isNot(contains('unaryunary')));
expect(op.name, isNot(contains('binarybinary')));
}
});
});
});
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
library package_a;

export 'types/class_a.dart';
export 'types/class_c.dart' show ClassC;
export 'types/private_types.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
typedef ClassC = _PrivateClassC;

class _PrivateClassC {
final String anotherProperty;

_PrivateClassC(this.anotherProperty);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class _PrivateClass {}

bool get _privateProperty => false;

String _privateFunction() => 'This is private';

enum _PrivateEnum {
value1,
value2,
}
Loading