fbf331e0dd
Change-Id: I42b4a44fd6a2197e499e8623274b3cd1a4b5556f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/394003 Commit-Queue: Samuel Rawlins <srawlins@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Bob Nystrom <rnystrom@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
294 lines
9.8 KiB
Dart
294 lines
9.8 KiB
Dart
// Copyright (c) 2018, 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:analysis_server/src/services/completion/dart/completion_manager.dart';
|
|
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
|
|
import 'package:analysis_server/src/utilities/null_string_sink.dart';
|
|
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
|
import 'package:analyzer/dart/analysis/results.dart';
|
|
import 'package:analyzer/dart/ast/ast.dart';
|
|
import 'package:analyzer/dart/ast/visitor.dart';
|
|
import 'package:analyzer/file_system/overlay_file_system.dart';
|
|
import 'package:analyzer/file_system/physical_file_system.dart';
|
|
import 'package:analyzer/src/util/performance/operation_performance.dart';
|
|
|
|
/// A runner that can request code completion at the location of each identifier
|
|
/// in a Dart file.
|
|
class CompletionRunner {
|
|
/// The sink to which output is to be written.
|
|
final StringSink output;
|
|
|
|
/// A flag indicating whether to produce output about missing suggestions.
|
|
final bool printMissing;
|
|
|
|
/// A flag indicating whether to produce output about the quality of the sort
|
|
/// order.
|
|
final bool printQuality;
|
|
|
|
/// A flag indicating whether to produce timing information.
|
|
final bool timing;
|
|
|
|
/// A flag indicating whether to produce verbose output.
|
|
final bool verbose;
|
|
|
|
/// A flag indicating whether we should delete each identifier before
|
|
/// attempting to complete at that offset.
|
|
bool deleteBeforeCompletion = false;
|
|
|
|
/// Initialize a newly created completion runner.
|
|
CompletionRunner({
|
|
StringSink? output,
|
|
bool? printMissing,
|
|
bool? printQuality,
|
|
bool? timing,
|
|
bool? verbose,
|
|
}) : output = output ?? NullStringSink(),
|
|
printMissing = printMissing ?? false,
|
|
printQuality = printQuality ?? false,
|
|
timing = timing ?? false,
|
|
verbose = verbose ?? false;
|
|
|
|
/// Test the completion engine at the locations of each of the identifiers in
|
|
/// each of the files in the given [analysisRoot].
|
|
Future<void> runAll(String analysisRoot) async {
|
|
var resourceProvider = OverlayResourceProvider(
|
|
PhysicalResourceProvider.INSTANCE,
|
|
);
|
|
var collection = AnalysisContextCollection(
|
|
includedPaths: <String>[analysisRoot],
|
|
resourceProvider: resourceProvider,
|
|
);
|
|
var contributor = DartCompletionManager(
|
|
budget: CompletionBudget(const Duration(seconds: 30)),
|
|
);
|
|
var stamp = 1;
|
|
|
|
var fileCount = 0;
|
|
var identifierCount = 0;
|
|
var expectedCount = 0;
|
|
var missingCount = 0;
|
|
var indexCount = List<int>.filled(20, 0);
|
|
var filteredIndexCount = List<int>.filled(20, 0);
|
|
|
|
// Consider getting individual timings so that we can also report the
|
|
// longest and shortest times, or even a distribution.
|
|
var timer = Stopwatch();
|
|
|
|
for (var context in collection.contexts) {
|
|
for (var path in context.contextRoot.analyzedFiles()) {
|
|
if (!path.endsWith('.dart')) {
|
|
continue;
|
|
}
|
|
fileCount++;
|
|
output.write('.');
|
|
var result =
|
|
await context.currentSession.getResolvedUnit(path)
|
|
as ResolvedUnitResult;
|
|
var content = result.content;
|
|
var lineInfo = result.lineInfo;
|
|
var identifiers = _identifiersIn(result.unit);
|
|
|
|
for (var identifier in identifiers) {
|
|
identifierCount++;
|
|
var offset = identifier.offset;
|
|
if (deleteBeforeCompletion) {
|
|
var modifiedContent =
|
|
content.substring(0, offset) +
|
|
content.substring(identifier.end);
|
|
resourceProvider.setOverlay(
|
|
path,
|
|
content: modifiedContent,
|
|
modificationStamp: stamp++,
|
|
);
|
|
result =
|
|
await context.currentSession.getResolvedUnit(path)
|
|
as ResolvedUnitResult;
|
|
}
|
|
|
|
timer.start();
|
|
var dartRequest = DartCompletionRequest.forResolvedUnit(
|
|
resolvedUnit: result,
|
|
offset: offset,
|
|
);
|
|
var suggestions = await contributor.computeSuggestions(
|
|
dartRequest,
|
|
OperationPerformanceImpl('<root>'),
|
|
maxSuggestions: -1,
|
|
useFilter: true,
|
|
);
|
|
timer.stop();
|
|
|
|
if (!identifier.inDeclarationContext() &&
|
|
!_isNamedExpressionName(identifier)) {
|
|
expectedCount++;
|
|
suggestions = _sort(suggestions.toList());
|
|
var index = _indexOf(suggestions, identifier.name);
|
|
if (index < 0) {
|
|
missingCount++;
|
|
if (printMissing) {
|
|
var location = lineInfo.getLocation(offset);
|
|
output.writeln(
|
|
'Missing suggestion of "${identifier.name}" at '
|
|
'$path:${location.lineNumber}:${location.columnNumber}',
|
|
);
|
|
if (verbose) {
|
|
_printSuggestions(suggestions);
|
|
}
|
|
}
|
|
} else if (printQuality) {
|
|
if (index < indexCount.length) {
|
|
indexCount[index]++;
|
|
}
|
|
var filteredSuggestions = _filterBy(
|
|
suggestions,
|
|
identifier.name.substring(0, 1),
|
|
);
|
|
var filteredIndex = _indexOf(
|
|
filteredSuggestions,
|
|
identifier.name,
|
|
);
|
|
if (filteredIndex < filteredIndexCount.length) {
|
|
filteredIndexCount[filteredIndex]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (deleteBeforeCompletion) {
|
|
resourceProvider.removeOverlay(path);
|
|
}
|
|
}
|
|
}
|
|
output.writeln();
|
|
if (printMissing) {
|
|
output.writeln();
|
|
}
|
|
output.writeln('Found $identifierCount identifiers in $fileCount files');
|
|
if (expectedCount > 0) {
|
|
output.writeln(' $expectedCount were expected to code complete');
|
|
if (printQuality) {
|
|
var percent = (missingCount * 100 / expectedCount).round();
|
|
output.writeln(
|
|
' $percent% of which were missing suggestions '
|
|
'($missingCount)',
|
|
);
|
|
|
|
var foundCount = expectedCount - missingCount;
|
|
|
|
void printCount(int count) {
|
|
if (count < 10) {
|
|
output.write(' $count ');
|
|
} else if (count < 100) {
|
|
output.write(' $count ');
|
|
} else if (count < 1000) {
|
|
output.write(' $count ');
|
|
} else if (count < 10000) {
|
|
output.write(' $count ');
|
|
} else {
|
|
output.write(' $count ');
|
|
}
|
|
var percent = (count * 100 / foundCount).floor();
|
|
for (var j = 0; j < percent; j++) {
|
|
output.write('-');
|
|
}
|
|
output.writeln();
|
|
}
|
|
|
|
void printCounts(List<int> counts) {
|
|
var nearTopCount = 0;
|
|
for (var i = 0; i < counts.length; i++) {
|
|
var count = counts[i];
|
|
printCount(count);
|
|
nearTopCount += count;
|
|
}
|
|
printCount(foundCount - nearTopCount);
|
|
}
|
|
|
|
output.writeln();
|
|
output.writeln('By position in the list');
|
|
printCounts(indexCount);
|
|
output.writeln();
|
|
output.writeln('By position in the list (filtered by first character)');
|
|
printCounts(filteredIndexCount);
|
|
output.writeln();
|
|
}
|
|
}
|
|
if (timing && identifierCount > 0) {
|
|
var time = timer.elapsedMilliseconds;
|
|
var averageTime = (time / identifierCount).round();
|
|
output.writeln(
|
|
'completion took $time ms, '
|
|
'which is an average of $averageTime ms per completion',
|
|
);
|
|
}
|
|
}
|
|
|
|
List<CompletionSuggestionBuilder> _filterBy(
|
|
List<CompletionSuggestionBuilder> suggestions,
|
|
String pattern,
|
|
) {
|
|
return suggestions
|
|
.where((suggestion) => suggestion.completion.startsWith(pattern))
|
|
.toList();
|
|
}
|
|
|
|
/// Return a list containing information about the identifiers in the given
|
|
/// compilation [unit].
|
|
List<SimpleIdentifier> _identifiersIn(CompilationUnit unit) {
|
|
var visitor = IdentifierCollector();
|
|
unit.accept(visitor);
|
|
return visitor.identifiers;
|
|
}
|
|
|
|
/// If the given list of [suggestions] includes a suggestion for the given
|
|
/// [identifier], return the index of the suggestion. Otherwise, return `-1`.
|
|
int _indexOf(
|
|
List<CompletionSuggestionBuilder> suggestions,
|
|
String identifier,
|
|
) {
|
|
for (var i = 0; i < suggestions.length; i++) {
|
|
if (suggestions[i].completion == identifier) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// Return `true` if the given [identifier] is being used as the name of a
|
|
/// named expression.
|
|
bool _isNamedExpressionName(SimpleIdentifier identifier) {
|
|
var parent = identifier.parent;
|
|
return parent is NamedExpression && parent.name.label == identifier;
|
|
}
|
|
|
|
/// Print information about the given [suggestions].
|
|
void _printSuggestions(List<CompletionSuggestionBuilder> suggestions) {
|
|
if (suggestions.isEmpty) {
|
|
output.writeln(' No suggestions');
|
|
return;
|
|
}
|
|
output.writeln(' Suggestions:');
|
|
for (var suggestion in suggestions) {
|
|
output.writeln(' ${suggestion.completion}');
|
|
}
|
|
}
|
|
|
|
List<CompletionSuggestionBuilder> _sort(
|
|
List<CompletionSuggestionBuilder> suggestions,
|
|
) {
|
|
suggestions.sort((first, second) => second.relevance - first.relevance);
|
|
return suggestions;
|
|
}
|
|
}
|
|
|
|
/// A visitor that will collect simple identifiers in the AST being visited.
|
|
class IdentifierCollector extends RecursiveAstVisitor<void> {
|
|
/// The simple identifiers that were collected.
|
|
final List<SimpleIdentifier> identifiers = <SimpleIdentifier>[];
|
|
|
|
@override
|
|
void visitSimpleIdentifier(SimpleIdentifier node) {
|
|
identifiers.add(node);
|
|
}
|
|
}
|