| // 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 'dart:async'; |
| |
| 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.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/source/line_info.dart'; |
| import 'package:analyzer/src/generated/source.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 timing information. |
| */ |
| final bool timing; |
| |
| /** |
| * A flag indicating whether to use the CFE when running the tests. |
| */ |
| final bool useCFE; |
| |
| /** |
| * 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 timing, |
| bool useCFE, |
| bool verbose}) |
| : this.output = output ?? new NullStringSink(), |
| this.printMissing = printMissing ?? false, |
| this.timing = timing ?? false, |
| this.useCFE = useCFE ?? false, |
| this.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 { |
| OverlayResourceProvider resourceProvider = |
| new OverlayResourceProvider(PhysicalResourceProvider.INSTANCE); |
| AnalysisContextCollection collection = new AnalysisContextCollection( |
| includedPaths: <String>[analysisRoot], |
| resourceProvider: resourceProvider, |
| useCFE: useCFE); // ignore: deprecated_member_use |
| DartCompletionManager contributor = new DartCompletionManager(); |
| CompletionPerformance performance = new CompletionPerformance(); |
| int stamp = 1; |
| |
| int fileCount = 0; |
| int identifierCount = 0; |
| int missingCount = 0; |
| |
| // Consider getting individual timings so that we can also report the |
| // longest and shortest times, or even a distribution. |
| Stopwatch timer = new Stopwatch(); |
| |
| for (AnalysisContext context in collection.contexts) { |
| for (String path in context.contextRoot.analyzedFiles()) { |
| fileCount++; |
| output.write('.'); |
| ResolveResult result = |
| await context.currentSession.getResolvedAst(path); |
| String content = result.content; |
| LineInfo lineInfo = result.lineInfo; |
| List<SimpleIdentifier> identifiers = _identifiersIn(result.unit); |
| |
| for (SimpleIdentifier identifier in identifiers) { |
| identifierCount++; |
| int offset = identifier.offset; |
| if (deleteBeforeCompletion) { |
| String modifiedContent = content.substring(0, offset) + |
| content.substring(identifier.end); |
| resourceProvider.setOverlay(path, |
| content: modifiedContent, modificationStamp: stamp++); |
| result = await context.currentSession.getResolvedAst(path); |
| } |
| |
| timer.start(); |
| CompletionRequestImpl request = |
| new CompletionRequestImpl(result, offset, performance); |
| List<CompletionSuggestion> suggestions = |
| await contributor.computeSuggestions(request); |
| timer.stop(); |
| |
| if (!identifier.inDeclarationContext() && |
| !_isNamedExpressionName(identifier)) { |
| if (!_hasSuggestion(suggestions, identifier.name)) { |
| missingCount++; |
| if (printMissing) { |
| CharacterLocation location = lineInfo.getLocation(offset); |
| output.writeln('Missing suggestion of "${identifier.name}" at ' |
| '$path:${location.lineNumber}:${location.columnNumber}'); |
| if (verbose) { |
| _printSuggestions(suggestions); |
| } |
| } |
| } |
| } |
| } |
| if (deleteBeforeCompletion) { |
| resourceProvider.removeOverlay(path); |
| } |
| } |
| } |
| output.writeln(); |
| if (printMissing) { |
| output.writeln(); |
| } |
| if (identifierCount == 0) { |
| output.writeln('No identifiers found in $fileCount files'); |
| } else { |
| int percent = (missingCount * 100 / identifierCount).round(); |
| output.writeln('$percent% missing suggestions ' |
| '($missingCount of $identifierCount in $fileCount files)'); |
| } |
| if (timing && identifierCount > 0) { |
| int time = timer.elapsedMilliseconds; |
| int averageTime = (time / identifierCount).round(); |
| output.writeln('completion took $time ms, ' |
| 'which is an average of $averageTime ms per completion'); |
| } |
| } |
| |
| /** |
| * Return `true` if the given list of [suggestions] includes a suggestion for |
| * the given [identifier]. |
| */ |
| bool _hasSuggestion( |
| List<CompletionSuggestion> suggestions, String identifier) { |
| for (CompletionSuggestion suggestion in suggestions) { |
| if (suggestion.completion == identifier) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return a list containing information about the identifiers in the given |
| * compilation [unit]. |
| */ |
| List<SimpleIdentifier> _identifiersIn(CompilationUnit unit) { |
| IdentifierCollector visitor = new IdentifierCollector(); |
| unit.accept(visitor); |
| return visitor.identifiers; |
| } |
| |
| /** |
| * Return `true` if the given [identifier] is being used as the name of a |
| * named expression. |
| */ |
| bool _isNamedExpressionName(SimpleIdentifier identifier) { |
| AstNode parent = identifier.parent; |
| return parent is NamedExpression && parent.name == identifier; |
| } |
| |
| /** |
| * Print information about the given [suggestions]. |
| */ |
| void _printSuggestions(List<CompletionSuggestion> suggestions) { |
| if (suggestions.length == 0) { |
| output.writeln(' No suggestions'); |
| return; |
| } |
| output.writeln(' Suggestions:'); |
| for (CompletionSuggestion suggestion in suggestions) { |
| output.writeln(' ${suggestion.completion}'); |
| } |
| } |
| } |
| |
| /** |
| * 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 |
| visitSimpleIdentifier(SimpleIdentifier node) { |
| identifiers.add(node); |
| } |
| } |