// 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/diagnostic/diagnostic.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: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; CompletionMetricsComputer(this.rootPath, this.options); /// Applies an overlay in [filePath] at [expectedCompletion]. Future 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 computeInContext(ContextRoot root) async { // Create a new collection to avoid consuming large quantities of memory. var collection = AnalysisContextCollectionImpl( includedPaths: root.includedPaths.toList(), excludedPaths: root.excludedPaths.toList(), resourceProvider: provider, ); 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 computeMetrics() async { var collection = AnalysisContextCollectionImpl( includedPaths: [rootPath], resourceProvider: PhysicalResourceProvider.INSTANCE, ); for (var context in collection.contexts) { await computeInContext(context.contextRoot); } } /// Computes suggestions for [expectedCompletion] and computes metrics from /// the resulting suggestions. Future computeSuggestionsAndMetrics( ExpectedCompletion expectedCompletion, AnalysisContext context, ); /// Removes the overlay which has been applied to [filePath]. Future removeOverlay(AnalysisContext context, String filePath); /// Resolves all analyzed files within [context]. Future> resolveAnalyzedFiles({ required AnalysisContext context, }) async { var analyzedFileCount = context.contextRoot.analyzedFiles().length; logger.write('Resolving $analyzedFileCount files...\n'); var progress = ProgressBar(logger, analyzedFileCount); var results = []; 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 = getFirstErrorOrNull(result); 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); /// Given some [ResolvedUnitResult] returns the first error of high severity /// if such an error exists, `null` otherwise. static Diagnostic? getFirstErrorOrNull( ResolvedUnitResult resolvedUnitResult, ) { for (var error in resolvedUnitResult.errors) { if (error.severity == Severity.error) { return error; } } return null; } /// 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 OVERLAY = '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 PREFIX_LENGTH = 'prefix-length'; /// A flag that causes information to be printed about the completion requests /// that were the slowest to return suggestions. static const String PRINT_SLOWEST_RESULTS = '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; CompletionMetricsOptions(ArgResults results) : overlay = OverlayMode.parseFlag(results[OVERLAY] as String), prefixLength = int.parse(results[PREFIX_LENGTH] as String), printSlowestResults = results[PRINT_SLOWEST_RESULTS] as bool; } 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; const OverlayMode(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; ProgressBar(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%)'); } }