forked from dart-lang/dartdoc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcanonicalization.dart
269 lines (231 loc) · 9.54 KB
/
canonicalization.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright (c) 2019, 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:analyzer/dart/element/element2.dart';
// ignore: implementation_imports
import 'package:analyzer/src/dart/element/element.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/warnings.dart';
const int _separatorChar = 0x3B;
/// Searches [PackageGraph.libraryExports] for a public, documented library
/// which exports this [ModelElement], ideally in its library's package.
Library? canonicalLibraryCandidate(ModelElement modelElement) {
var thisAndExported =
modelElement.packageGraph.libraryExports[modelElement.library.element];
if (thisAndExported == null) {
return null;
}
// Since we're looking for a library, go up in the tree until we find it.
var topLevelElement = modelElement.element;
while (topLevelElement.enclosingElement2 is! LibraryElement2 &&
topLevelElement.enclosingElement2 != null) {
topLevelElement = topLevelElement.enclosingElement2!;
}
var topLevelElementName = topLevelElement.name3;
if (topLevelElementName == null) {
// Any member of an unnamed extension is not public, and has no
// canonical library.
return null;
}
final candidateLibraries = thisAndExported.where((l) {
if (!l.isPublic) return false;
if (l.package.documentedWhere == DocumentLocation.missing) return false;
if (modelElement is Library) return true;
var lookup = l.element.exportNamespace.definedNames2[topLevelElementName];
return topLevelElement ==
(lookup is PropertyAccessorElement2 ? lookup.variable3 : lookup);
}).toList(growable: true);
if (candidateLibraries.isEmpty) {
return null;
}
if (candidateLibraries.length == 1) {
return candidateLibraries.single;
}
var remoteLibraries = candidateLibraries
.where((l) => l.package.documentedWhere == DocumentLocation.remote);
if (remoteLibraries.length == 1) {
// If one or more local libraries export code from a remotely documented
// library (and we're linking to remote libraries), then just use the remote
// library.
return remoteLibraries.single;
}
var topLevelModelElement =
ModelElement.forElement(topLevelElement, modelElement.packageGraph);
return _Canonicalization(topLevelModelElement)
.canonicalLibraryCandidate(candidateLibraries);
}
/// Canonicalization support in Dartdoc.
///
/// This provides heuristic scoring to determine which library a human likely
/// considers this element to be primarily 'from', and therefore, canonical.
/// Still warn if the heuristic isn't very confident.
final class _Canonicalization {
final ModelElement _modelElement;
_Canonicalization(this._modelElement);
/// Append an encoded form of the given [component] to the given [buffer].
void _encode(StringBuffer buffer, String component) {
var length = component.length;
for (var i = 0; i < length; i++) {
var currentChar = component.codeUnitAt(i);
if (currentChar == _separatorChar) {
buffer.writeCharCode(_separatorChar);
}
buffer.writeCharCode(currentChar);
}
}
// Copied from package analyzer ElementLocationImpl.fromElement.
String _getElementLocation(Element2 element) {
var components = <String>[];
Element2? ancestor = element;
while (ancestor != null) {
if (ancestor is! ElementImpl2) {
if (ancestor is LibraryElementImpl) {
components.insert(0, ancestor.identifier);
} else {
throw Exception('${ancestor.runtimeType} is not an ElementImpl2');
}
ancestor = ancestor.enclosingElement2;
} else {
components.insert(0, ancestor.identifier);
if (ancestor is LocalFunctionElementImpl) {
ancestor = (ancestor.wrappedElement.enclosingElement2
as ExecutableElementImpl)
.element;
} else {
ancestor = ancestor.enclosingElement2;
}
}
}
var buffer = StringBuffer();
var length = components.length;
for (var i = 0; i < length; i++) {
if (i > 0) {
buffer.writeCharCode(_separatorChar);
}
_encode(buffer, components[i]);
}
return buffer.toString();
}
/// Calculates a candidate for the canonical library of [_modelElement], among [libraries].
Library canonicalLibraryCandidate(Iterable<Library> libraries) {
var locationPieces = _getElementLocation(_modelElement.element)
.split(_locationSplitter)
.where((s) => s.isNotEmpty)
.toSet();
var scoredCandidates = libraries
.map((library) => _scoreElementWithLibrary(
library, _modelElement.fullyQualifiedName, locationPieces))
.toList(growable: false)
..sort();
final librariesByScore = scoredCandidates.map((s) => s.library).toList();
var secondHighestScore =
scoredCandidates[scoredCandidates.length - 2].score;
var highestScore = scoredCandidates.last.score;
var confidence = highestScore - secondHighestScore;
final canonicalLibrary = librariesByScore.last;
if (confidence <
_modelElement.config.ambiguousReexportScorerMinConfidence) {
var libraryNames = librariesByScore.map((l) => l.name);
var message = '$libraryNames -> ${canonicalLibrary.name} '
'(confidence ${confidence.toStringAsPrecision(4)})';
_modelElement.warn(PackageWarning.ambiguousReexport,
message: message, extendedDebug: scoredCandidates.map((s) => '$s'));
}
return canonicalLibrary;
}
// TODO(srawlins): This function is minimally tested; it's tricky to unit test
// because it takes a lot of elements into account, like URIs, differing
// package names, etc. Anyways, add more tests, in addition to the
// `StringName` tests in `model_test.dart`.
static _ScoredCandidate _scoreElementWithLibrary(Library library,
String elementQualifiedName, Set<String> elementLocationPieces) {
var scoredCandidate = _ScoredCandidate(library);
// Large boost for `@canonicalFor`, essentially overriding all other
// concerns.
if (library.canonicalFor.contains(elementQualifiedName)) {
scoredCandidate._alterScore(5.0, _Reason.canonicalFor);
}
// Penalty for deprecated libraries.
if (library.isDeprecated) {
scoredCandidate._alterScore(-1.0, _Reason.deprecated);
}
var libraryNamePieces = {
...library.name.split('.').where((s) => s.isNotEmpty)
};
// Give a big boost if the library has the package name embedded in it.
if (libraryNamePieces.contains(library.package.name)) {
scoredCandidate._alterScore(1.0, _Reason.packageName);
}
// Same idea as the above, for the Dart SDK.
if (library.name == 'dart:core') {
scoredCandidate._alterScore(0.9, _Reason.packageName);
}
// Give a tiny boost for libraries with long names, assuming they're
// more specific (and therefore more likely to be the owner of this symbol).
scoredCandidate._alterScore(
.01 * libraryNamePieces.length, _Reason.longName);
// If we don't know the location of this element (which shouldn't be
// possible), return our best guess.
assert(elementLocationPieces.isNotEmpty);
if (elementLocationPieces.isEmpty) return scoredCandidate;
// The more pieces we have of the location in our library name, the more we
// should boost our score.
scoredCandidate._alterScore(
libraryNamePieces.intersection(elementLocationPieces).length.toDouble() /
elementLocationPieces.length.toDouble(),
_Reason.sharedNamePart,
);
// If pieces of location at least start with elements of our library name,
// boost the score a little bit.
var scoreBoost = 0.0;
for (var piece in elementLocationPieces.expand((item) => item.split('_'))) {
for (var namePiece in libraryNamePieces) {
if (piece.startsWith(namePiece)) {
scoreBoost += 0.001;
}
}
}
scoredCandidate._alterScore(scoreBoost, _Reason.locationPartStart);
return scoredCandidate;
}
}
/// A pattern that can split [Locatable.location] strings.
final _locationSplitter = RegExp(r'(package:|[\\/;.])');
/// This class represents the score for a particular element; how likely
/// it is that this is the canonical element.
class _ScoredCandidate implements Comparable<_ScoredCandidate> {
final List<(_Reason, double)> _reasons = [];
final Library library;
/// The score accumulated so far. Higher means it is more likely that this
/// is the intended canonical Library.
double score = 0.0;
_ScoredCandidate(this.library);
void _alterScore(double scoreDelta, _Reason reason) {
score += scoreDelta;
if (scoreDelta != 0) {
_reasons.add((reason, scoreDelta));
}
}
@override
int compareTo(_ScoredCandidate other) => score.compareTo(other.score);
@override
String toString() {
var reasonText = _reasons.map((r) {
var (reason, scoreDelta) = r;
var scoreDeltaPrefix = scoreDelta >= 0 ? '+' : '';
return '$reason ($scoreDeltaPrefix${scoreDelta.toStringAsPrecision(4)})';
});
return '${library.name}: ${score.toStringAsPrecision(4)} - $reasonText';
}
}
/// A reason that a candidate's score is changed.
enum _Reason {
canonicalFor('marked @canonicalFor'),
deprecated('is deprecated'),
packageName('embeds package name'),
longName('name is long'),
sharedNamePart('element location shares parts with name'),
locationPartStart('element location parts start with parts of name');
final String text;
const _Reason(this.text);
}