Skip to content

Commit 449478c

Browse files
authored
Search: specify package rank in generation; distinguish non-core Dart libs (dart-lang#3427)
1 parent 81af1bf commit 449478c

File tree

8 files changed

+2754
-2608
lines changed

8 files changed

+2754
-2608
lines changed

lib/resources/docs.dart.js

Lines changed: 2488 additions & 2515 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/resources/docs.dart.js.map

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/generator/generator_backend.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ abstract class GeneratorBackendBase implements GeneratorBackend {
170170
@override
171171
void generateSearchIndex(List<Indexable> indexedElements) {
172172
var json = generator_util.generateSearchIndexJson(
173-
indexedElements, options.prettyIndexJson, options.packageOrder);
173+
indexedElements,
174+
packageOrder: options.packageOrder,
175+
pretty: options.prettyIndexJson,
176+
);
174177
if (!options.useBaseHref) {
175178
json = json.replaceAll(htmlBasePlaceholder, '');
176179
}

lib/src/generator/generator_utils.dart

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,24 @@ String generateCategoryJson(Iterable<Categorization> categories, bool pretty) {
3535
return encoder.convert(indexItems.toList(growable: false));
3636
}
3737

38-
String generateSearchIndexJson(Iterable<Indexable> indexedElements, bool pretty,
39-
List<String> packageOrder) {
38+
/// Generates the text of the search index file (`index.json`) containing
39+
/// [indexedItems] and [packageOrder].
40+
///
41+
/// Passing `pretty: true` will use a [JsonEncoder] with a single-space indent.
42+
String generateSearchIndexJson(Iterable<Indexable> indexedElements,
43+
{required List<String> packageOrder, required bool pretty}) {
4044
final indexItems = [
41-
{packageOrderKey: packageOrder},
4245
for (final indexable
4346
in indexedElements.sorted(_compareElementRepresentations))
4447
{
4548
'name': indexable.name,
4649
'qualifiedName': indexable.fullyQualifiedName,
4750
'href': indexable.href,
4851
'kind': indexable.kind.index,
52+
// TODO(srawlins): Only include this for [Inheritable] items.
4953
'overriddenDepth': indexable.overriddenDepth,
50-
if (indexable is ModelElement) 'packageName': indexable.package.name,
54+
if (indexable is ModelElement)
55+
'packageRank': _packageRank(packageOrder, indexable),
5156
if (indexable is ModelElement)
5257
'desc': _removeHtmlTags(indexable.oneLineDoc),
5358
if (indexable is EnclosedElement)
@@ -65,8 +70,44 @@ String generateSearchIndexJson(Iterable<Indexable> indexedElements, bool pretty,
6570
return encoder.convert(indexItems);
6671
}
6772

68-
/// The key used in the `index.json` file used to specify the package order.
69-
const packageOrderKey = '__PACKAGE_ORDER__';
73+
/// The "package rank" of [element], given a [packageOrder].
74+
///
75+
/// Briefly, this is 10 times the element's package's position in the
76+
/// [packageOrder], or 10 times the length of [packageOrder] if the element's
77+
/// package is not listed in [packageOrder].
78+
int _packageRank(List<String> packageOrder, ModelElement element) {
79+
if (packageOrder.isEmpty) return 0;
80+
var packageName = element.package.name;
81+
var index = packageOrder.indexOf(packageName);
82+
if (index == -1) return packageOrder.length * 10;
83+
if (packageName == 'Dart' &&
84+
!_dartCoreLibraries.contains(element.library.name)) {
85+
// Non-"core" Dart SDK libraries should be ranked slightly lower than "core"
86+
// Dart SDK libraries. The "core" Dart SDK libraries are the ones labeled as
87+
// such at <https://api.dart.dev>, which can be used in both VM and Web
88+
// environments.
89+
// Note we choose integers throughout this function (as opposed to adding
90+
// 0.5 here) in order to facilitate a proper [Comparable] comparison.
91+
return index * 10 + 5;
92+
}
93+
return index * 10;
94+
}
95+
96+
/// The set of "core" Dart libraries, used to rank contained items above items
97+
/// declared elsewhere in the Dart SDK.
98+
const _dartCoreLibraries = {
99+
'dart:async',
100+
'dart:collection',
101+
'dart:convert',
102+
'dart:core',
103+
'dart:developer',
104+
'dart:math',
105+
'dart:typed_data',
106+
107+
// Plus the two core Flutter engine libraries.
108+
'dart:ui',
109+
'dart:web_ui',
110+
};
70111

71112
String _removeHtmlTags(String? input) =>
72113
input?.replaceAll(_htmlTagPattern, '') ?? '';

lib/src/search.dart

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'dart:convert';
66

7-
import 'package:dartdoc/src/generator/generator_utils.dart';
87
import 'package:dartdoc/src/model/indexable.dart';
98
import 'package:meta/meta.dart';
109

@@ -17,30 +16,14 @@ enum _MatchPosition {
1716
}
1817

1918
class Index {
20-
final List<String> packageOrder;
2119
final List<IndexItem> index;
2220

2321
@visibleForTesting
24-
Index(this.packageOrder, this.index);
22+
Index(this.index);
2523

2624
factory Index.fromJson(String text) {
2725
var jsonIndex = (jsonDecode(text) as List).cast<Map<String, dynamic>>();
28-
var indexList = <IndexItem>[];
29-
var packageOrder = <String>[];
30-
for (var entry in jsonIndex) {
31-
if (entry.containsKey(packageOrderKey)) {
32-
packageOrder.addAll((entry[packageOrderKey] as List).cast<String>());
33-
} else {
34-
indexList.add(IndexItem.fromMap(entry));
35-
}
36-
}
37-
return Index(packageOrder, indexList);
38-
}
39-
40-
int packageOrderPosition(String packageName) {
41-
if (packageOrder.isEmpty) return 0;
42-
var index = packageOrder.indexOf(packageName);
43-
return index == -1 ? packageOrder.length : index;
26+
return Index(jsonIndex.map(IndexItem.fromMap).toList());
4427
}
4528

4629
List<IndexItem> find(String rawQuery) {
@@ -84,8 +67,7 @@ class Index {
8467
}
8568

8669
// Prefer packages higher in the package order.
87-
comparison = packageOrderPosition(a.item.packageName) -
88-
packageOrderPosition(b.item.packageName);
70+
comparison = a.item.packageRank - b.item.packageRank;
8971
if (comparison != 0) {
9072
return comparison;
9173
}
@@ -113,11 +95,7 @@ class Index {
11395
class IndexItem {
11496
final String name;
11597
final String qualifiedName;
116-
117-
// TODO(srawlins): Store the index of the package in package order instead of
118-
// this String. The Strings bloat the `index.json` file and keeping duplicate
119-
// parsed Strings in memory is expensive.
120-
final String packageName;
98+
final int packageRank;
12199
final Kind kind;
122100
final String? href;
123101
final int overriddenDepth;
@@ -127,7 +105,7 @@ class IndexItem {
127105
IndexItem._({
128106
required this.name,
129107
required this.qualifiedName,
130-
required this.packageName,
108+
required this.packageRank,
131109
required this.kind,
132110
required this.desc,
133111
required this.href,
@@ -144,14 +122,11 @@ class IndexItem {
144122
// "href":"dartdoc/Dartdoc-class.html",
145123
// "kind":1,
146124
// "overriddenDepth":0,
147-
// "packageName":"dartdoc"
125+
// "packageRank":0
148126
// ["enclosedBy":{"name":"dartdoc","kind":2}]
149127
// }
150128
// ```
151129
factory IndexItem.fromMap(Map<String, dynamic> data) {
152-
// Note that this map also contains 'packageName', but we're not currently
153-
// using that info.
154-
155130
EnclosedBy? enclosedBy;
156131
if (data['enclosedBy'] != null) {
157132
final map = data['enclosedBy'] as Map<String, dynamic>;
@@ -164,7 +139,7 @@ class IndexItem {
164139
return IndexItem._(
165140
name: data['name'],
166141
qualifiedName: data['qualifiedName'],
167-
packageName: data['packageName'],
142+
packageRank: data['packageRank'] as int,
168143
href: data['href'],
169144
kind: Kind.values[data['kind'] as int],
170145
overriddenDepth: (data['overriddenDepth'] as int?) ?? 0,

test/dartdoc_test_base.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ abstract class DartdocTestBase {
4040

4141
List<String> get experiments => [];
4242

43+
bool get skipUnreachableSdkLibraries => true;
44+
4345
@mustCallSuper
4446
Future<void> setUp() async {
4547
packageMetaProvider = testPackageMetaProvider;
@@ -83,12 +85,13 @@ analyzer:
8385
packageMetaProvider,
8486
packageConfigProvider,
8587
additionalArguments: additionalArguments,
88+
skipUnreachableSdkLibraries: skipUnreachableSdkLibraries,
8689
);
8790
}
8891

8992
/// Creates a single library named [libraryName], with optional preamble
90-
/// [libraryPreamble]. Optionally, pass a [FileGenerator] to create
91-
/// extra files in the package such as `dartdoc_options.yaml`.
93+
/// [libraryPreamble]. Optionally, pass [extraFiles] such as
94+
/// `dartdoc_options.yaml`.
9295
Future<Library> bootPackageWithLibrary(String libraryContent,
9396
{String libraryPreamble = '',
9497
Iterable<d.Descriptor> extraFiles = const [],

test/search_index_test.dart

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:dartdoc/src/generator/generator_utils.dart';
8+
import 'package:dartdoc/src/model/model.dart';
9+
import 'package:test/test.dart';
10+
import 'package:test_reflective_loader/test_reflective_loader.dart';
11+
12+
import 'dartdoc_test_base.dart';
13+
import 'src/utils.dart';
14+
15+
void main() {
16+
defineReflectiveSuite(() {
17+
if (recordsAllowed) {
18+
defineReflectiveTests(SearchIndexTest);
19+
}
20+
});
21+
}
22+
23+
@reflectiveTest
24+
class SearchIndexTest extends DartdocTestBase {
25+
@override
26+
String get libraryName => 'index_json';
27+
28+
/// We need the "unreachable" SDK libraries in order to include the non-"core"
29+
/// ones like 'dart:io'.
30+
@override
31+
bool get skipUnreachableSdkLibraries => false;
32+
33+
/// Returns the JSON-decoded text of the search index which is generated with
34+
/// the elements from [libraryContent].
35+
///
36+
/// Specify `actAsFlutter: true` to auto-include dependencies, and to specify
37+
/// a package order with "flutter" and "Dart".
38+
Future<List<Map<String, dynamic>>> jsonIndexForPackageWithLibrary(
39+
String libraryContent, {
40+
bool actAsFlutter = false,
41+
}) async {
42+
var library = await bootPackageWithLibrary(
43+
libraryContent,
44+
additionalArguments:
45+
actAsFlutter ? const ['--auto-include-dependencies'] : const [],
46+
);
47+
var libraries = [
48+
library,
49+
if (actAsFlutter)
50+
...library.packageGraph.libraries
51+
.where((l) => l.name.startsWith('dart:')),
52+
];
53+
var text = generateSearchIndexJson(
54+
[
55+
// For each library, gather the elements for the library, it's members,
56+
// and the members of its container members.
57+
for (var library in libraries) ...[
58+
library,
59+
...library.allModelElements,
60+
for (var element in library.allModelElements.whereType<Container>())
61+
...element.allModelElements,
62+
],
63+
],
64+
packageOrder: actAsFlutter ? const ['flutter', 'Dart'] : const [],
65+
pretty: false,
66+
);
67+
return (jsonDecode(text) as List).cast<Map<String, dynamic>>();
68+
}
69+
70+
void test_class() async {
71+
var jsonIndex = await jsonIndexForPackageWithLibrary('''
72+
/// A class.
73+
class C {}
74+
''');
75+
var classItem = jsonIndex.named('index_json.C');
76+
77+
expect(classItem['kind'], equals(Kind.class_.index));
78+
expect(classItem['overriddenDepth'], equals(0));
79+
expect(classItem['desc'], equals('A class.'));
80+
expect(
81+
classItem['enclosedBy'],
82+
equals({
83+
'name': 'index_json',
84+
'kind': Kind.library.index,
85+
'href':
86+
'%%__HTMLBASE_dartdoc_internal__%%index_json/index_json-library.html',
87+
}),
88+
);
89+
}
90+
91+
void test_library() async {
92+
var jsonIndex = await jsonIndexForPackageWithLibrary('''
93+
/// A library.
94+
library;
95+
''');
96+
var libraryItem = jsonIndex.named('index_json');
97+
98+
expect(libraryItem['kind'], equals(Kind.library.index));
99+
expect(libraryItem['overriddenDepth'], equals(0));
100+
// TODO(srawlins): Should not be blank.
101+
expect(libraryItem['desc'], equals(''));
102+
}
103+
104+
void test_method() async {
105+
var jsonIndex = await jsonIndexForPackageWithLibrary('''
106+
class C {
107+
/// A method.
108+
void m() {}
109+
}
110+
''');
111+
var methodItem = jsonIndex.named('index_json.C.m');
112+
113+
expect(methodItem['kind'], equals(Kind.method.index));
114+
expect(methodItem['overriddenDepth'], equals(0));
115+
expect(methodItem['desc'], equals('A method.'));
116+
expect(
117+
methodItem['enclosedBy'],
118+
equals({
119+
'name': 'C',
120+
'kind': Kind.class_.index,
121+
'href': '%%__HTMLBASE_dartdoc_internal__%%index_json/C-class.html',
122+
}),
123+
);
124+
}
125+
126+
void test_overriddenMethod() async {
127+
var jsonIndex = await jsonIndexForPackageWithLibrary('''
128+
class C {
129+
void m() {}
130+
}
131+
class D extends C {
132+
/// A method.
133+
@override
134+
void m() {}
135+
}
136+
''');
137+
var methodItem = jsonIndex.named('index_json.D.m');
138+
139+
expect(methodItem['kind'], equals(Kind.method.index));
140+
expect(methodItem['overriddenDepth'], equals(1));
141+
expect(methodItem['desc'], equals('A method.'));
142+
expect(
143+
methodItem['enclosedBy'],
144+
equals({
145+
'name': 'D',
146+
'kind': Kind.class_.index,
147+
'href': '%%__HTMLBASE_dartdoc_internal__%%index_json/D-class.html',
148+
}),
149+
);
150+
}
151+
152+
void test_sdkPackageRank_core() async {
153+
var jsonIndex = await jsonIndexForPackageWithLibrary(
154+
actAsFlutter: true,
155+
'''
156+
/// A library.
157+
library;
158+
''',
159+
);
160+
var classItem = jsonIndex.named('dart:core.List');
161+
162+
expect(classItem['kind'], equals(Kind.class_.index));
163+
expect(classItem['packageRank'], equals(10));
164+
}
165+
166+
void test_sdkPackageRank_nonCore() async {
167+
var jsonIndex = await jsonIndexForPackageWithLibrary(
168+
actAsFlutter: true,
169+
'''
170+
/// A library.
171+
library;
172+
''',
173+
);
174+
var classItem = jsonIndex.named('dart:io.File');
175+
176+
expect(classItem['kind'], equals(Kind.class_.index));
177+
expect(classItem['packageRank'], equals(15));
178+
}
179+
}
180+
181+
extension on List<Map<String, dynamic>> {
182+
Map<String, dynamic> named(String qualifiedName) =>
183+
firstWhere((e) => e['qualifiedName'] == qualifiedName);
184+
}

0 commit comments

Comments
 (0)