| // 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. | |
| new({ | |
| 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()) { | |
| 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; | |
| } | |
| /// 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); | |
| } | |
| } |