blob: dcb36a915da94b3d99e4e54858f42262f5b8b721 [file] [log] [blame]
// 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/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.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_plugin/protocol/protocol_common.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();
var statistics = CompletionPerformance();
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 request = CompletionRequestImpl(result, offset, statistics);
var dartRequest = await DartCompletionRequestImpl.from(request);
var suggestions = await request.performance.runRequestOperation(
(performance) async {
return await contributor.computeSuggestions(
dartRequest,
performance,
);
},
);
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<CompletionSuggestion> _filterBy(
List<CompletionSuggestion> 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<CompletionSuggestion> 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<CompletionSuggestion> suggestions) {
if (suggestions.isEmpty) {
output.writeln(' No suggestions');
return;
}
output.writeln(' Suggestions:');
for (var suggestion in suggestions) {
output.writeln(' ${suggestion.completion}');
}
}
List<CompletionSuggestion> _sort(List<CompletionSuggestion> 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);
}
}