eb03a1673d
Work towards https://github.com/dart-lang/sdk/issues/60635 Change-Id: I0107a65580bc41d769c2a384319225b256df2fbe Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426420 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com>
339 lines
11 KiB
Dart
339 lines
11 KiB
Dart
// 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<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(),
|
|
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<void> 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<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 = 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%)');
|
|
}
|
|
}
|