Version 2.14.0-374.0.dev
Merge commit 'af50a684d12a69fc90a06e394a37adfb957abaa9' into 'dev'
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index e6b028b..3896905 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -40,6 +40,7 @@
import 'package:analysis_server/src/server/features.dart';
import 'package:analysis_server/src/server/sdk_configuration.dart';
import 'package:analysis_server/src/services/flutter/widget_descriptions.dart';
+import 'package:analysis_server/src/utilities/process.dart';
import 'package:analysis_server/src/utilities/request_statistics.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
@@ -130,6 +131,7 @@
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
InstrumentationService instrumentationService, {
http.Client? httpClient,
+ ProcessRunner? processRunner,
RequestStatisticsHelper? requestStatistics,
DiagnosticServer? diagnosticServer,
this.detachableFileSystemManager,
@@ -143,6 +145,7 @@
baseResourceProvider,
instrumentationService,
httpClient,
+ processRunner,
NotificationManager(channel, baseResourceProvider.pathContext),
requestStatistics: requestStatistics,
enableBazelWatcher: enableBazelWatcher,
@@ -429,9 +432,11 @@
bool isPubspec(String filePath) =>
file_paths.isPubspecYaml(resourceProvider.pathContext, filePath);
- // When a pubspec is opened, trigger package name caching for completion.
- if (!pubPackageService.isRunning && files.any(isPubspec)) {
- pubPackageService.beginPackageNamePreload();
+ // When pubspecs are opened, trigger pre-loading of pub package names and
+ // versions.
+ final pubspecs = files.where(isPubspec).toList();
+ if (pubspecs.isNotEmpty) {
+ pubPackageService.beginCachePreloads(pubspecs);
}
priorityFiles.clear();
@@ -688,6 +693,18 @@
}
@override
+ void pubspecChanged(String pubspecPath) {
+ analysisServer.pubPackageService.fetchPackageVersionsViaPubOutdated(
+ pubspecPath,
+ pubspecWasModified: true);
+ }
+
+ @override
+ void pubspecRemoved(String pubspecPath) {
+ analysisServer.pubPackageService.flushPackageCaches(pubspecPath);
+ }
+
+ @override
void recordAnalysisErrors(String path, List<AnalysisError> errors) {
filesToFlush.add(path);
_notificationManager.recordAnalysisErrors(
diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
index 6a351b1..4b28f5c 100644
--- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart
+++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
@@ -19,12 +19,14 @@
import 'package:analysis_server/src/services/completion/dart/extension_cache.dart';
import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analysis_server/src/services/search/element_visitors.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
import 'package:analysis_server/src/utilities/file_string_sink.dart';
import 'package:analysis_server/src/utilities/null_string_sink.dart';
+import 'package:analysis_server/src/utilities/process.dart';
import 'package:analysis_server/src/utilities/request_statistics.dart';
import 'package:analysis_server/src/utilities/tee_string_sink.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
@@ -152,14 +154,26 @@
ResourceProvider baseResourceProvider,
this.instrumentationService,
http.Client? httpClient,
+ ProcessRunner? processRunner,
this.notificationManager, {
this.requestStatistics,
bool enableBazelWatcher = false,
}) : resourceProvider = OverlayResourceProvider(baseResourceProvider),
pubApi = PubApi(instrumentationService, httpClient,
Platform.environment['PUB_HOSTED_URL']) {
- pubPackageService =
- PubPackageService(instrumentationService, baseResourceProvider, pubApi);
+ // We can only spawn processes (eg. to run pub commands) when backed by
+ // a real file system, otherwise we may try to run commands in folders that
+ // don't really exist. If processRunner was supplied, it's likely a mock
+ // from a test in which case the pub command should still be created.
+ if (baseResourceProvider is PhysicalResourceProvider) {
+ processRunner ??= ProcessRunner();
+ }
+ final pubCommand = processRunner != null
+ ? PubCommand(instrumentationService, processRunner)
+ : null;
+
+ pubPackageService = PubPackageService(
+ instrumentationService, baseResourceProvider, pubApi, pubCommand);
performance = performanceDuringStartup;
pluginManager = PluginManager(
diff --git a/pkg/analysis_server/lib/src/context_manager.dart b/pkg/analysis_server/lib/src/context_manager.dart
index 721f37c..28509f0 100644
--- a/pkg/analysis_server/lib/src/context_manager.dart
+++ b/pkg/analysis_server/lib/src/context_manager.dart
@@ -122,6 +122,12 @@
/// TODO(scheglov) Just pass results in here?
void listenAnalysisDriver(AnalysisDriver driver);
+ /// The `pubspec.yaml` at [path] was added/modified.
+ void pubspecChanged(String path);
+
+ /// The `pubspec.yaml` at [path] was removed.
+ void pubspecRemoved(String path);
+
/// Record error information for the file with the given [path].
void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors);
}
@@ -566,12 +572,21 @@
_instrumentationService.logWatchEvent('<unknown>', path, type.toString());
+ final isPubpsec = file_paths.isPubspecYaml(pathContext, path);
if (file_paths.isAnalysisOptionsYaml(pathContext, path) ||
file_paths.isDotPackages(pathContext, path) ||
file_paths.isPackageConfigJson(pathContext, path) ||
- file_paths.isPubspecYaml(pathContext, path) ||
+ isPubpsec ||
false) {
_createAnalysisContexts();
+
+ if (isPubpsec) {
+ if (type == ChangeType.REMOVE) {
+ callbacks.pubspecRemoved(path);
+ } else {
+ callbacks.pubspecChanged(path);
+ }
+ }
return;
}
@@ -727,6 +742,12 @@
void listenAnalysisDriver(AnalysisDriver driver) {}
@override
+ void pubspecChanged(String pubspecPath) {}
+
+ @override
+ void pubspecRemoved(String pubspecPath) {}
+
+ @override
void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors) {}
}
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index cac5665..2dcd219 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -35,6 +35,7 @@
import 'package:analysis_server/src/services/completion/completion_performance.dart'
show CompletionPerformance;
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
+import 'package:analysis_server/src/utilities/process.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/error/error.dart';
@@ -126,6 +127,7 @@
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
InstrumentationService instrumentationService, {
http.Client? httpClient,
+ ProcessRunner? processRunner,
DiagnosticServer? diagnosticServer,
// Disable to avoid using this in unit tests.
bool enableBazelWatcher = false,
@@ -137,6 +139,7 @@
baseResourceProvider,
instrumentationService,
httpClient,
+ processRunner,
LspNotificationManager(channel, baseResourceProvider.pathContext),
enableBazelWatcher: enableBazelWatcher,
) {
@@ -184,10 +187,10 @@
RefactoringWorkspace(driverMap.values, searchEngine);
void addPriorityFile(String filePath) {
- // When a pubspec is opened, trigger package name caching for completion.
- if (!pubPackageService.isRunning &&
- file_paths.isPubspecYaml(resourceProvider.pathContext, filePath)) {
- pubPackageService.beginPackageNamePreload();
+ // When pubspecs are opened, trigger pre-loading of pub package names and
+ // versions.
+ if (file_paths.isPubspecYaml(resourceProvider.pathContext, filePath)) {
+ pubPackageService.beginCachePreloads([filePath]);
}
final didAdd = priorityFiles.add(filePath);
@@ -852,6 +855,18 @@
}
@override
+ void pubspecChanged(String pubspecPath) {
+ analysisServer.pubPackageService.fetchPackageVersionsViaPubOutdated(
+ pubspecPath,
+ pubspecWasModified: true);
+ }
+
+ @override
+ void pubspecRemoved(String pubspecPath) {
+ analysisServer.pubPackageService.flushPackageCaches(pubspecPath);
+ }
+
+ @override
void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors) {
final errorsToSend = errors.where(_shouldSendError).toList();
filesToFlush.add(path);
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 03be64d..da9bb2a 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -49,6 +49,15 @@
],
};
+/// Pattern for docComplete text on completion items that can be upgraded to
+/// the "detail" field so that it can be shown more prominently by clients.
+///
+/// This is typically used for labels like _latest compatible_ and _latest_ in
+/// the pubspec version items. These go into docComplete so that they appear
+/// reasonably for non-LSP clients where there is no equivalent of the detail
+/// field.
+final _upgradableDocCompletePattern = RegExp(r'^_([\w ]{0,20})_$');
+
lsp.Either2<String, lsp.MarkupContent> asStringOrMarkupContent(
Set<lsp.MarkupKind>? preferredFormats, String content) {
return preferredFormats == null
@@ -967,7 +976,21 @@
final insertText = insertTextInfo.first;
final insertTextFormat = insertTextInfo.last;
final isMultilineCompletion = insertText.contains('\n');
- final cleanedDoc = cleanDartdoc(suggestion.docComplete);
+
+ var cleanedDoc = cleanDartdoc(suggestion.docComplete);
+ var detail = getCompletionDetail(suggestion, completionKind,
+ supportsCompletionDeprecatedFlag || supportsDeprecatedTag);
+
+ // To improve the display of some items (like pubspec version numbers),
+ // short labels in the format `_foo_` in docComplete are "upgraded" to the
+ // detail field.
+ final labelMatch = cleanedDoc != null
+ ? _upgradableDocCompletePattern.firstMatch(cleanedDoc)
+ : null;
+ if (labelMatch != null) {
+ cleanedDoc = null;
+ detail = labelMatch.group(1);
+ }
// Because we potentially send thousands of these items, we should minimise
// the generated JSON as much as possible - for example using nulls in place
@@ -982,8 +1005,7 @@
commitCharacters:
includeCommitCharacters ? dartCompletionCommitCharacters : null,
data: resolutionData,
- detail: getCompletionDetail(suggestion, completionKind,
- supportsCompletionDeprecatedFlag || supportsDeprecatedTag),
+ detail: detail,
documentation: cleanedDoc != null
? asStringOrMarkupContent(formats, cleanedDoc)
: null,
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
index 960346f..5d7f681 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
@@ -176,9 +176,11 @@
const Producer();
/// A utility method used to create a suggestion for the [identifier].
- CompletionSuggestion identifier(String identifier, {int relevance = 1000}) =>
+ CompletionSuggestion identifier(String identifier,
+ {int relevance = 1000, String? docComplete}) =>
CompletionSuggestion(CompletionSuggestionKind.IDENTIFIER, relevance,
- identifier, identifier.length, 0, false, false);
+ identifier, identifier.length, 0, false, false,
+ docComplete: docComplete);
/// A utility method used to create a suggestion for the package [packageName].
CompletionSuggestion packageName(String packageName,
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
index 724bb6f..a34d791 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
@@ -37,18 +37,25 @@
@override
Iterable<CompletionSuggestion> suggestions(
YamlCompletionRequest request) sync* {
- // TOOD(dantup): Consider supporting async completion requests so this
- // could call packageDetails() (with a short timeout, and pub retries
- // disabled). A user that explicitly invokes completion in the location
- // of a version may be prepared to wait a short period for a web request
- // to get completion versions (this is also the only way for non-LSP
- // clients to get them, since there are no resolve calls).
+ final versions = request.pubPackageService
+ ?.cachedPubOutdatedVersions(request.filePath, package);
+ final resolvable = versions?.resolvableVersion;
+ var latest = versions?.latestVersion;
+
+ // If we didn't get a latest version from the "pub outdated" results, we can
+ // use the result from the Pub API if we've called it (this will usually
+ // only be the case for LSP where a resolve() call was sent).
//
- // Supporting this will require making the completion async further up.
- final details = request.pubPackageService?.cachedPackageDetails(package);
- final version = details?.latestVersion;
- if (version != null) {
- yield identifier('^$version');
+ // This allows us (in some cases) to still show version numbers even if the
+ // package was newly added to pubspec and not saved, so not yet in the
+ // "pub outdated" results.
+ latest ??= request.pubPackageService?.cachedPubApiLatestVersion(package);
+
+ if (resolvable != null && resolvable != latest) {
+ yield identifier('^$resolvable', docComplete: '_latest compatible_');
+ }
+ if (latest != null) {
+ yield identifier('^$latest', docComplete: '_latest_');
}
}
}
diff --git a/pkg/analysis_server/lib/src/services/pub/pub_command.dart b/pkg/analysis_server/lib/src/services/pub/pub_command.dart
new file mode 100644
index 0000000..a649d65
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/pub/pub_command.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2021, 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:analysis_server/src/utilities/process.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:path/path.dart' as path;
+
+/// A class for interacting with the `pub` command.
+///
+/// `pub` commands will be queued and not run concurrently.
+class PubCommand {
+ static const String _pubEnvironmentKey = 'PUB_ENVIRONMENT';
+ final InstrumentationService _instrumentationService;
+ late final ProcessRunner _processRunner;
+ late final String _pubPath;
+ late final String _pubEnvironmentValue;
+
+ /// Tracks the last queued command to avoid overlapping because pub does not
+ /// do its own locking when accessing the cache.
+ ///
+ /// https://github.com/dart-lang/pub/issues/1178
+ ///
+ /// This does not prevent running concurrently with commands spawned by other
+ /// tools (such as the IDE).
+ var _lastQueuedCommand = Future<void>.value();
+
+ PubCommand(this._instrumentationService, this._processRunner) {
+ _pubPath = path.join(
+ path.dirname(Platform.resolvedExecutable),
+ Platform.isWindows ? 'pub.bat' : 'pub',
+ );
+
+ // When calling the `pub` command, we must add an identifier to the
+ // PUB_ENVIRONMENT environment variable (joined with colons).
+ const _pubEnvString = 'analysis_server.pub_api';
+ final existingPubEnv = Platform.environment[_pubEnvironmentKey];
+ _pubEnvironmentValue = [
+ if (existingPubEnv?.isNotEmpty ?? false) existingPubEnv,
+ _pubEnvString,
+ ].join(':');
+ }
+
+ /// Runs `pub outdated --show-all` and returns the results.
+ ///
+ /// If any error occurs executing the command, returns an empty list.
+ Future<List<PubOutdatedPackageDetails>> outdatedVersions(
+ String pubspecPath) async {
+ final packageDirectory = path.dirname(pubspecPath);
+ final result = await _runPubJsonCommand(
+ ['outdated', '--show-all', '--json'],
+ workingDirectory: packageDirectory);
+
+ if (result == null) {
+ return [];
+ }
+
+ final packages =
+ (result['packages'] as List<dynamic>?)?.cast<Map<String, Object?>>();
+ if (packages == null) {
+ return [];
+ }
+
+ return packages
+ .map(
+ (json) => PubOutdatedPackageDetails(
+ json['package'] as String,
+ currentVersion: _version(json, 'current'),
+ latestVersion: _version(json, 'latest'),
+ resolvableVersion: _version(json, 'resolvable'),
+ upgradableVersion: _version(json, 'upgradable'),
+ ),
+ )
+ .toList();
+ }
+
+ /// Runs a pub command and decodes JSON from `stdout`.
+ ///
+ /// Returns null if:
+ /// - exit code is non-zero
+ /// - returned text cannot be decoded as JSON
+ Future<Map<String, Object?>?> _runPubJsonCommand(List<String> args,
+ {required String workingDirectory}) async {
+ // Atomically replace the lastQueuedCommand future with our own to ensure
+ // only one command waits on any previous commands future.
+ final completer = Completer<void>();
+ final lastCommand = _lastQueuedCommand;
+ _lastQueuedCommand = completer.future;
+ // And wait for that previous command to finish.
+ await lastCommand.catchError((_) {});
+
+ try {
+ final command = [_pubPath, ...args];
+
+ _instrumentationService.logInfo('Running pub command $command');
+ final result = await _processRunner.run(_pubPath, args,
+ workingDirectory: workingDirectory,
+ environment: {_pubEnvironmentKey: _pubEnvironmentValue});
+
+ if (result.exitCode != 0) {
+ _instrumentationService.logError(
+ 'pub command returned ${result.exitCode} exit code: ${result.stderr}.');
+ return null;
+ }
+
+ try {
+ final results = jsonDecode(result.stdout);
+ _instrumentationService.logInfo('pub command completed successfully');
+ return results;
+ } catch (e) {
+ _instrumentationService
+ .logError('pub command returned invalid JSON: $e.');
+ return null;
+ }
+ } catch (e) {
+ _instrumentationService.logError('pub command failed to run: $e.');
+ return null;
+ } finally {
+ completer.complete();
+ }
+ }
+
+ String? _version(Map<String, Object?> json, String type) {
+ final versionType = json[type] as Map<String, Object?>?;
+ final version =
+ versionType != null ? versionType['version'] as String? : null;
+ return version;
+ }
+}
+
+class PubOutdatedPackageDetails {
+ final String packageName;
+ final String? currentVersion;
+ final String? latestVersion;
+ final String? resolvableVersion;
+ final String? upgradableVersion;
+
+ PubOutdatedPackageDetails(
+ this.packageName, {
+ required this.currentVersion,
+ required this.latestVersion,
+ required this.resolvableVersion,
+ required this.upgradableVersion,
+ });
+}
diff --git a/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart b/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
index d0990db..4b6ef3a8 100644
--- a/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
+++ b/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
@@ -6,10 +6,11 @@
import 'dart:convert';
import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/instrumentation/service.dart';
import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
/// Information about Pub packages that can be converted to/from JSON and
/// cached to disk.
@@ -130,16 +131,26 @@
/// A service for providing Pub package information.
///
-/// Uses a [PubApi] to communicate with Pub and caches to disk using [cacheResourceProvider].
+/// Uses a [PubApi] to communicate with the Pub API and a [PubCommand] to
+/// interact with the local `pub` command.
+///
+/// Expensive results are cached to disk using [resourceProvider].
class PubPackageService {
final InstrumentationService _instrumentationService;
final PubApi _api;
+
+ /// A wrapper over the "pub" command line too.
+ ///
+ /// This can be null when not running on a real file system because it may
+ /// try to interact with folders that don't really exist.
+ final PubCommand? _command;
+
Timer? _nextPackageNameListRequestTimer;
Timer? _nextWriteDiskCacheTimer;
- /// [ResourceProvider] used for caching. This should generally be a
- /// [PhysicalResourceProvider] outside of tests.
- final ResourceProvider cacheResourceProvider;
+ /// [ResourceProvider] used for accessing the disk for caches and checking
+ /// project types. This will be a [PhysicalResourceProvider] outside of tests.
+ final ResourceProvider resourceProvider;
/// The current cache of package information. Initially `null`, but
/// overwritten after first read of cache from disk or fetch from the API.
@@ -148,25 +159,50 @@
int _packageDetailsRequestsInFlight = 0;
- PubPackageService(
- this._instrumentationService, this.cacheResourceProvider, this._api);
+ /// A cache of version numbers from running the "pub outdated" command used
+ /// for completion in pubspec.yaml.
+ final _pubspecPackageVersions =
+ <String, Map<String, PubOutdatedPackageDetails>>{};
- /// Gets the last set of package results or an empty List if no results.
+ PubPackageService(this._instrumentationService, this.resourceProvider,
+ this._api, this._command);
+
+ /// Gets the last set of package results from the Pub API or an empty List if
+ /// no results.
+ ///
+ /// This data is used for completion of package names in pubspec.yaml
+ /// and for clients that support lazy resolution of completion items may also
+ /// include their descriptions and/or version numbers.
List<PubPackage> get cachedPackages =>
packageCache?.packages.values.toList() ?? [];
- bool get isRunning => _nextPackageNameListRequestTimer != null;
+ @visibleForTesting
+ bool get isPackageNamesTimerRunning =>
+ _nextPackageNameListRequestTimer != null;
@visibleForTesting
File get packageCacheFile {
- final cacheFolder = cacheResourceProvider
+ final cacheFolder = resourceProvider
.getStateLocation('.pub-package-details-cache')!
..create();
return cacheFolder.getChildAssumingFile('packages.json');
}
- /// Begin a request to pre-load the package name list.
+ /// Begins preloading caches for package names and pub versions.
+ void beginCachePreloads(List<String> pubspecs) {
+ beginPackageNamePreload();
+ for (final pubspec in pubspecs) {
+ fetchPackageVersionsViaPubOutdated(pubspec, pubspecWasModified: false);
+ }
+ }
+
+ /// Begin a timer to pre-load and update the package name list if one has not
+ /// already been started.
void beginPackageNamePreload() {
+ if (isPackageNamesTimerRunning) {
+ return;
+ }
+
// If first time, try to read from disk.
var cache = packageCache;
if (cache == null) {
@@ -179,11 +215,69 @@
Timer(cache.cacheTimeRemaining, _fetchFromServer);
}
- /// Gets the cached package details for package [packageName].
+ /// Gets the latest cached package version fetched from the Pub API for the
+ /// package [packageName].
+ String? cachedPubApiLatestVersion(String packageName) =>
+ packageCache?.packages[packageName]?.latestVersion;
+
+ /// Gets the package versions cached using "pub outdated" for the package
+ /// [packageName] for the project using [pubspecPath].
///
- /// Returns null if no package details are cached.
- PubPackage? cachedPackageDetails(String packageName) =>
- packageCache?.packages[packageName];
+ /// Versions in here might only be available for packages that are in the
+ /// pubspec on disk. Newly-added packages in the overlay might not be
+ /// available.
+ PubOutdatedPackageDetails? cachedPubOutdatedVersions(
+ String pubspecPath, String packageName) {
+ final pubspecCache = _pubspecPackageVersions[pubspecPath];
+ return pubspecCache != null ? pubspecCache[packageName] : null;
+ }
+
+ /// Begin a request to pre-load package versions using the "pub outdated"
+ /// command.
+ ///
+ /// If [pubspecWasModified] is true, the command will always be run. Otherwise it
+ /// will only be run if data is not already cached.
+ Future<void> fetchPackageVersionsViaPubOutdated(String pubspecPath,
+ {required bool pubspecWasModified}) async {
+ final pubCommand = _command;
+ if (pubCommand == null) {
+ return;
+ }
+
+ // If we already have a cache for the file and it was not modified (only
+ // opened) we do not need to re-run the command.
+ if (!pubspecWasModified &&
+ _pubspecPackageVersions.containsKey(pubspecPath)) {
+ return;
+ }
+
+ // Check if this pubspec is inside a DEPS-managed folder, and if so
+ // just cache an empty set of results since Pub is not managing
+ // dependencies.
+ if (_hasAncestorDEPSFile(pubspecPath)) {
+ _pubspecPackageVersions.putIfAbsent(pubspecPath, () => {});
+ return;
+ }
+
+ final results = await pubCommand.outdatedVersions(pubspecPath);
+ final cache = _pubspecPackageVersions.putIfAbsent(pubspecPath, () => {});
+ for (final package in results) {
+ // We use the versions from the "pub outdated" results but only cache them
+ // in-memory for this specific pubspec, as the resolved version may be
+ // restricted by constraints/dependencies in the pubspec. The "pub"
+ // command does caching of the JSON versions to make "pub outdated" fast.
+ cache[package.packageName] = package;
+ }
+ }
+
+ /// Clears package caches for [pubspecPath].
+ ///
+ /// Does not remove other caches that are not pubspec-specific (for example
+ /// the latest version pulled directly from the Pub API independant of
+ /// pubspec).
+ Future<void> flushPackageCaches(String pubspecPath) async {
+ _pubspecPackageVersions.remove(pubspecPath);
+ }
/// Gets package details for package [packageName].
///
@@ -267,6 +361,19 @@
}
}
+ /// Checks whether there is a DEPS file in any folder walking up from the
+ /// pubspec at [pubspecPath].
+ bool _hasAncestorDEPSFile(String pubspecPath) {
+ var folder = path.dirname(pubspecPath);
+ do {
+ if (resourceProvider.getFile(path.join(folder, 'DEPS')).exists) {
+ return true;
+ }
+ folder = path.dirname(folder);
+ } while (folder != path.dirname(folder));
+ return false;
+ }
+
/// Writes the package cache to disk after
/// [PackageDetailsCache._writeCacheDebounceDuration] has elapsed, restarting
/// the timer each time this method is called.
diff --git a/pkg/analysis_server/lib/src/utilities/process.dart b/pkg/analysis_server/lib/src/utilities/process.dart
new file mode 100644
index 0000000..70ea179
--- /dev/null
+++ b/pkg/analysis_server/lib/src/utilities/process.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+/// An abstraction over [Process] from 'dart:io' to allow mocking in tests.
+class ProcessRunner {
+ Future<ProcessResult> run(
+ String executable,
+ List<String> arguments, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ Encoding? stdoutEncoding = systemEncoding,
+ Encoding? stderrEncoding = systemEncoding,
+ }) async {
+ return Process.run(
+ executable,
+ arguments,
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ runInShell: runInShell,
+ stdoutEncoding: stdoutEncoding,
+ stderrEncoding: stderrEncoding,
+ );
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/completion.dart b/pkg/analysis_server/test/lsp/completion.dart
index 75c85b1..6d70a26 100644
--- a/pkg/analysis_server/test/lsp/completion.dart
+++ b/pkg/analysis_server/test/lsp/completion.dart
@@ -8,6 +8,9 @@
import 'server_abstract.dart';
mixin CompletionTestMixin on AbstractLspAnalysisServerTest {
+ /// The last set of completion results fetched.
+ List<CompletionItem> completionResults = [];
+
int sortTextSorter(CompletionItem item1, CompletionItem item2) =>
(item1.sortText ?? item1.label).compareTo(item2.sortText ?? item2.label);
@@ -40,23 +43,24 @@
if (openCloseFile) {
await openFile(fileUri, withoutMarkers(content));
}
- final res = await getCompletion(fileUri, positionFromMarker(content));
+ completionResults =
+ await getCompletion(fileUri, positionFromMarker(content));
if (openCloseFile) {
await closeFile(fileUri);
}
// Sort the completions by sortText and filter to those we expect, so the ordering
// can be compared.
- final sortedResults = res
+ final sortedResults = completionResults
.where((r) => expectCompletions.contains(r.label))
.toList()
- ..sort(sortTextSorter);
+ ..sort(sortTextSorter);
expect(sortedResults.map((item) => item.label), equals(expectCompletions));
// Check the edits apply correctly.
if (applyEditsFor != null) {
- var item = res.singleWhere((c) => c.label == applyEditsFor);
+ var item = completionResults.singleWhere((c) => c.label == applyEditsFor);
final insertFormat = item.insertTextFormat;
if (resolve) {
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index 9d26755..52bf479 100644
--- a/pkg/analysis_server/test/lsp/completion_yaml_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
@@ -2,6 +2,8 @@
// 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';
+
import 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:http/http.dart';
import 'package:linter/src/rules.dart';
@@ -367,7 +369,7 @@
);
}
- Future<void> test_package_version() async {
+ Future<void> test_package_versions_fromApi() async {
httpClient.sendHandler = (BaseRequest request) async {
if (request.url.path.startsWith(PubApi.packageNameListPath)) {
return Response(samplePackageList, 200);
@@ -418,6 +420,163 @@
);
}
+ Future<void> test_package_versions_fromPubOutdated() async {
+ final json = r'''
+ {
+ "packages": [
+ {
+ "package": "one",
+ "latest": { "version": "3.2.1" },
+ "resolvable": { "version": "1.2.4" }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, json, '');
+
+ final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ^''';
+
+ final expected = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ^1.2.4''';
+
+ await initialize();
+ await openFile(pubspecFileUri, withoutMarkers(content));
+ await pumpEventQueue(times: 500);
+
+ await verifyCompletions(
+ pubspecFileUri,
+ content,
+ expectCompletions: ['^1.2.4', '^3.2.1'],
+ applyEditsFor: '^1.2.4',
+ expectedContent: expected,
+ openCloseFile: false,
+ );
+ }
+
+ Future<void> test_package_versions_fromPubOutdated_afterChange() async {
+ final initialJson = r'''
+ {
+ "packages": [
+ {
+ "package": "one",
+ "latest": { "version": "3.2.1" },
+ "resolvable": { "version": "1.2.3" }
+ }
+ ]
+ }
+ ''';
+ final updatedJson = r'''
+ {
+ "packages": [
+ {
+ "package": "one",
+ "latest": { "version": "2.1.0" },
+ "resolvable": { "version": "2.3.4" }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, initialJson, '');
+
+ final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ^''';
+
+ final expected = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ^2.3.4''';
+
+ newFile(pubspecFilePath, content: content);
+ await initialize();
+ await openFile(pubspecFileUri, withoutMarkers(content));
+ await pumpEventQueue(times: 500);
+
+ // Modify the underlying file which should trigger an update of the
+ // cached data.
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, updatedJson, '');
+ modifyFile(pubspecFilePath, '$content# trailing comment');
+ await pumpEventQueue(times: 500);
+
+ await verifyCompletions(
+ pubspecFileUri,
+ content,
+ expectCompletions: ['^2.3.4', '^2.1.0'],
+ applyEditsFor: '^2.3.4',
+ expectedContent: expected,
+ openCloseFile: false,
+ );
+
+ // Also veryify the detail fields were populated as expected.
+ expect(
+ completionResults.singleWhere((c) => c.label == '^2.3.4').detail,
+ equals('latest compatible'),
+ );
+ expect(
+ completionResults.singleWhere((c) => c.label == '^2.1.0').detail,
+ equals('latest'),
+ );
+ }
+
+ Future<void> test_package_versions_fromPubOutdated_afterDelete() async {
+ final initialJson = r'''
+ {
+ "packages": [
+ {
+ "package": "one",
+ "latest": { "version": "3.2.1" },
+ "resolvable": { "version": "1.2.3" }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, initialJson, '');
+
+ final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ^''';
+
+ newFile(pubspecFilePath, content: content);
+ await initialize();
+ await openFile(pubspecFileUri, withoutMarkers(content));
+ await pumpEventQueue(times: 500);
+
+ // Delete the underlying file which should trigger eviction of the cache.
+ deleteFile(pubspecFilePath);
+ await pumpEventQueue(times: 500);
+
+ await verifyCompletions(
+ pubspecFileUri,
+ content,
+ expectCompletions: [],
+ openCloseFile: false,
+ );
+
+ // There should have been no version numbers.
+ expect(completionResults, isEmpty);
+ }
+
Future<void> test_topLevel() async {
final content = '''
version: 1.0.0
diff --git a/pkg/analysis_server/test/lsp/pub_package_service_test.dart b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
index b00f285..c22a0f5 100644
--- a/pkg/analysis_server/test/lsp/pub_package_service_test.dart
+++ b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
@@ -2,10 +2,16 @@
// 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';
+
import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/instrumentation/service.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:collection/collection.dart';
import 'package:http/http.dart';
+import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -15,6 +21,7 @@
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(PubApiTest);
+ defineReflectiveTests(PubCommandTest);
defineReflectiveTests(PubPackageServiceTest);
});
}
@@ -78,6 +85,168 @@
}
@reflectiveTest
+class PubCommandTest with ResourceProviderMixin {
+ late MockProcessRunner processRunner;
+ late PubCommand pubCommand;
+ late String pubspecPath, pubspec2Path;
+
+ void setUp() {
+ pubspecPath = convertPath('/home/project/pubspec.yaml');
+ pubspec2Path = convertPath('/home/project2/pubspec.yaml');
+ processRunner = MockProcessRunner();
+ pubCommand = PubCommand(InstrumentationService.NULL_SERVICE, processRunner);
+ }
+
+ Future<void> test_doesNotRunConcurrently() async {
+ var isRunning = false;
+ processRunner.runHandler = (executable, args, {dir, env}) async {
+ expect(isRunning, isFalse,
+ reason: 'pub commands should not run concurrently');
+ isRunning = true;
+ await pumpEventQueue(times: 500);
+ isRunning = false;
+ return ProcessResult(0, 0, '', '');
+ };
+ await Future.wait([
+ pubCommand.outdatedVersions(pubspecPath),
+ pubCommand.outdatedVersions(pubspecPath),
+ ]);
+ }
+
+ Future<void> test_outdated_args() async {
+ processRunner.runHandler = (executable, args, {dir, env}) {
+ var expectedPubPath = path.join(
+ path.dirname(Platform.resolvedExecutable),
+ Platform.isWindows ? 'pub.bat' : 'pub',
+ );
+ expect(executable, equals(expectedPubPath));
+ expect(
+ args,
+ equals([
+ 'outdated',
+ '--show-all',
+ '--json',
+ ]));
+ expect(dir, equals(convertPath('/home/project')));
+ expect(
+ env!['PUB_ENVIRONMENT'],
+ anyOf(equals('analysis_server.pub_api'),
+ endsWith(':analysis_server.pub_api')));
+ return ProcessResult(0, 0, '', '');
+ };
+ await pubCommand.outdatedVersions(pubspecPath);
+ }
+
+ Future<void> test_outdated_invalidJson() async {
+ processRunner.runHandler = (String executable, List<String> args,
+ {dir, env}) =>
+ ProcessResult(1, 0, 'NOT VALID JSON', '');
+ final result = await pubCommand.outdatedVersions(pubspecPath);
+ expect(result, isEmpty);
+ }
+
+ Future<void> test_outdated_missingFields() async {
+ final validJson = r'''
+ {
+ "packages": [
+ {
+ "package": "foo",
+ "current": { "version": "1.0.0" },
+ "upgradable": { "version": "2.0.0" },
+ "resolvable": { }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, validJson, '');
+ final result = await pubCommand.outdatedVersions(pubspecPath);
+ expect(result, hasLength(1));
+ final package = result.first;
+ expect(package.packageName, equals('foo'));
+ expect(package.currentVersion, equals('1.0.0'));
+ expect(package.upgradableVersion, equals('2.0.0'));
+ expect(package.resolvableVersion, isNull);
+ expect(package.latestVersion, isNull);
+ }
+
+ Future<void> test_outdated_multiplePubspecs() async {
+ final pubspecJson1 = r'''
+ {
+ "packages": [
+ {
+ "package": "foo",
+ "resolvable": { "version": "1.1.1" }
+ }
+ ]
+ }
+ ''';
+ final pubspecJson2 = r'''
+ {
+ "packages": [
+ {
+ "package": "foo",
+ "resolvable": { "version": "2.2.2" }
+ }
+ ]
+ }
+ ''';
+
+ processRunner.runHandler = (executable, args, {dir, env}) {
+ // Return different json based on the directory we were invoked in.
+ final json =
+ dir == path.dirname(pubspecPath) ? pubspecJson1 : pubspecJson2;
+ return ProcessResult(1, 0, json, '');
+ };
+ final result1 = await pubCommand.outdatedVersions(pubspecPath);
+ final result2 = await pubCommand.outdatedVersions(pubspec2Path);
+ expect(result1.first.resolvableVersion, equals('1.1.1'));
+ expect(result2.first.resolvableVersion, equals('2.2.2'));
+ }
+
+ Future<void> test_outdated_nonZeroExitCode() async {
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 123, '{}', '');
+ final result = await pubCommand.outdatedVersions(pubspecPath);
+ expect(result, isEmpty);
+ }
+
+ Future<void> test_validJson() async {
+ final validJson = r'''
+ {
+ "packages": [
+ {
+ "package": "foo",
+ "current": { "version": "1.0.0" },
+ "upgradable": { "version": "2.0.0" },
+ "resolvable": { "version": "3.0.0" },
+ "latest": { "version": "4.0.0" }
+ },
+ {
+ "package": "bar",
+ "current": { "version": "1.0.0" },
+ "upgradable": { "version": "2.0.0" },
+ "resolvable": { "version": "3.0.0" },
+ "latest": { "version": "4.0.0" }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, validJson, '');
+ final result = await pubCommand.outdatedVersions(pubspecPath);
+ expect(result, hasLength(2));
+ result.forEachIndexed((index, package) {
+ expect(package.packageName, equals(index == 0 ? 'foo' : 'bar'));
+ expect(package.currentVersion, equals('1.0.0'));
+ expect(package.upgradableVersion, equals('2.0.0'));
+ expect(package.resolvableVersion, equals('3.0.0'));
+ expect(package.latestVersion, equals('4.0.0'));
+ });
+ }
+}
+
+@reflectiveTest
class PubPackageServiceTest extends AbstractLspAnalysisServerTest {
/// A sample API response for package names. This should match the JSON served
/// at https://pub.dev/api/package-name-completion-data.
@@ -194,13 +363,13 @@
Future<void> test_packageCache_initializesOnPubspecOpen() async {
await initialize();
- expect(server.pubPackageService.isRunning, isFalse);
+ expect(server.pubPackageService.isPackageNamesTimerRunning, isFalse);
expect(server.pubPackageService.packageCache, isNull);
expectPackages([]);
await openFile(pubspecFileUri, '');
await pumpEventQueue();
- expect(server.pubPackageService.isRunning, isTrue);
+ expect(server.pubPackageService.isPackageNamesTimerRunning, isTrue);
expect(server.pubPackageService.packageCache, isNotNull);
expectPackages([]);
}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 963542a..2e49ae5 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -48,6 +48,7 @@
late MockLspServerChannel channel;
late TestPluginManager pluginManager;
late LspAnalysisServer server;
+ late MockProcessRunner processRunner;
late MockHttpClient httpClient;
/// The number of context builds that had already occurred the last time
@@ -164,6 +165,7 @@
void setUp() {
httpClient = MockHttpClient();
+ processRunner = MockProcessRunner();
channel = MockLspServerChannel(debugPrintCommunication);
// Create an SDK in the mock file system.
MockSdk(resourceProvider: resourceProvider);
@@ -175,7 +177,8 @@
DartSdkManager(convertPath('/sdk')),
CrashReportingAttachmentsBuilder.empty,
InstrumentationService.NULL_SERVICE,
- httpClient: httpClient);
+ httpClient: httpClient,
+ processRunner: processRunner);
server.pluginManager = pluginManager;
projectFolderPath = convertPath('/home/test');
diff --git a/pkg/analysis_server/test/mocks.dart b/pkg/analysis_server/test/mocks.dart
index d6cfa2c..e1f3ecd 100644
--- a/pkg/analysis_server/test/mocks.dart
+++ b/pkg/analysis_server/test/mocks.dart
@@ -3,9 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
+import 'package:analysis_server/src/utilities/process.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
@@ -47,6 +50,32 @@
}
}
+class MockProcessRunner implements ProcessRunner {
+ FutureOr<ProcessResult> Function(String executable, List<String> arguments,
+ {String? dir, Map<String, String>? env})? runHandler =
+ (executable, arguments, {dir, env}) => throw UnimplementedError();
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) {
+ return super.noSuchMethod(invocation);
+ }
+
+ @override
+ Future<ProcessResult> run(
+ String executable,
+ List<String> arguments, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ Encoding? stdoutEncoding = systemEncoding,
+ Encoding? stderrEncoding = systemEncoding,
+ }) async {
+ return runHandler!(executable, arguments,
+ dir: workingDirectory, env: environment);
+ }
+}
+
class MockSource implements Source {
@override
final String fullName;
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
index 4f4923f..4eab94d 100644
--- a/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
+++ b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
@@ -2,8 +2,11 @@
// 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';
+
import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/instrumentation/service.dart';
import 'package:http/http.dart';
@@ -22,6 +25,7 @@
@reflectiveTest
class PubspecGeneratorTest extends YamlGeneratorTest {
late MockHttpClient httpClient;
+ late MockProcessRunner processRunner;
late PubPackageService pubPackageService;
@@ -34,10 +38,12 @@
void setUp() {
httpClient = MockHttpClient();
+ processRunner = MockProcessRunner();
pubPackageService = PubPackageService(
InstrumentationService.NULL_SERVICE,
resourceProvider,
- PubApi(InstrumentationService.NULL_SERVICE, httpClient, null));
+ PubApi(InstrumentationService.NULL_SERVICE, httpClient, null),
+ PubCommand(InstrumentationService.NULL_SERVICE, processRunner));
}
void tearDown() {
@@ -339,4 +345,46 @@
''');
assertSuggestion('two: ');
}
+
+ void test_packageVersion() async {
+ final json = r'''
+ {
+ "packages": [
+ {
+ "package": "one",
+ "latest": { "version": "3.2.1" },
+ "resolvable": { "version": "1.2.4" }
+ }
+ ]
+ }
+ ''';
+ processRunner.runHandler =
+ (executable, args, {dir, env}) => ProcessResult(1, 0, json, '');
+
+ pubPackageService.beginCachePreloads([convertPath('/home/test/$fileName')]);
+ await pumpEventQueue(times: 500);
+
+ getCompletions('''
+dependencies:
+ one: ^
+''');
+ assertSuggestion('^1.2.4');
+ assertSuggestion('^3.2.1');
+ }
+
+ /// Ensure in a repo with a DEPS file like the SDK, we do not run pub
+ /// processes to cache the version numbers.
+ void test_packageVersion_withDEPSfile() async {
+ var didRun = false;
+ processRunner.runHandler = (executable, args, {dir, env}) {
+ didRun = true;
+ return ProcessResult(1, 0, '', '');
+ };
+
+ newFile('/home/DEPS');
+ pubPackageService.beginCachePreloads([convertPath('/home/test/$fileName')]);
+ await pumpEventQueue(times: 500);
+
+ expect(didRun, isFalse);
+ }
}
diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart
index f362f93..ae8a8fc 100644
--- a/pkg/analyzer/lib/src/dart/element/element.dart
+++ b/pkg/analyzer/lib/src/dart/element/element.dart
@@ -15,6 +15,7 @@
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:analyzer/src/dart/analysis/session.dart';
import 'package:analyzer/src/dart/ast/ast_factory.dart';
@@ -4005,6 +4006,64 @@
return getTypeFromParts(className, _definingCompilationUnit, _parts);
}
+ /// Indicates whether it is unnecessary to report an undefined identifier
+ /// error for an identifier reference with the given [name] and optional
+ /// [prefix].
+ ///
+ /// This method is intended to reduce spurious errors in circumstances where
+ /// an undefined identifier occurs as the result of a missing (most likely
+ /// code generated) file. It will only return `true` in a circumstance where
+ /// the current library is guaranteed to have at least one other error (due to
+ /// a missing part or import), so there is no risk that ignoring the undefined
+ /// identifier would cause an invalid program to be treated as valid.
+ bool shouldIgnoreUndefined({
+ required String? prefix,
+ required String name,
+ }) {
+ for (var importElement in imports) {
+ if (importElement.prefix?.name == prefix &&
+ importElement.importedLibrary?.isSynthetic != false) {
+ var showCombinators = importElement.combinators
+ .whereType<ShowElementCombinator>()
+ .toList();
+ if (prefix != null && showCombinators.isEmpty) {
+ return true;
+ }
+ for (var combinator in showCombinators) {
+ if (combinator.shownNames.contains(name)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ if (prefix == null && name.startsWith(r'_$')) {
+ for (var partElement in parts) {
+ if (partElement.isSynthetic && isGeneratedSource(partElement.source)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// Convenience wrapper around [shouldIgnoreUndefined] that calls it for a
+ /// given (possibly prefixed) identifier [node].
+ bool shouldIgnoreUndefinedIdentifier(Identifier node) {
+ if (node is PrefixedIdentifier) {
+ return shouldIgnoreUndefined(
+ prefix: node.prefix.name,
+ name: node.identifier.name,
+ );
+ }
+
+ return shouldIgnoreUndefined(
+ prefix: null,
+ name: (node as SimpleIdentifier).name,
+ );
+ }
+
@override
T toLegacyElementIfOptOut<T extends Element>(T element) {
if (isNonNullableByDefault) return element;
diff --git a/pkg/analyzer/lib/src/dart/element/scope.dart b/pkg/analyzer/lib/src/dart/element/scope.dart
index 1f15606..4df2190 100644
--- a/pkg/analyzer/lib/src/dart/element/scope.dart
+++ b/pkg/analyzer/lib/src/dart/element/scope.dart
@@ -4,7 +4,6 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
-import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/resolver/scope.dart' as impl;
@@ -104,38 +103,6 @@
_element.units.forEach(_addUnitElements);
}
- bool shouldIgnoreUndefined({
- required String? prefix,
- required String name,
- }) {
- for (var importElement in _element.imports) {
- if (importElement.prefix?.name == prefix &&
- importElement.importedLibrary?.isSynthetic != false) {
- var showCombinators = importElement.combinators
- .whereType<ShowElementCombinator>()
- .toList();
- if (prefix != null && showCombinators.isEmpty) {
- return true;
- }
- for (var combinator in showCombinators) {
- if (combinator.shownNames.contains(name)) {
- return true;
- }
- }
- }
- }
-
- if (prefix == null && name.startsWith(r'_$')) {
- for (var partElement in _element.parts) {
- if (partElement.isSynthetic && isGeneratedSource(partElement.source)) {
- return true;
- }
- }
- }
-
- return false;
- }
-
void _addExtension(ExtensionElement element) {
_addGetter(element);
if (!extensions.contains(element)) {
diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
index 7485e6e..9de1a34 100644
--- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
@@ -10,12 +10,12 @@
import 'package:analyzer/src/dart/ast/ast_factory.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
+import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/extension_member_resolver.dart';
import 'package:analyzer/src/dart/resolver/invocation_inference_helper.dart';
-import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/migratable_ast_info_provider.dart';
import 'package:analyzer/src/generated/resolver.dart';
@@ -42,7 +42,7 @@
final InheritanceManager3 _inheritance;
/// The element for the library containing the compilation unit being visited.
- final LibraryElement _definingLibrary;
+ final LibraryElementImpl _definingLibrary;
/// The URI of [_definingLibrary].
final Uri _definingLibraryUri;
@@ -271,7 +271,7 @@
}) {
_setDynamicResolution(node, whyNotPromotedList: whyNotPromotedList);
- if (nameScope.shouldIgnoreUndefined2(prefix: prefix, name: name)) {
+ if (_definingLibrary.shouldIgnoreUndefined(prefix: prefix, name: name)) {
return;
}
diff --git a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
index e2d5336..10c9f96 100644
--- a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
@@ -13,7 +13,6 @@
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/extension_member_resolver.dart';
import 'package:analyzer/src/dart/resolver/resolution_result.dart';
-import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/error/assignment_verifier.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';
@@ -590,7 +589,7 @@
if (hasRead && readElement == null || hasWrite && writeElement == null) {
if (!forAnnotation &&
- !_resolver.nameScope.shouldIgnoreUndefined2(
+ !_resolver.definingLibrary.shouldIgnoreUndefined(
prefix: target.name,
name: identifier.name,
)) {
diff --git a/pkg/analyzer/lib/src/dart/resolver/resolution_visitor.dart b/pkg/analyzer/lib/src/dart/resolver/resolution_visitor.dart
index 6a03d38..f08fb8d 100644
--- a/pkg/analyzer/lib/src/dart/resolver/resolution_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/resolution_visitor.dart
@@ -56,6 +56,7 @@
/// 3. Resolve all [TypeName]s - set elements and types.
/// 4. Resolve all [GenericFunctionType]s - set their types.
class ResolutionVisitor extends RecursiveAstVisitor<void> {
+ LibraryElementImpl _libraryElement;
final TypeProvider _typeProvider;
final CompilationUnitElementImpl _unitElement;
final bool _isNonNullableByDefault;
@@ -102,13 +103,14 @@
);
var typeNameResolver = TypeNameResolver(
- libraryElement.typeSystem,
+ libraryElement,
typeProvider,
isNonNullableByDefault,
errorReporter,
);
return ResolutionVisitor._(
+ libraryElement,
typeProvider,
unitElement,
isNonNullableByDefault,
@@ -122,6 +124,7 @@
}
ResolutionVisitor._(
+ this._libraryElement,
this._typeProvider,
this._unitElement,
this._isNonNullableByDefault,
@@ -1211,7 +1214,7 @@
// If the type is not an InterfaceType, then visitTypeName() sets the type
// to be a DynamicTypeImpl
Identifier name = typeName.name;
- if (!_nameScope.shouldIgnoreUndefined(name)) {
+ if (!_libraryElement.shouldIgnoreUndefinedIdentifier(name)) {
_errorReporter.reportErrorForNode(errorCode, name, [name.name]);
}
}
diff --git a/pkg/analyzer/lib/src/dart/resolver/scope.dart b/pkg/analyzer/lib/src/dart/resolver/scope.dart
index 95850f3..845c8e1 100644
--- a/pkg/analyzer/lib/src/dart/resolver/scope.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/scope.dart
@@ -352,35 +352,6 @@
}
extension ScopeExtension on Scope {
- /// Return `true` if the fact that the given [node] is not defined should be
- /// ignored (from the perspective of error reporting).
- bool shouldIgnoreUndefined(Identifier node) {
- if (node is PrefixedIdentifier) {
- return shouldIgnoreUndefined2(
- prefix: node.prefix.name,
- name: node.identifier.name,
- );
- }
-
- return shouldIgnoreUndefined2(
- prefix: null,
- name: (node as SimpleIdentifier).name,
- );
- }
-
- /// Return `true` if the fact that the identifier with the given [prefix]
- /// (might be `null`) and [name] is not defined should be ignored (from the
- /// perspective of error reporting).
- bool shouldIgnoreUndefined2({
- required String? prefix,
- required String name,
- }) {
- return _enclosingLibraryScope.shouldIgnoreUndefined(
- prefix: prefix,
- name: name,
- );
- }
-
List<ExtensionElement> get extensions {
return _enclosingLibraryScope.extensions;
}
diff --git a/pkg/analyzer/lib/src/dart/resolver/simple_identifier_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/simple_identifier_resolver.dart
index a3e5943..4c872f9 100644
--- a/pkg/analyzer/lib/src/dart/resolver/simple_identifier_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/simple_identifier_resolver.dart
@@ -14,7 +14,6 @@
import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
import 'package:analyzer/src/dart/resolver/invocation_inference_helper.dart';
import 'package:analyzer/src/dart/resolver/property_element_resolver.dart';
-import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';
@@ -203,7 +202,8 @@
CompileTimeErrorCode.UNDEFINED_IDENTIFIER_AWAIT,
node,
);
- } else if (!_resolver.nameScope.shouldIgnoreUndefined(node)) {
+ } else if (!_resolver.definingLibrary
+ .shouldIgnoreUndefinedIdentifier(node)) {
_errorReporter.reportErrorForNode(
CompileTimeErrorCode.UNDEFINED_IDENTIFIER,
node,
diff --git a/pkg/analyzer/lib/src/dart/resolver/type_name_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/type_name_resolver.dart
index 4acea06..3f3c167 100644
--- a/pkg/analyzer/lib/src/dart/resolver/type_name_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/type_name_resolver.dart
@@ -16,7 +16,6 @@
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
-import 'package:analyzer/src/dart/resolver/scope.dart';
import 'package:analyzer/src/diagnostic/diagnostic_factory.dart';
import 'package:analyzer/src/error/codes.dart';
@@ -24,6 +23,7 @@
///
/// The client must set [nameScope] before calling [resolveTypeName].
class TypeNameResolver {
+ final LibraryElementImpl _libraryElement;
final TypeSystemImpl typeSystem;
final DartType dynamicType;
final bool isNonNullableByDefault;
@@ -56,9 +56,10 @@
/// If [resolveTypeName] reported an error, this flag is set to `true`.
bool hasErrorReported = false;
- TypeNameResolver(this.typeSystem, TypeProvider typeProvider,
+ TypeNameResolver(this._libraryElement, TypeProvider typeProvider,
this.isNonNullableByDefault, this.errorReporter)
- : dynamicType = typeProvider.dynamicType;
+ : typeSystem = _libraryElement.typeSystem,
+ dynamicType = typeProvider.dynamicType;
bool get _genericMetadataIsEnabled =>
enclosingClass!.library.featureSet.isEnabled(Feature.generic_metadata);
@@ -285,7 +286,7 @@
void _resolveToElement(TypeNameImpl node, Element? element) {
if (element == null) {
node.type = dynamicType;
- if (!nameScope.shouldIgnoreUndefined(node.name)) {
+ if (!_libraryElement.shouldIgnoreUndefinedIdentifier(node.name)) {
_ErrorHelper(errorReporter).reportNullOrNonTypeElement(node, null);
}
return;
diff --git a/pkg/analyzer/lib/src/dart/resolver/typed_literal_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/typed_literal_resolver.dart
index ee009b4..e781de9 100644
--- a/pkg/analyzer/lib/src/dart/resolver/typed_literal_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/typed_literal_resolver.dart
@@ -37,7 +37,7 @@
TypeSystemImpl typeSystem, TypeProviderImpl typeProvider,
{MigratableAstInfoProvider migratableAstInfoProvider =
const MigratableAstInfoProvider()}) {
- var library = resolver.definingLibrary as LibraryElementImpl;
+ var library = resolver.definingLibrary;
var analysisOptions = library.context.analysisOptions;
var analysisOptionsImpl = analysisOptions as AnalysisOptionsImpl;
return TypedLiteralResolver._(
diff --git a/pkg/analyzer/lib/src/generated/element_resolver.dart b/pkg/analyzer/lib/src/generated/element_resolver.dart
index cc5fc13..2a378e11 100644
--- a/pkg/analyzer/lib/src/generated/element_resolver.dart
+++ b/pkg/analyzer/lib/src/generated/element_resolver.dart
@@ -498,7 +498,8 @@
var declaration = node.thisOrAncestorOfType<ClassDeclaration>();
var superclassName = declaration?.extendsClause?.superclass.name;
if (superclassName != null &&
- _resolver.nameScope.shouldIgnoreUndefined(superclassName)) {
+ _resolver.definingLibrary
+ .shouldIgnoreUndefinedIdentifier(superclassName)) {
return;
}
var argumentList = node.argumentList;
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 9bdf31e..09b83ed 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -268,7 +268,7 @@
/// breaking change).
ResolverVisitor(
InheritanceManager3 inheritanceManager,
- LibraryElement definingLibrary,
+ LibraryElementImpl definingLibrary,
Source source,
TypeProvider typeProvider,
AnalysisErrorListener errorListener,
@@ -279,7 +279,7 @@
inheritanceManager,
definingLibrary,
source,
- definingLibrary.typeSystem as TypeSystemImpl,
+ definingLibrary.typeSystem,
typeProvider,
errorListener,
featureSet ??
@@ -291,7 +291,7 @@
ResolverVisitor._(
this.inheritance,
- LibraryElement definingLibrary,
+ LibraryElementImpl definingLibrary,
Source source,
this.typeSystem,
TypeProvider typeProvider,
@@ -2303,7 +2303,7 @@
ResolverVisitorForMigration(
InheritanceManager3 inheritanceManager,
- LibraryElement definingLibrary,
+ LibraryElementImpl definingLibrary,
Source source,
TypeProvider typeProvider,
AnalysisErrorListener errorListener,
@@ -2373,7 +2373,7 @@
static const _nameScopeProperty = 'nameScope';
/// The element for the library containing the compilation unit being visited.
- final LibraryElement definingLibrary;
+ final LibraryElementImpl definingLibrary;
/// The source representing the compilation unit being visited.
final Source source;
@@ -3164,7 +3164,7 @@
/// [nameScope] is the scope used to resolve identifiers in the node that will
/// first be visited. If `null` or unspecified, a new [LibraryScope] will be
/// created based on [definingLibrary] and [typeProvider].
- VariableResolverVisitor(LibraryElement definingLibrary, Source source,
+ VariableResolverVisitor(LibraryElementImpl definingLibrary, Source source,
TypeProvider typeProvider, AnalysisErrorListener errorListener,
{Scope? nameScope})
: super(definingLibrary, source, typeProvider as TypeProviderImpl,
diff --git a/pkg/nnbd_migration/lib/src/fix_builder.dart b/pkg/nnbd_migration/lib/src/fix_builder.dart
index 8a70226..bcfdef6 100644
--- a/pkg/nnbd_migration/lib/src/fix_builder.dart
+++ b/pkg/nnbd_migration/lib/src/fix_builder.dart
@@ -141,7 +141,7 @@
TypeProvider typeProvider,
TypeSystemImpl typeSystem,
Variables? variables,
- LibraryElement definingLibrary,
+ LibraryElementImpl definingLibrary,
NullabilityMigrationListener? listener,
CompilationUnit? unit,
bool? warnOnWeakCode,
@@ -170,7 +170,7 @@
this._typeSystem,
this._variables,
this.source,
- LibraryElement definingLibrary,
+ LibraryElementImpl definingLibrary,
this.listener,
this.unit,
this.migrationResolutionHooks,
diff --git a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
index f22569b..867f72a 100644
--- a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
@@ -6,6 +6,7 @@
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/analysis/session.dart';
+import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
@@ -146,7 +147,7 @@
result.typeProvider,
library.typeSystem as TypeSystemImpl,
_variables,
- library,
+ library as LibraryElementImpl,
_permissive! ? listener : null,
unit,
warnOnWeakCode,
diff --git a/pkg/nnbd_migration/test/fix_builder_test.dart b/pkg/nnbd_migration/test/fix_builder_test.dart
index a9026b2..74fa1a8 100644
--- a/pkg/nnbd_migration/test/fix_builder_test.dart
+++ b/pkg/nnbd_migration/test/fix_builder_test.dart
@@ -5,6 +5,7 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
+import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/error/hint_codes.dart';
import 'package:analyzer/src/generated/element_type_provider.dart';
@@ -3886,7 +3887,7 @@
typeProvider,
typeSystem,
variables,
- definingLibrary,
+ definingLibrary as LibraryElementImpl,
null,
scope.thisOrAncestorOfType<CompilationUnit>(),
warnOnWeakCode,
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index d7e8fcb..b1a7315 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -988,6 +988,8 @@
NoBackgroundCompilerScope no_bg_compiler(Thread::Current());
heap()->CollectAllGarbage();
Thread* thread = Thread::Current();
+ SafepointMutexLocker ml(
+ thread->isolate_group()->constant_canonicalization_mutex());
HeapIterationScope iteration(thread);
VerifyCanonicalVisitor check_canonical(thread);
iteration.IterateObjects(&check_canonical);
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index 0df5c45..a1b8649 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -19122,8 +19122,9 @@
Zone* zone = thread->zone();
Instance& result = Instance::Handle(zone);
const Class& cls = Class::Handle(zone, this->clazz());
- SafepointMutexLocker ml(
- thread->isolate_group()->constant_canonicalization_mutex());
+ ASSERT(thread->isolate_group()
+ ->constant_canonicalization_mutex()
+ ->IsOwnedByCurrentThread());
result ^= cls.LookupCanonicalInstance(zone, *this);
return (result.ptr() == this->ptr());
}
@@ -21100,7 +21101,9 @@
ObjectStore* object_store = isolate_group->object_store();
{
- SafepointMutexLocker ml(isolate_group->type_canonicalization_mutex());
+ ASSERT(thread->isolate_group()
+ ->constant_canonicalization_mutex()
+ ->IsOwnedByCurrentThread());
CanonicalTypeSet table(zone, object_store->canonical_types());
type ^= table.GetOrNull(CanonicalTypeKey(*this));
object_store->set_canonical_types(table.Release());
@@ -21457,7 +21460,9 @@
FunctionType& type = FunctionType::Handle(zone);
ObjectStore* object_store = isolate_group->object_store();
{
- SafepointMutexLocker ml(isolate_group->type_canonicalization_mutex());
+ ASSERT(thread->isolate_group()
+ ->constant_canonicalization_mutex()
+ ->IsOwnedByCurrentThread());
CanonicalFunctionTypeSet table(zone,
object_store->canonical_function_types());
type ^= table.GetOrNull(CanonicalFunctionTypeKey(*this));
@@ -22008,7 +22013,9 @@
TypeParameter& type_parameter = TypeParameter::Handle(zone);
ObjectStore* object_store = isolate_group->object_store();
{
- SafepointMutexLocker ml(isolate_group->type_canonicalization_mutex());
+ ASSERT(thread->isolate_group()
+ ->constant_canonicalization_mutex()
+ ->IsOwnedByCurrentThread());
CanonicalTypeParameterSet table(zone,
object_store->canonical_type_parameters());
type_parameter ^= table.GetOrNull(CanonicalTypeParameterKey(*this));
diff --git a/tools/VERSION b/tools/VERSION
index 40d74c7..cee8dd7 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 14
PATCH 0
-PRERELEASE 373
+PRERELEASE 374
PRERELEASE_PATCH 0
\ No newline at end of file