| // Copyright (c) 2022, 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:io' show stdout; | |
| import 'dart:math' as math; | |
| import 'package:analyzer/dart/analysis/analysis_context.dart'; | |
| import 'package:analyzer/dart/analysis/context_root.dart'; | |
| import 'package:analyzer/dart/analysis/results.dart'; | |
| import 'package:analyzer/file_system/overlay_file_system.dart'; | |
| import 'package:analyzer/file_system/physical_file_system.dart'; | |
| import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; | |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; | |
| import 'package:analyzer/src/utilities/extensions/diagnostic.dart'; | |
| import 'package:args/args.dart'; | |
| import 'package:cli_util/cli_logging.dart'; | |
| import 'visitors.dart'; | |
| final logger = Logger.standard(ansi: Ansi(Ansi.terminalSupportsAnsi)); | |
| /// This is the main metrics computer class for code completions. After the | |
| /// object is constructed, [computeCompletionMetrics] is executed to do analysis | |
| /// and print a summary of the metrics gathered from the completion tests. | |
| abstract class CompletionMetricsComputer { | |
| final String rootPath; | |
| final CompletionMetricsOptions options; | |
| late ResolvedUnitResult resolvedUnitResult; | |
| final OverlayResourceProvider provider = OverlayResourceProvider( | |
| PhysicalResourceProvider.INSTANCE, | |
| ); | |
| int overlayModificationStamp = 0; | |
| new(this.rootPath, this.options); | |
| /// Applies an overlay in [filePath] at [expectedCompletion]. | |
| Future<void> applyOverlay( | |
| AnalysisContext context, | |
| String filePath, | |
| ExpectedCompletion expectedCompletion, | |
| ); | |
| /// Compute the metrics for the files in the context [root], creating a | |
| /// separate context collection to prevent accumulating memory. The metrics | |
| /// should be captured in the [collector]. | |
| Future<void> computeInContext(ContextRoot root) async { | |
| // Create a new collection to avoid consuming large quantities of memory. | |
| var collection = AnalysisContextCollectionImpl( | |
| includedPaths: root.includedPaths.toList(), | |
| resourceProvider: provider, | |
| withFineDependencies: true, | |
| ); | |
| var context = collection.contexts[0]; | |
| setupForResolution(context); | |
| logger.write('Computing completions at root: ${root.root.path}\n'); | |
| var results = await resolveAnalyzedFiles(context: context); | |
| logger.write('Analyzing completion suggestions...\n'); | |
| var progress = ProgressBar(logger, results.length); | |
| for (var result in results) { | |
| resolvedUnitResult = result; | |
| var filePath = result.path; | |
| // Use the ExpectedCompletionsVisitor to compute the set of expected | |
| // completions for this CompilationUnit. | |
| var visitor = ExpectedCompletionsVisitor( | |
| result, | |
| caretOffset: options.prefixLength, | |
| ); | |
| resolvedUnitResult.unit.accept(visitor); | |
| for (var expectedCompletion in visitor.expectedCompletions) { | |
| await applyOverlay(context, filePath, expectedCompletion); | |
| await computeSuggestionsAndMetrics(expectedCompletion, context); | |
| await removeOverlay(context, filePath); | |
| } | |
| progress.tick(); | |
| } | |
| progress.complete(); | |
| } | |
| Future<void> computeMetrics() async { | |
| var collection = AnalysisContextCollectionImpl( | |
| includedPaths: [rootPath], | |
| resourceProvider: PhysicalResourceProvider.INSTANCE, | |
| withFineDependencies: true, | |
| ); | |
| for (var context in collection.contexts) { | |
| await computeInContext(context.contextRoot); | |
| } | |
| } | |
| /// Computes suggestions for [expectedCompletion] and computes metrics from | |
| /// the resulting suggestions. | |
| Future<void> computeSuggestionsAndMetrics( | |
| ExpectedCompletion expectedCompletion, | |
| AnalysisContext context, | |
| ); | |
| /// Removes the overlay which has been applied to [filePath]. | |
| Future<void> removeOverlay(AnalysisContext context, String filePath); | |
| /// Resolves all analyzed files within [context]. | |
| Future<List<ResolvedUnitResult>> resolveAnalyzedFiles({ | |
| required AnalysisContext context, | |
| }) async { | |
| var analyzedFileCount = context.contextRoot.analyzedFiles().length; | |
| logger.write('Resolving $analyzedFileCount files...\n'); | |
| var progress = ProgressBar(logger, analyzedFileCount); | |
| var results = <ResolvedUnitResult>[]; | |
| var pathContext = context.contextRoot.resourceProvider.pathContext; | |
| for (var filePath in context.contextRoot.analyzedFiles()) { | |
| if (file_paths.isGenerated(filePath) || | |
| filePath.endsWith('animated_icons.dart') || | |
| filePath.endsWith('animated_icons_data.dart')) { | |
| continue; | |
| } | |
| if (file_paths.isDart(pathContext, filePath)) { | |
| try { | |
| var result = await context.currentSession.getResolvedUnit( | |
| filePath, | |
| ) as ResolvedUnitResult; | |
| var analysisError = result.diagnostics.errors.firstOrNull; | |
| if (analysisError != null) { | |
| progress.clear(); | |
| print('File $filePath skipped due to errors such as:'); | |
| print(' ${analysisError.toString()}'); | |
| print(''); | |
| continue; | |
| } else { | |
| results.add(result); | |
| } | |
| } catch (exception, stackTrace) { | |
| progress.clear(); | |
| print('Exception caught analyzing: $filePath'); | |
| print(exception.toString()); | |
| print(stackTrace); | |
| } | |
| } | |
| progress.tick(); | |
| } | |
| progress.complete(); | |
| return results; | |
| } | |
| /// Performs setup tasks with [context] before resolution. | |
| void setupForResolution(AnalysisContext context); | |
| /// Gets overlay content for [content], applying a change at | |
| /// [expectedCompletion] with [prefixLength], according to [overlay], one of | |
| /// the [CompletionMetricsOptions]. | |
| static String getOverlayContent( | |
| String content, | |
| ExpectedCompletion expectedCompletion, | |
| OverlayMode overlay, | |
| int prefixLength, | |
| ) { | |
| assert(content.isNotEmpty); | |
| var offset = expectedCompletion.offset; | |
| var length = expectedCompletion.syntacticEntity.length; | |
| assert(offset >= 0); | |
| assert(length > 0); | |
| var tokenEndOffset = offset + length; | |
| if (length >= prefixLength) { | |
| // Rather than removing the whole token, remove the characters after | |
| // the given prefix length. | |
| offset += prefixLength; | |
| } | |
| if (overlay == OverlayMode.removeToken) { | |
| return content.substring(0, offset) + content.substring(tokenEndOffset); | |
| } else if (overlay == OverlayMode.removeRestOfFile) { | |
| return content.substring(0, offset); | |
| } else { | |
| throw ArgumentError.value(overlay, 'overlay'); | |
| } | |
| } | |
| } | |
| /// The options specified on the command-line. | |
| class CompletionMetricsOptions { | |
| /// An option to control whether and how overlays should be produced. | |
| static const String overlayOption = 'overlay'; | |
| /// An option controlling how long of a prefix should be used. | |
| /// | |
| /// This affects the offset of the completion request, and how much content is | |
| /// removed in each of the overlay modes. | |
| static const String prefixLengthOption = 'prefix-length'; | |
| /// A flag that causes information to be printed about the completion requests | |
| /// that were the slowest to return suggestions. | |
| static const String printSlowestResultsFlag = 'print-slowest-results'; | |
| /// The overlay mode that should be used. | |
| final OverlayMode overlay; | |
| final int prefixLength; | |
| /// A flag indicating whether information should be printed about the | |
| /// completion requests that were the slowest to return suggestions. | |
| final bool printSlowestResults; | |
| new(ArgResults results) | |
| : overlay = OverlayMode.parseFlag(results.option(overlayOption)!), | |
| prefixLength = int.parse(results.option(prefixLengthOption)!), | |
| printSlowestResults = results.flag(printSlowestResultsFlag); | |
| } | |
| enum OverlayMode { | |
| /// A mode indicating that no overlays should be produced. | |
| none('none'), | |
| /// A mode indicating that everything from the completion offset to the end of | |
| /// the file should be removed. | |
| removeRestOfFile('remove-rest-of-file'), | |
| /// A mode indicating that the token whose offset is the same as the | |
| /// completion offset should be removed. | |
| removeToken('remove-token'); | |
| final String flag; | |
| new(this.flag); | |
| static OverlayMode parseFlag(String flag) { | |
| for (var mode in values) { | |
| if (flag == mode.flag) { | |
| return mode; | |
| } | |
| } | |
| throw ArgumentError.value(flag, 'overlay'); | |
| } | |
| } | |
| /// A facility for drawing a progress bar in the terminal. | |
| /// | |
| /// The bar is instantiated with the total number of "ticks" to be completed, | |
| /// and progress is made by calling [tick]. The bar is drawn across one entire | |
| /// line, like so: | |
| /// | |
| /// [---------- ] | |
| /// | |
| /// The hyphens represent completed progress, and the whitespace represents | |
| /// remaining progress. | |
| /// | |
| /// If there is no terminal, the progress bar will not be drawn. | |
| class ProgressBar { | |
| /// Whether the progress bar should be drawn. | |
| late bool _shouldDrawProgress; | |
| /// The width of the terminal, in terms of characters. | |
| late int _width; | |
| final Logger _logger; | |
| /// The inner width of the terminal, in terms of characters. | |
| /// | |
| /// This represents the number of characters available for drawing progress. | |
| late int _innerWidth; | |
| final int _totalTickCount; | |
| int _tickCount = 0; | |
| new(this._logger, this._totalTickCount) { | |
| if (!stdout.hasTerminal) { | |
| _shouldDrawProgress = false; | |
| } else { | |
| _shouldDrawProgress = true; | |
| _width = stdout.terminalColumns; | |
| // Inclusion of the percent indicator assumes a terminal width of at least | |
| // 12 (2 brackets + 1 space + 2 parenthesis characters + 3 digits + | |
| // 1 period + 2 digits + 1 '%' character). | |
| _innerWidth = stdout.terminalColumns - 12; | |
| _logger.write('[${' ' * _innerWidth}]'); | |
| } | |
| } | |
| /// Clears the progress bar from the terminal, allowing other logging to be | |
| /// printed. | |
| void clear() { | |
| if (!_shouldDrawProgress) { | |
| return; | |
| } | |
| _logger.write('\r${' ' * _width}\r'); | |
| } | |
| /// Draws the progress bar as complete, and print two newlines. | |
| void complete() { | |
| if (!_shouldDrawProgress) { | |
| return; | |
| } | |
| _logger.write('\r[${'-' * _innerWidth}]\n\n'); | |
| } | |
| /// Progresses the bar by one tick. | |
| void tick() { | |
| if (!_shouldDrawProgress) { | |
| return; | |
| } | |
| _tickCount++; | |
| var fractionComplete = math.max( | |
| 0, | |
| _tickCount * _innerWidth ~/ _totalTickCount - 1, | |
| ); | |
| // The inner space consists of hyphens, one spinner character, spaces, and a | |
| // percentage (8 characters). | |
| var hyphens = '-' * fractionComplete; | |
| var trailingSpace = ' ' * (_innerWidth - fractionComplete - 1); | |
| var spinner = AnsiProgress.kAnimationItems[_tickCount % 4]; | |
| var pctComplete = (_tickCount * 100 / _totalTickCount).toStringAsFixed(2); | |
| _logger.write('\r[$hyphens$spinner$trailingSpace] ($pctComplete%)'); | |
| } | |
| } |