afcfbbeba8
(Part of https://github.com/dart-lang/sdk/issues/63288) This change migrates the packages owned by the developer experience team to use the new constructor declaration syntax, described in https://github.com/dart-lang/language/blob/main/accepted/future-releases/primary-constructors/feature-specification.md#abbreviations-of-in-body-constructor-declarations. This change was performed in an automated fashion, by (a) bumping the packages' SDK constraints to `3.13.0-0`, (b) enabling the lints `unnecessary_type_name_in_constructor` and `unnecessary_const_in_enum_constructor`, (c) fixing the resulting lint failures using `dart fix`, and then (d) reformatting the affected files. To ease code review, I've reverted unrelated formatting changes. Since this change requires bumping SDK constaints to `3.13.0-0`, it was only performed on packages that are *not* published on pub. (Packages that *are* published on pub should remain on lower language versions until at least after the stable version of 3.13 is released, so that we don't block users on the stable channel from receiving updates to those packages.) Change-Id: Ibb4daebafd239da58251e838ea6a3f336a6a6964 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505046 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
328 lines
11 KiB
Dart
328 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/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(),
|
|
excludedPaths: root.excludedPaths.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%)');
|
|
}
|
|
}
|