blob: 34056c40a6289fd32b98114bac82de3c9901780b [file] [log] [blame]
// 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:analysis_server/src/services/completion/dart/documentation_cache.dart';
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/error/error.dart' as err;
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/dartdoc/dartdoc_directive_info.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.
final 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 documentationCache = DocumentationCache(DartdocDirectiveInfo());
var results = await resolveAnalyzedFiles(
context: context,
documentationCache: documentationCache,
);
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.
final 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,
documentationCache,
);
await removeOverlay(filePath);
}
progress.tick();
}
progress.complete();
}
Future<void> computeMetrics() async {
final collection = AnalysisContextCollectionImpl(
includedPaths: [rootPath],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
for (final 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,
DocumentationCache documentationCache,
);
/// Removes the overlay which has been applied to [filePath].
Future<void> removeOverlay(String filePath);
/// Resolves all analyzed files within [context].
Future<List<ResolvedUnitResult>> resolveAnalyzedFiles({
required AnalysisContext context,
required DocumentationCache documentationCache,
}) async {
final analyzedFileCount = context.contextRoot.analyzedFiles().length;
logger.write('Resolving $analyzedFileCount files...\n');
final progress = ProgressBar(logger, analyzedFileCount);
final results = <ResolvedUnitResult>[];
final pathContext = context.contextRoot.resourceProvider.pathContext;
for (final filePath in context.contextRoot.analyzedFiles()) {
if (file_paths.isDart(pathContext, filePath)) {
try {
final result = await context.currentSession.getResolvedUnit(filePath)
as ResolvedUnitResult;
final 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);
documentationCache.cacheFromResult(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 err.AnalysisError? getFirstErrorOrNull(
ResolvedUnitResult resolvedUnitResult) {
for (final 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;
final 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 (final 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++;
final fractionComplete =
math.max(0, _tickCount * _innerWidth ~/ _totalTickCount - 1);
// The inner space consists of hyphens, one spinner character, spaces, and a
// percentage (8 characters).
final hyphens = '-' * fractionComplete;
final trailingSpace = ' ' * (_innerWidth - fractionComplete - 1);
final spinner = AnsiProgress.kAnimationItems[_tickCount % 4];
final pctComplete = (_tickCount * 100 / _totalTickCount).toStringAsFixed(2);
_logger.write('\r[$hyphens$spinner$trailingSpace] ($pctComplete%)');
}
}