| // 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_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 dartRequest = DartCompletionRequest.from( |
| resolvedUnit: result, |
| offset: offset, |
| ); |
| var suggestions = await statistics.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); |
| } |
| } |