Version 2.13.0-101.0.dev
Merge commit 'd901f7326a86cecf53ae312a31d2d435d3c773e5' into 'dev'
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index c7afc20..d32f2dd 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -54,6 +54,7 @@
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element;
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:analyzer_plugin/utilities/navigation/navigation_dart.dart';
+import 'package:http/http.dart' as http;
import 'package:telemetry/crash_reporting.dart';
import 'package:telemetry/telemetry.dart' as telemetry;
import 'package:watcher/watcher.dart';
@@ -127,6 +128,7 @@
DartSdkManager sdkManager,
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
InstrumentationService instrumentationService, {
+ http.Client httpClient,
RequestStatisticsHelper requestStatistics,
DiagnosticServer diagnosticServer,
this.detachableFileSystemManager,
@@ -137,6 +139,7 @@
crashReportingAttachmentsBuilder,
baseResourceProvider,
instrumentationService,
+ httpClient,
NotificationManager(channel, baseResourceProvider.pathContext),
requestStatistics: requestStatistics,
) {
@@ -421,6 +424,14 @@
/// Set the priority files to the given [files].
void setPriorityFiles(String requestId, List<String> files) {
+ 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();
+ }
+
priorityFiles.clear();
priorityFiles.addAll(files);
// Set priority files in drivers.
@@ -429,7 +440,12 @@
});
}
+ @override
Future<void> shutdown() {
+ super.shutdown();
+
+ pubApi.close();
+
if (options.analytics != null) {
options.analytics
.waitForLastPing(timeout: Duration(milliseconds: 200))
diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
index 0853e07..22595fe 100644
--- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart
+++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
@@ -4,6 +4,7 @@
import 'dart:core';
import 'dart:io' as io;
+import 'dart:io';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/collections.dart';
@@ -15,6 +16,8 @@
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.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_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';
@@ -42,6 +45,8 @@
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/services/available_declarations.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
/// Implementations of [AbstractAnalysisServer] implement a server that listens
/// on a [CommunicationChannel] for analysis messages and process them.
@@ -93,6 +98,12 @@
/// or `null` if the initial analysis is not yet complete
ServerPerformance performanceAfterStartup;
+ /// A client for making requests to the pub.dev API.
+ final PubApi pubApi;
+
+ /// A service for fetching pub.dev package details.
+ PubPackageService pubPackageService;
+
/// The class into which performance information is currently being recorded.
/// During startup, this will be the same as [performanceDuringStartup]
/// and after startup is complete, this switches to [performanceAfterStartup].
@@ -126,9 +137,14 @@
this.crashReportingAttachmentsBuilder,
ResourceProvider baseResourceProvider,
this.instrumentationService,
+ http.Client httpClient,
this.notificationManager, {
this.requestStatistics,
- }) : resourceProvider = OverlayResourceProvider(baseResourceProvider) {
+ }) : resourceProvider = OverlayResourceProvider(baseResourceProvider),
+ pubApi = PubApi(instrumentationService, httpClient,
+ Platform.environment['PUB_HOSTED_URL']) {
+ pubPackageService =
+ PubPackageService(instrumentationService, baseResourceProvider, pubApi);
performance = performanceDuringStartup;
pluginManager = PluginManager(
@@ -398,6 +414,11 @@
bool fatal = false,
});
+ @mustCallSuper
+ void shutdown() {
+ pubPackageService.shutdown();
+ }
+
/// Return the path to the location of the byte store on disk, or `null` if
/// there is no on-disk byte store.
String _getByteStorePath() {
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index a9c9aa1..ea54b61 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -163,7 +163,7 @@
var generator = FixDataGenerator(provider);
return generator.getSuggestions(file, offset);
} else if (file_paths.isPubspecYaml(pathContext, file)) {
- var generator = PubspecGenerator(provider);
+ var generator = PubspecGenerator(provider, server.pubPackageService);
return generator.getSuggestions(file, offset);
}
return const YamlCompletionResults.empty();
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index e581d84..4268054 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -115,7 +115,8 @@
} else if (file_paths.isFixDataYaml(pathContext, path.result)) {
generator = FixDataGenerator(server.resourceProvider);
} else if (file_paths.isPubspecYaml(pathContext, path.result)) {
- generator = PubspecGenerator(server.resourceProvider);
+ generator = PubspecGenerator(
+ server.resourceProvider, server.pubPackageService);
}
if (generator != null) {
serverResultsFuture = _getServerYamlItems(
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 6a5bbc1..46e1fe1 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -43,10 +43,11 @@
import 'package:analyzer/src/dart/analysis/driver.dart' as nd;
import 'package:analyzer/src/dart/analysis/status.dart' as nd;
import 'package:analyzer/src/generated/sdk.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
-import 'package:path/path.dart';
+import 'package:http/http.dart' as http;
import 'package:watcher/watcher.dart';
/// Instances of the class [LspAnalysisServer] implement an LSP-based server
@@ -134,6 +135,7 @@
DartSdkManager sdkManager,
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
InstrumentationService instrumentationService, {
+ http.Client httpClient,
DiagnosticServer diagnosticServer,
}) : super(
options,
@@ -142,6 +144,7 @@
crashReportingAttachmentsBuilder,
baseResourceProvider,
instrumentationService,
+ httpClient,
LspNotificationManager(channel, baseResourceProvider.pathContext),
) {
notificationManager.server = this;
@@ -185,8 +188,14 @@
RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??=
RefactoringWorkspace(driverMap.values, searchEngine);
- void addPriorityFile(String path) {
- final didAdd = priorityFiles.add(path);
+ 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();
+ }
+
+ final didAdd = priorityFiles.add(filePath);
assert(didAdd);
if (didAdd) {
_updateDriversAndPluginsPriorityFiles();
@@ -616,7 +625,10 @@
return MessageActionItem.fromJson(response.result);
}
+ @override
Future<void> shutdown() {
+ super.shutdown();
+
// Defer closing the channel so that the shutdown response can be sent and
// logged.
Future(() {
@@ -664,13 +676,15 @@
..addAll(_temporaryAnalysisRoots.values);
final excludedPaths = clientConfiguration.analysisExcludedFolders
- .expand((excludePath) => isAbsolute(excludePath)
+ .expand((excludePath) => resourceProvider.pathContext
+ .isAbsolute(excludePath)
? [excludePath]
// Apply the relative path to each open workspace folder.
// TODO(dantup): Consider supporting per-workspace config by
// calling workspace/configuration whenever workspace folders change
// and caching the config for each one.
- : _explicitAnalysisRoots.map((root) => join(root, excludePath)))
+ : _explicitAnalysisRoots.map(
+ (root) => resourceProvider.pathContext.join(root, excludePath)))
.toSet();
// If the roots didn't actually change from the last time they were set
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 720155c..7125eaa 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -308,12 +308,10 @@
return lsp.CompletionItem(
label: label,
kind: completionKind,
- tags: supportedTags.isNotEmpty
- ? [
- if (supportsDeprecatedTag && declaration.isDeprecated)
- lsp.CompletionItemTag.Deprecated
- ]
- : null,
+ tags: nullIfEmpty([
+ if (supportsDeprecatedTag && declaration.isDeprecated)
+ lsp.CompletionItemTag.Deprecated
+ ]),
commitCharacters:
includeCommitCharacters ? lsp.dartCompletionCommitCharacters : null,
detail: getDeclarationCompletionDetail(declaration, completionKind,
@@ -631,6 +629,8 @@
);
}
+List<T> nullIfEmpty<T>(List<T> items) => items.isEmpty ? null : items;
+
/// Returns the file system path for a TextDocumentIdentifier.
ErrorOr<String> pathOfDoc(lsp.TextDocumentIdentifier doc) =>
pathOfUri(Uri.tryParse(doc?.uri));
@@ -782,6 +782,8 @@
return const [lsp.CompletionItemKind.Variable];
case server.CompletionSuggestionKind.PARAMETER:
return const [lsp.CompletionItemKind.Value];
+ case server.CompletionSuggestionKind.PACKAGE_NAME:
+ return const [lsp.CompletionItemKind.Module];
default:
return const [];
}
@@ -894,12 +896,10 @@
return lsp.CompletionItem(
label: label,
kind: completionKind,
- tags: supportedTags.isNotEmpty
- ? [
- if (supportsDeprecatedTag && suggestion.isDeprecated)
- lsp.CompletionItemTag.Deprecated
- ]
- : null,
+ tags: nullIfEmpty([
+ if (supportsDeprecatedTag && suggestion.isDeprecated)
+ lsp.CompletionItemTag.Deprecated
+ ]),
commitCharacters:
includeCommitCharacters ? dartCompletionCommitCharacters : null,
data: resolutionData,
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
index cd4d7bd..7f60edd 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
@@ -47,7 +47,7 @@
/// Initialize a newly created suggestion generator for analysis options
/// files.
AnalysisOptionsGenerator(ResourceProvider resourceProvider)
- : super(resourceProvider);
+ : super(resourceProvider, null);
@override
Producer get topLevelProducer => analysisOptionsProducer;
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
index 81bc727..15ef594 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
@@ -72,7 +72,8 @@
}));
/// Initialize a newly created suggestion generator for fix data files.
- FixDataGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+ FixDataGenerator(ResourceProvider resourceProvider)
+ : super(resourceProvider, null);
@override
Producer get topLevelProducer => fixDataProducer;
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 31c1975..06a5401 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
@@ -179,6 +180,12 @@
CompletionSuggestion(CompletionSuggestionKind.IDENTIFIER, relevance,
identifier, identifier.length, 0, false, false);
+ /// A utility method used to create a suggestion for the package [packageName].
+ CompletionSuggestion packageName(String packageName,
+ {int relevance = 1000}) =>
+ CompletionSuggestion(CompletionSuggestionKind.PACKAGE_NAME, relevance,
+ packageName, packageName.length, 0, false, false);
+
/// Return the completion suggestions appropriate to this location.
Iterable<CompletionSuggestion> suggestions(YamlCompletionRequest request);
}
@@ -188,6 +195,9 @@
/// The resource provider used to access the file system.
final ResourceProvider resourceProvider;
+ /// The Pub package service used for looking up package names/versions.
+ final PubPackageService pubPackageService;
+
/// The absolute path of the file in which completions are being requested.
final String filePath;
@@ -198,5 +208,6 @@
YamlCompletionRequest(
{@required this.filePath,
@required this.precedingText,
- @required this.resourceProvider});
+ @required this.resourceProvider,
+ @required this.pubPackageService});
}
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 2b3794f..d167c72 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
@@ -2,10 +2,26 @@
// 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 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/services/completion/yaml/producer.dart';
import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/file_system/file_system.dart';
+/// An object that represents the location of a package name.
+class PubPackageNameProducer extends Producer {
+ const PubPackageNameProducer();
+
+ @override
+ Iterable<CompletionSuggestion> suggestions(
+ YamlCompletionRequest request) sync* {
+ final cachedPackages = request.pubPackageService.cachedPackages;
+ var relevance = cachedPackages.length;
+ yield* cachedPackages.map((package) =>
+ packageName('${package.packageName}: ', relevance: relevance--));
+ }
+}
+
/// A completion generator that can produce completion suggestions for pubspec
/// files.
class PubspecGenerator extends YamlCompletionGenerator {
@@ -24,8 +40,8 @@
'flutter': EmptyProducer(),
'sdk': EmptyProducer(),
}),
- 'dependencies': EmptyProducer(),
- 'dev_dependencies': EmptyProducer(),
+ 'dependencies': PubPackageNameProducer(),
+ 'dev_dependencies': PubPackageNameProducer(),
// TODO(brianwilkerson) Suggest names already listed under 'dependencies'
// and 'dev_dependencies'.
'dependency_overrides': EmptyProducer(),
@@ -78,7 +94,9 @@
});
/// Initialize a newly created suggestion generator for pubspec files.
- PubspecGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+ PubspecGenerator(
+ ResourceProvider resourceProvider, PubPackageService pubPackageService)
+ : super(resourceProvider, pubPackageService);
@override
Producer get topLevelProducer => pubspecProducer;
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
index 2746a76..cfa4242e 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
@@ -4,6 +4,7 @@
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analysis_server/src/utilities/extensions/yaml.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:yaml/yaml.dart';
@@ -15,9 +16,13 @@
/// completion was requested.
final ResourceProvider resourceProvider;
+ /// A service used for collecting Pub package information. May be null for
+ /// generators that do not use Pub packages.
+ final PubPackageService pubPackageService;
+
/// Initialize a newly created generator to use the [resourceProvider] to
/// access the content of the file in which completion was requested.
- YamlCompletionGenerator(this.resourceProvider);
+ YamlCompletionGenerator(this.resourceProvider, this.pubPackageService);
/// Return the producer used to produce suggestions at the top-level of the
/// file.
@@ -56,7 +61,8 @@
var request = YamlCompletionRequest(
filePath: filePath,
precedingText: precedingText,
- resourceProvider: resourceProvider);
+ resourceProvider: resourceProvider,
+ pubPackageService: pubPackageService);
return getSuggestionsForPath(request, nodePath, offset);
}
@@ -87,7 +93,7 @@
/// Return the result of parsing the file [content] into a YAML node.
YamlNode _parseYaml(String content) {
try {
- return loadYamlNode(content);
+ return loadYamlNode(content, recover: true);
} on YamlException {
// If the file can't be parsed, then fall through to return `null`.
}
diff --git a/pkg/analysis_server/lib/src/services/pub/pub_api.dart b/pkg/analysis_server/lib/src/services/pub/pub_api.dart
new file mode 100644
index 0000000..7f43aa0
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/pub/pub_api.dart
@@ -0,0 +1,147 @@
+// 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:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// A class for interacting with the Pub API.
+///
+/// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md
+///
+/// Failed requests will automatically be retried.
+class PubApi {
+ static const packageNameListPath = '/api/package-name-completion-data';
+
+ /// Maximum number of retries if requests fail.
+ static const maxFailedRequests = 5;
+
+ /// Initial wait period between retries. Doubled for each failure (but restarts
+ /// from this value for each new request).
+ static int _failedRetryInitialDelaySeconds = 1;
+
+ @visibleForTesting
+ static set failedRetryInitialDelaySeconds(int value) {
+ _failedRetryInitialDelaySeconds = value;
+ }
+
+ final InstrumentationService instrumentationService;
+ final http.Client httpClient;
+ final String _pubHostedUrl;
+ final _headers = {
+ 'Accept': 'application/vnd.pub.v2+json',
+ 'Accept-Encoding': 'gzip',
+ 'User-Agent': 'Dart Analysis Server/${Platform.version.split(' ').first}'
+ ' (+https://github.com/dart-lang/sdk)',
+ };
+
+ PubApi(this.instrumentationService, http.Client httpClient,
+ String envPubHostedUrl)
+ : httpClient =
+ httpClient != null ? _NoCloseHttpClient(httpClient) : http.Client(),
+ _pubHostedUrl = _validPubHostedUrl(envPubHostedUrl);
+
+ /// Fetches a list of package names from the Pub API.
+ ///
+ /// Failed requests will be retried a number of times. If no successful response
+ /// is received, will return null.
+ Future<List<PubApiPackage>> allPackages() async {
+ final json = await _getJson('$_pubHostedUrl$packageNameListPath');
+ if (json == null) {
+ return null;
+ }
+
+ final packageNames = json['packages'];
+ return packageNames is List
+ ? packageNames.map((name) => PubApiPackage(name)).toList()
+ : null;
+ }
+
+ void close() {
+ httpClient.close();
+ }
+
+ /// Calls a pub API and decodes the resulting JSON.
+ ///
+ /// Automatically retries the request for specific types of failures after
+ /// [_failedRetryInitialDelaySeconds] doubling each time. After [maxFailedRequests]
+ /// requests or upon a 4XX response, will return `null` and not retry.
+ Future<Map<String, dynamic>> _getJson(String url) async {
+ var requestCount = 0;
+ var retryAfterSeconds = _failedRetryInitialDelaySeconds;
+ while (requestCount++ < maxFailedRequests) {
+ try {
+ final response =
+ await httpClient.get(Uri.parse(url), headers: _headers);
+ if (response.statusCode == 200) {
+ instrumentationService.logInfo('Pub API request successful for $url');
+ return jsonDecode(response.body);
+ } else if (response.statusCode >= 400 && response.statusCode < 500) {
+ // Do not retry 4xx responses.
+ instrumentationService.logError(
+ 'Pub API returned ${response.statusCode} ${response.reasonPhrase} '
+ 'for $url. Not retrying.');
+ return null;
+ }
+ instrumentationService.logError(
+ 'Pub API returned ${response.statusCode} ${response.reasonPhrase} '
+ 'for $url on attempt $requestCount');
+ } catch (e) {
+ if (e is! IOException && e is! FormatException) {
+ instrumentationService
+ .logError('Error calling pub API for $url. Not retrying. $e');
+ return null;
+ }
+ instrumentationService.logError('Error calling pub API for $url: $e');
+ }
+ if (requestCount >= maxFailedRequests) {
+ instrumentationService
+ .logInfo('Pub API request failed after $requestCount requests');
+ } else {
+ // Sleep before the next try.
+ await Future.delayed(Duration(seconds: retryAfterSeconds));
+ retryAfterSeconds *= 2;
+ }
+ }
+ return null;
+ }
+
+ /// Returns a valid Pub base URL from [envPubHostedUrl] if valid, otherwise using
+ /// the default 'https://pub.dartlang.org'.
+ static String _validPubHostedUrl(String envPubHostedUrl) {
+ final validUrl = envPubHostedUrl != null &&
+ (Uri.tryParse(envPubHostedUrl)?.isAbsolute ?? false)
+ ? envPubHostedUrl
+ : 'https://pub.dartlang.org';
+
+ // Discard any trailing slashes, as all API paths start with them.
+ return validUrl.endsWith('/')
+ ? validUrl.substring(0, validUrl.length - 1)
+ : validUrl;
+ }
+}
+
+class PubApiPackage {
+ final String packageName;
+
+ PubApiPackage(this.packageName);
+}
+
+/// A wrapper over a package:http Client that does not pass on calls to [close].
+///
+/// This is used to prevent the server closing a client that may be provided to
+/// it (while still allowing it to close any client it creates itself).
+class _NoCloseHttpClient extends http.BaseClient {
+ final http.Client client;
+
+ _NoCloseHttpClient(this.client);
+
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) =>
+ client.send(request);
+}
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
new file mode 100644
index 0000000..aca37e4
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
@@ -0,0 +1,173 @@
+// 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 'package:analysis_server/src/services/pub/pub_api.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';
+
+/// Information about Pub packages that can be converted to/from JSON and
+/// cached to disk.
+class PackageDetailsCache {
+ static const cacheVersion = 2;
+ static const maxCacheAge = Duration(hours: 18);
+ final Map<String, PubPackage> packages;
+ DateTime lastUpdatedUtc;
+
+ PackageDetailsCache._(this.packages, DateTime lastUpdated)
+ : lastUpdatedUtc = lastUpdated.toUtc();
+
+ Duration get cacheTimeRemaining {
+ final cacheAge = DateTime.now().toUtc().difference(lastUpdatedUtc);
+ final cacheTimeRemaining = maxCacheAge - cacheAge;
+ return cacheTimeRemaining < Duration.zero
+ ? Duration.zero
+ : cacheTimeRemaining;
+ }
+
+ Map<String, Object> toJson() {
+ return {
+ 'version': cacheVersion,
+ 'lastUpdated': lastUpdatedUtc.toIso8601String(),
+ 'packages': packages.values.toList(),
+ };
+ }
+
+ static PackageDetailsCache empty() {
+ return PackageDetailsCache._({}, DateTime.utc(2000));
+ }
+
+ static PackageDetailsCache fromApiResults(List<PubApiPackage> apiPackages) {
+ final packages = Map.fromEntries(apiPackages.map((package) =>
+ MapEntry(package.packageName, PubPackage.fromName(package))));
+
+ return PackageDetailsCache._(packages, DateTime.now().toUtc());
+ }
+
+ /// Deserialises cached package data from JSON.
+ ///
+ /// If the JSON version does not match the current version, will return null.
+ static PackageDetailsCache fromJson(Map<String, Object> json) {
+ if (json['version'] != cacheVersion) {
+ return null;
+ }
+
+ final packagesJson = json['packages'] as List<Object>;
+ final packages = packagesJson.map((json) => PubPackage.fromJson(json));
+ final packageMap = Map.fromEntries(
+ packages.map((package) => MapEntry(package.packageName, package)));
+ return PackageDetailsCache._(
+ packageMap, DateTime.parse(json['lastUpdated']));
+ }
+}
+
+/// Information about a single Pub package.
+class PubPackage {
+ String packageName;
+
+ PubPackage.fromJson(Map<String, Object> json)
+ : packageName = json['packageName'];
+
+ PubPackage.fromName(PubApiPackage package)
+ : packageName = package.packageName;
+
+ Map<String, Object> toJson() {
+ return {
+ if (packageName != null) 'packageName': packageName,
+ };
+ }
+}
+
+/// A service for providing Pub package information.
+///
+/// Uses a [PubApi] to communicate with Pub and caches to disk using [cacheResourceProvider].
+class PubPackageService {
+ final InstrumentationService _instrumentationService;
+ final PubApi _api;
+ Timer _nextRequestTimer;
+
+ /// [ResourceProvider] used for caching. This should generally be a
+ /// [PhysicalResourceProvider] outside of tests.
+ final ResourceProvider cacheResourceProvider;
+
+ /// The current cache of package information. Initiailly null, but overwritten
+ /// after first read of cache from disk or fetch from the API.
+ @visibleForTesting
+ PackageDetailsCache packageCache;
+
+ PubPackageService(
+ this._instrumentationService, this.cacheResourceProvider, this._api);
+
+ /// Gets the last set of package results or an empty List if no results.
+ List<PubPackage> get cachedPackages =>
+ packageCache?.packages?.values?.toList() ?? [];
+
+ bool get isRunning => _nextRequestTimer != null;
+
+ @visibleForTesting
+ File get packageCacheFile {
+ final cacheFolder = cacheResourceProvider
+ .getStateLocation('.pub-package-details-cache')
+ ..create();
+ return cacheFolder.getChildAssumingFile('packages.json');
+ }
+
+ /// Begin a request to pre-load the package name list.
+ void beginPackageNamePreload() {
+ // If first time, try to read from disk.
+ packageCache ??= readDiskCache() ?? PackageDetailsCache.empty();
+
+ // If there is no queued request, initialize one when the current cache expires.
+ _nextRequestTimer ??=
+ Timer(packageCache.cacheTimeRemaining, _fetchFromServer);
+ }
+
+ PubPackage cachedPackageDetails(String packageName) =>
+ packageCache.packages[packageName];
+
+ @visibleForTesting
+ PackageDetailsCache readDiskCache() {
+ final file = packageCacheFile;
+ if (!file.exists) {
+ return null;
+ }
+ try {
+ final contents = file.readAsStringSync();
+ final json = jsonDecode(contents) as Map<String, Object>;
+ return PackageDetailsCache.fromJson(json);
+ } catch (e) {
+ _instrumentationService.logError('Error reading pub cache file: $e');
+ return null;
+ }
+ }
+
+ void shutdown() => _nextRequestTimer?.cancel();
+
+ @visibleForTesting
+ void writeDiskCache(PackageDetailsCache cache) {
+ final file = packageCacheFile;
+ file.writeAsStringSync(jsonEncode(cache.toJson()));
+ }
+
+ Future<void> _fetchFromServer() async {
+ try {
+ final packages = await _api.allPackages();
+ if (packages == null) {
+ // If we never got a valid response, just skip until the next refresh.
+ return;
+ }
+ packageCache = PackageDetailsCache.fromApiResults(packages);
+ writeDiskCache(packageCache);
+ } catch (e) {
+ _instrumentationService.logError('Failed to fetch packages from Pub: $e');
+ } finally {
+ _nextRequestTimer =
+ Timer(PackageDetailsCache.maxCacheAge, _fetchFromServer);
+ }
+ }
+}
diff --git a/pkg/analysis_server/pubspec.yaml b/pkg/analysis_server/pubspec.yaml
index ef86eeb..855f489 100644
--- a/pkg/analysis_server/pubspec.yaml
+++ b/pkg/analysis_server/pubspec.yaml
@@ -18,6 +18,7 @@
convert: any
crypto: any
dart_style: any
+ http: any
html: any
intl: any
linter: any
@@ -34,7 +35,6 @@
dev_dependencies:
analyzer_utilities:
path: ../analyzer_utilities
- http: any
logging: any
matcher: any
mockito: any
diff --git a/pkg/analysis_server/test/lsp/completion.dart b/pkg/analysis_server/test/lsp/completion.dart
index 194f9fd..f1f5b9a 100644
--- a/pkg/analysis_server/test/lsp/completion.dart
+++ b/pkg/analysis_server/test/lsp/completion.dart
@@ -8,11 +8,15 @@
import 'server_abstract.dart';
mixin CompletionTestMixin on AbstractLspAnalysisServerTest {
+ int sortTextSorter(CompletionItem item1, CompletionItem item2) =>
+ (item1.sortText ?? item1.label).compareTo(item2.sortText ?? item2.label);
+
Future<void> verifyCompletions(
Uri fileUri,
String content, {
List<String> expectCompletions,
- String verifyEditsFor,
+ String applyEditsFor,
+ bool resolve = false,
String expectedContent,
String expectedContentIfInserting,
bool verifyInsertReplaceRanges = false,
@@ -36,20 +40,24 @@
final res = await getCompletion(fileUri, positionFromMarker(content));
await closeFile(fileUri);
- for (final expectedCompletion in expectCompletions) {
- expect(
- res.any((c) => c.label == expectedCompletion),
- isTrue,
- reason:
- '"$expectedCompletion" was not in ${res.map((c) => '"${c.label}"')}',
- );
- }
+ // Sort the completions by sortText and filter to those we expect, so the ordering
+ // can be compared.
+ final sortedResults = res
+ .where((r) => expectCompletions.contains(r.label))
+ .toList()
+ ..sort(sortTextSorter);
+
+ expect(sortedResults.map((item) => item.label), equals(expectCompletions));
// Check the edits apply correctly.
- if (verifyEditsFor != null) {
- final item = res.singleWhere((c) => c.label == verifyEditsFor);
+ if (applyEditsFor != null) {
+ var item = res.singleWhere((c) => c.label == applyEditsFor);
final insertFormat = item.insertTextFormat;
+ if (resolve) {
+ item = await resolveCompletion(item);
+ }
+
if (verifyInsertReplaceRanges &&
expectedContent != expectedContentIfInserting) {
// Replacing.
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index d69a171..0c13828 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -682,7 +682,7 @@
mainFileUri,
content,
expectCompletions: [expectedLabel],
- verifyEditsFor: expectedLabel,
+ applyEditsFor: expectedLabel,
verifyInsertReplaceRanges: true,
expectedContent: expectedReplaced,
expectedContentIfInserting: expectedInserted,
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index 9d9711d..9a9da49 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 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:http/http.dart';
import 'package:linter/src/rules.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -43,7 +45,7 @@
'always_declare_return_types',
'annotate_overrides',
],
- verifyEditsFor: 'annotate_overrides',
+ applyEditsFor: 'annotate_overrides',
expectedContent: expected,
);
}
@@ -63,7 +65,7 @@
analysisOptionsUri,
content,
expectCompletions: ['annotate_overrides'],
- verifyEditsFor: 'annotate_overrides',
+ applyEditsFor: 'annotate_overrides',
expectedContent: expected,
);
}
@@ -78,7 +80,7 @@
analysisOptionsUri,
content,
expectCompletions: ['linter: '],
- verifyEditsFor: 'linter: ',
+ applyEditsFor: 'linter: ',
expectedContent: expected,
);
}
@@ -93,7 +95,7 @@
analysisOptionsUri,
content,
expectCompletions: ['linter: '],
- verifyEditsFor: 'linter: ',
+ applyEditsFor: 'linter: ',
expectedContent: expected,
);
}
@@ -126,7 +128,7 @@
fixDataUri,
content,
expectCompletions: ['kind: '],
- verifyEditsFor: 'kind: ',
+ applyEditsFor: 'kind: ',
expectedContent: expected,
);
}
@@ -147,7 +149,7 @@
fixDataUri,
content,
expectCompletions: ['kind: '],
- verifyEditsFor: 'kind: ',
+ applyEditsFor: 'kind: ',
expectedContent: expected,
);
}
@@ -164,7 +166,7 @@
fixDataUri,
content,
expectCompletions: ['transforms:'],
- verifyEditsFor: 'transforms:',
+ applyEditsFor: 'transforms:',
expectedContent: expected,
);
}
@@ -179,7 +181,7 @@
fixDataUri,
content,
expectCompletions: ['transforms:'],
- verifyEditsFor: 'transforms:',
+ applyEditsFor: 'transforms:',
expectedContent: expected,
);
}
@@ -188,6 +190,29 @@
@reflectiveTest
class PubspecCompletionTest extends AbstractLspAnalysisServerTest
with CompletionTestMixin {
+ static const samplePackageList = '''
+ { "packages": ["one", "two", "three"] }
+ ''';
+
+ static const samplePackageDetails = '''
+ {
+ "name":"package",
+ "latest":{
+ "version":"1.2.3",
+ "pubspec":{
+ "description":"Description of package"
+ }
+ }
+ }
+ ''';
+
+ @override
+ void setUp() {
+ super.setUp();
+ // Cause retries to run immediately.
+ PubApi.failedRetryInitialDelaySeconds = 0;
+ }
+
Future<void> test_insertReplaceRanges() async {
final content = '''
name: foo
@@ -215,7 +240,7 @@
pubspecFileUri,
content,
expectCompletions: ['sdk: '],
- verifyEditsFor: 'sdk: ',
+ applyEditsFor: 'sdk: ',
verifyInsertReplaceRanges: true,
expectedContent: expectedReplaced,
expectedContentIfInserting: expectedInserted,
@@ -241,7 +266,7 @@
pubspecFileUri,
content,
expectCompletions: ['flutter: ', 'sdk: '],
- verifyEditsFor: 'sdk: ',
+ applyEditsFor: 'sdk: ',
expectedContent: expected,
);
}
@@ -265,7 +290,39 @@
pubspecFileUri,
content,
expectCompletions: ['flutter: ', 'sdk: '],
- verifyEditsFor: 'sdk: ',
+ applyEditsFor: 'sdk: ',
+ expectedContent: expected,
+ );
+ }
+
+ Future<void> test_package_names() async {
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ ^''';
+
+ final expected = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ one: ''';
+
+ await verifyCompletions(
+ pubspecFileUri,
+ content,
+ expectCompletions: ['one: ', 'two: ', 'three: '],
+ applyEditsFor: 'one: ',
expectedContent: expected,
);
}
@@ -282,7 +339,7 @@
pubspecFileUri,
content,
expectCompletions: ['name: ', 'description: '],
- verifyEditsFor: 'name: ',
+ applyEditsFor: 'name: ',
expectedContent: expected,
);
}
@@ -297,7 +354,7 @@
pubspecFileUri,
content,
expectCompletions: ['name: ', 'description: '],
- verifyEditsFor: 'name: ',
+ applyEditsFor: 'name: ',
expectedContent: expected,
);
}
diff --git a/pkg/analysis_server/test/lsp/pub_package_service_test.dart b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
new file mode 100644
index 0000000..f76aaa9
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
@@ -0,0 +1,309 @@
+// 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 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../mocks.dart';
+import 'server_abstract.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(PubApiTest);
+ defineReflectiveTests(PubPackageServiceTest);
+ });
+}
+
+@reflectiveTest
+class PubApiTest {
+ static const pubDefaultUrl = 'https://pub.dartlang.org';
+
+ Uri lastCalledUrl;
+ MockHttpClient httpClient;
+
+ PubApi api;
+
+ Future<void> check_pubHostedUrl(String envValue, String expectedBase) async {
+ final api =
+ PubApi(InstrumentationService.NULL_SERVICE, httpClient, envValue);
+ await api.allPackages();
+ expect(lastCalledUrl.toString(),
+ equals('$expectedBase/api/package-name-completion-data'));
+ }
+
+ void setUp() {
+ httpClient = MockHttpClient();
+ lastCalledUrl = null;
+ httpClient.sendHandler = (BaseRequest request) async {
+ lastCalledUrl = request.url;
+ return Response('{}', 200);
+ };
+ }
+
+ Future<void> test_envPubHostedUrl_emptyString() =>
+ check_pubHostedUrl('', pubDefaultUrl);
+
+ Future<void> test_envPubHostedUrl_invalidUrl() =>
+ check_pubHostedUrl('test', pubDefaultUrl);
+
+ Future<void> test_envPubHostedUrl_missingScheme() =>
+ // It's hard to tell that this is intended to be a valid URL minus the scheme
+ // so it will fail validation and fall back to the default.
+ check_pubHostedUrl('pub.example.org', pubDefaultUrl);
+
+ Future<void> test_envPubHostedUrl_null() =>
+ check_pubHostedUrl(null, pubDefaultUrl);
+
+ Future<void> test_envPubHostedUrl_valid() =>
+ check_pubHostedUrl('https://pub.example.org', 'https://pub.example.org');
+
+ Future<void> test_envPubHostedUrl_validTrailingSlash() =>
+ check_pubHostedUrl('https://pub.example.org/', 'https://pub.example.org');
+
+ Future<void> test_httpClient_closesOwn() async {
+ final api = PubApi(InstrumentationService.NULL_SERVICE, null, null);
+ api.close();
+ expect(() => api.httpClient.get(Uri.parse('https://www.google.co.uk/')),
+ throwsA(anything));
+ }
+
+ Future<void> test_httpClient_doesNotCloseProvided() async {
+ final api = PubApi(InstrumentationService.NULL_SERVICE, httpClient, null);
+ api.close();
+ expect(httpClient.wasClosed, isFalse);
+ }
+}
+
+@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.
+ static const samplePackageList = '''
+ { "packages": ["one", "two", "three"] }
+ ''';
+
+ void expectPackages(List<String> packageNames) => expect(
+ server.pubPackageService.cachedPackages.map((p) => p.packageName),
+ equals(packageNames));
+
+ void providePubApiPackageList(String jsonResponse) {
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return Response(jsonResponse, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+ }
+
+ @override
+ void setUp() {
+ super.setUp();
+ // Cause retries to run immediately.
+ PubApi.failedRetryInitialDelaySeconds = 0;
+ }
+
+ Future<void> test_packageCache_diskCacheReplacedIfStale() async {
+ // This test should mirror test_packageCache_diskCacheUsedIfFresh
+ // besides the lastUpdated timestamp + expectations.
+ providePubApiPackageList(samplePackageList);
+
+ // Write a cache that should be considered stale.
+ server.pubPackageService.writeDiskCache(
+ PackageDetailsCache.fromApiResults([PubApiPackage('stalePackage1')])
+ ..lastUpdatedUtc = DateTime.utc(1990),
+ );
+
+ await initialize();
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ // Expect the sample list to have overwritten the stale cache.
+ expectPackages(['one', 'two', 'three']);
+ }
+
+ Future<void> test_packageCache_diskCacheUsedIfFresh() async {
+ // This test should mirror test_packageCache_diskCacheReplacedIfStale
+ // besides the lastUpdated timestamp + expectations.
+ providePubApiPackageList(samplePackageList);
+
+ // Write a cache that is not stale.
+ server.pubPackageService.writeDiskCache(
+ PackageDetailsCache.fromApiResults([PubApiPackage('freshPackage1')]));
+
+ await initialize();
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ // Expect the fresh cache to still be used.
+ expectPackages(['freshPackage1']);
+ }
+
+ Future<void> test_packageCache_doesNotRetryOn400() async {
+ var requestNum = 1;
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return requestNum++ == 1
+ ? Response('ERROR', 400)
+ : Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages([]);
+ expect(httpClient.sendHandlerCalls, equals(1));
+ }
+
+ Future<void> test_packageCache_doesNotRetryUnknownException() async {
+ httpClient.sendHandler =
+ (BaseRequest request) async => throw UnimplementedError();
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages([]);
+ expect(httpClient.sendHandlerCalls, equals(1));
+ }
+
+ Future<void> test_packageCache_fetchesFromServer() async {
+ // Provide the sample packages in the web request.
+ providePubApiPackageList(samplePackageList);
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages(['one', 'two', 'three']);
+ }
+
+ Future<void> test_packageCache_initializesOnPubspecOpen() async {
+ await initialize();
+
+ expect(server.pubPackageService.isRunning, isFalse);
+ expect(server.pubPackageService.packageCache, isNull);
+ expectPackages([]);
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expect(server.pubPackageService.isRunning, isTrue);
+ expect(server.pubPackageService.packageCache, isNotNull);
+ expectPackages([]);
+ }
+
+ Future<void> test_packageCache_readsDiskCache() async {
+ server.pubPackageService.writeDiskCache(
+ PackageDetailsCache.fromApiResults([PubApiPackage('package1')]));
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages(['package1']);
+ }
+
+ Future<void> test_packageCache_retriesOn500() async {
+ var requestNum = 1;
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return requestNum++ == 1
+ ? Response('ERROR', 500)
+ : Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages(['one', 'two', 'three']);
+ expect(httpClient.sendHandlerCalls, equals(2));
+ }
+
+ Future<void> test_packageCache_retriesOnInvalidJson() async {
+ var requestNum = 1;
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return requestNum++ == 1
+ ? Response('$samplePackageList{{{{{{{', 200)
+ : Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ await initialize();
+ expectPackages([]);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ expectPackages(['one', 'two', 'three']);
+ expect(httpClient.sendHandlerCalls, equals(2));
+ }
+
+ Future<void> test_packageCache_timeRemaining() async {
+ void expectHoursRemaining(DateTime cacheTime, int expectedHoursRemaining) {
+ final cache = PackageDetailsCache.empty();
+ cache.lastUpdatedUtc = cacheTime.toUtc();
+
+ final remainingHours = cache.cacheTimeRemaining.inHours;
+ expect(remainingHours, isNonNegative);
+ expect(remainingHours, closeTo(expectedHoursRemaining, 1));
+ }
+
+ final maxHours = PackageDetailsCache.maxCacheAge.inHours;
+
+ // Very old cache should have no time remaining.
+ expectHoursRemaining(DateTime(2020, 12, 1), 0);
+
+ // Cache from 1 hour ago should max-1 hours remaining.
+ expectHoursRemaining(DateTime.now().add(Duration(hours: -1)), maxHours - 1);
+
+ // Cache from 10 hours ago should max-10 hours remaining.
+ expectHoursRemaining(
+ DateTime.now().add(Duration(hours: -10)), maxHours - 10);
+
+ // Cache from maxAge ago should have no hours remaining.
+ expectHoursRemaining(
+ DateTime.now().add(-PackageDetailsCache.maxCacheAge), 0);
+ }
+
+ Future<void> test_packageCache_writesDiskCache() async {
+ // Provide the sample packages in the web request.
+ providePubApiPackageList(samplePackageList);
+
+ await initialize();
+ expect(server.pubPackageService.readDiskCache(), isNull);
+
+ await openFile(pubspecFileUri, '');
+ await pumpEventQueue();
+
+ final cache = server.pubPackageService.readDiskCache();
+ final packages = cache.packages.values.toList();
+
+ expect(packages.map((p) => p.packageName), equals(['one', 'two', 'three']));
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index bc85ae8..5ad6556 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -51,6 +51,7 @@
MockLspServerChannel channel;
TestPluginManager pluginManager;
LspAnalysisServer server;
+ MockHttpClient httpClient;
AnalysisServerOptions get serverOptions => AnalysisServerOptions();
@@ -119,6 +120,7 @@
}
void setUp() {
+ httpClient = MockHttpClient();
channel = MockLspServerChannel(debugPrintCommunication);
// Create an SDK in the mock file system.
MockSdk(resourceProvider: resourceProvider);
@@ -129,7 +131,8 @@
serverOptions,
DartSdkManager(convertPath('/sdk')),
CrashReportingAttachmentsBuilder.empty,
- InstrumentationService.NULL_SERVICE);
+ InstrumentationService.NULL_SERVICE,
+ httpClient: httpClient);
server.pluginManager = pluginManager;
projectFolderPath = convertPath('/home/test');
@@ -1064,6 +1067,20 @@
request, _fromJsonList(Location.fromJson));
}
+ Future<CompletionItem> getResolvedCompletion(
+ Uri uri,
+ Position pos,
+ String label, {
+ CompletionContext context,
+ }) async {
+ final completions = await getCompletion(uri, pos, context: context);
+
+ final completion = completions.singleWhere((c) => c.label == label);
+ expect(completion, isNotNull);
+
+ return resolveCompletion(completion);
+ }
+
Future<SemanticTokens> getSemanticTokens(Uri uri) {
final request = makeRequest(
Method.textDocument_semanticTokens_full,
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index aa30754..dd27eb3 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -31,6 +31,7 @@
import 'mapping_test.dart' as mapping;
import 'outline_test.dart' as outline;
import 'priority_files_test.dart' as priority_files;
+import 'pub_package_service_test.dart' as pub_package_service;
import 'reanalyze_test.dart' as reanalyze;
import 'references_test.dart' as references;
import 'rename_test.dart' as rename;
@@ -71,6 +72,7 @@
mapping.main();
outline.main();
priority_files.main();
+ pub_package_service.main();
reanalyze.main();
references.main();
rename.main();
diff --git a/pkg/analysis_server/test/mocks.dart b/pkg/analysis_server/test/mocks.dart
index c166c2c..d8c9e0a 100644
--- a/pkg/analysis_server/test/mocks.dart
+++ b/pkg/analysis_server/test/mocks.dart
@@ -13,6 +13,7 @@
import 'package:analysis_server/src/lsp/channel/lsp_channel.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/timestamped_data.dart';
+import 'package:http/http.dart' as http;
import 'package:test/test.dart';
const _jsonEncoder = JsonEncoder.withIndent(' ');
@@ -26,6 +27,34 @@
/// and no error.
Matcher isResponseSuccess(String id) => _IsResponseSuccess(id);
+class MockHttpClient extends http.BaseClient {
+ Future<http.Response> Function(http.BaseRequest request) sendHandler;
+ int sendHandlerCalls = 0;
+ bool wasClosed = false;
+
+ @override
+ void close() {
+ wasClosed = true;
+ }
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) {
+ return super.noSuchMethod(invocation);
+ }
+
+ @override
+ Future<http.StreamedResponse> send(http.BaseRequest request) {
+ if (wasClosed) {
+ throw Exception('get() called after close()');
+ }
+
+ return sendHandler(request)
+ .then((resp) => http.StreamedResponse(
+ Stream.value(resp.body.codeUnits), resp.statusCode))
+ .whenComplete(() => sendHandlerCalls++);
+ }
+}
+
/// A mock [LspServerCommunicationChannel] for testing [LspAnalysisServer].
class MockLspServerChannel implements LspServerCommunicationChannel {
final StreamController<lsp.Message> _clientToServer =
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 b00f1d4..acfead8 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
@@ -3,8 +3,14 @@
// BSD-style license that can be found in the LICENSE file.
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_package_service.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
+import '../../../../mocks.dart';
import 'yaml_generator_test_support.dart';
void main() {
@@ -15,11 +21,27 @@
@reflectiveTest
class PubspecGeneratorTest extends YamlGeneratorTest {
+ MockHttpClient httpClient;
+
+ PubPackageService pubPackageService;
@override
String get fileName => 'pubspec.yaml';
@override
- PubspecGenerator get generator => PubspecGenerator(resourceProvider);
+ PubspecGenerator get generator =>
+ PubspecGenerator(resourceProvider, pubPackageService);
+
+ void setUp() {
+ httpClient = MockHttpClient();
+ pubPackageService = PubPackageService(
+ InstrumentationService.NULL_SERVICE,
+ resourceProvider,
+ PubApi(InstrumentationService.NULL_SERVICE, httpClient, null));
+ }
+
+ void tearDown() {
+ pubPackageService.shutdown();
+ }
void test_empty() {
getCompletions('^');
@@ -254,4 +276,53 @@
''');
assertSuggestion('true');
}
+
+ void test_packageName() async {
+ const samplePackageList = '''
+ { "packages": ["one", "two", "three"] }
+ ''';
+
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ pubPackageService.beginPackageNamePreload();
+ await pumpEventQueue();
+
+ getCompletions('''
+dependencies:
+ ^
+''');
+ assertSuggestion('one: ');
+ assertSuggestion('two: ');
+ }
+
+ void test_packageName_invalidYaml() async {
+ const samplePackageList = '''
+ { "packages": ["one", "two", "three"] }
+ ''';
+
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ pubPackageService.beginPackageNamePreload();
+ await pumpEventQueue();
+
+ getCompletions('''
+dependencies:
+ one:
+ tw^
+ three:
+''');
+ assertSuggestion('two: ');
+ }
}
diff --git a/tools/VERSION b/tools/VERSION
index 26d7750..176ee19 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 13
PATCH 0
-PRERELEASE 100
+PRERELEASE 101
PRERELEASE_PATCH 0
\ No newline at end of file