diff --git a/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart b/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart index 42497c9f..b5912747 100644 --- a/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart +++ b/packages/custom_lint/lib/src/v2/custom_lint_analyzer_plugin.dart @@ -11,6 +11,7 @@ import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' show ResponseResult; import 'package:async/async.dart'; import 'package:custom_lint_core/custom_lint_core.dart'; +import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; @@ -114,6 +115,11 @@ class CustomLintServer { /// Whether plugins should include lints used for debugging. final bool includeBuiltInLints; + /// Allow mocking a SocketCustomLintServerToClientChannel for test + @visibleForTesting + Future get clientChannel => + _clientChannel.safeFirst; + late final StreamSubscription _requestSubscription; StreamSubscription? _clientChannelEventsSubscription; late PluginVersionCheckParams _pluginVersionCheckParams; @@ -122,6 +128,7 @@ class CustomLintServer { BehaviorSubject(); final _contextRoots = BehaviorSubject(); final _runner = PendingOperation(); + final _delayedRequest = []; /// A shorthand for accessing the current list of context roots. Future?> get _allContextRoots { @@ -186,7 +193,10 @@ class CustomLintServer { orElse: () async { return _runner.run(() async { final clientChannel = await _clientChannel.safeFirst; - if (clientChannel == null) return null; + if (clientChannel == null || !clientChannel.initialized) { + _delayedRequest.add(request); + return null; + } final response = await clientChannel.sendAnalyzerPluginRequest(request); @@ -291,6 +301,7 @@ class CustomLintServer { return _closeFuture = Future(() async { // Cancel pending operations await _contextRoots.close(); + _delayedRequest.clear(); // Flushes logs before stopping server. await _runner.wait(); @@ -396,6 +407,14 @@ class CustomLintServer { await clientChannel.init( debug: configs.any((e) => e != null && e.debug), ); + _sendDelayedRequest(); + } + + void _sendDelayedRequest() { + for (final request in _delayedRequest) { + unawaited(_handleRequest(request)); + } + _delayedRequest.clear(); } Future _handleEvent(CustomLintEvent event) => _runner.run(() async { diff --git a/packages/custom_lint/lib/src/v2/server_to_client_channel.dart b/packages/custom_lint/lib/src/v2/server_to_client_channel.dart index 769655c9..2163bb86 100644 --- a/packages/custom_lint/lib/src/v2/server_to_client_channel.dart +++ b/packages/custom_lint/lib/src/v2/server_to_client_channel.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart'; import 'package:uuid/uuid.dart'; @@ -81,6 +82,16 @@ class SocketCustomLintServerToClientChannel { final CustomLintWorkspace _workspace; AnalysisSetContextRootsParams _contextRoots; + bool _initialized = false; + + /// List request before initialized + @visibleForTesting + List initialRequest = []; + + /// Initial state + /// + /// Returns `true` if requested `analysis.setContextRoots` + bool get initialized => _initialized; late final Stream _messages = _channel.messages .map((e) => e! as Map) @@ -127,6 +138,7 @@ class SocketCustomLintServerToClientChannel { sendAnalyzerPluginRequest(_version.toRequest(const Uuid().v4())), sendAnalyzerPluginRequest(_contextRoots.toRequest(const Uuid().v4())), ]); + _initialized = true; } /// Updates the context roots on the client @@ -231,6 +243,7 @@ void main(List args) async { /// Sends a request based on the analyzer_plugin protocol, expecting /// an analyzer_plugin response. Future sendAnalyzerPluginRequest(Request request) async { + if (!_initialized) initialRequest.add(request); final response = await sendCustomLintRequest( CustomLintRequest.analyzerPluginRequest(request, id: request.id), ); diff --git a/packages/custom_lint/test/run_plugin.dart b/packages/custom_lint/test/run_plugin.dart index b9382ea4..eb3c574b 100644 --- a/packages/custom_lint/test/run_plugin.dart +++ b/packages/custom_lint/test/run_plugin.dart @@ -20,10 +20,11 @@ Future> runServerInCliModeForApp( } class ManualRunner { - ManualRunner(this.runner, this.channel); + ManualRunner(this.runner, this.channel, this.server); final CustomLintRunner runner; final ServerIsolateChannel channel; + final CustomLintServer server; Future get initialize => runner.initialize; @@ -91,7 +92,7 @@ Future startRunnerForApp( unawaited(runner.initialize); - return ManualRunner(runner, channel); + return ManualRunner(runner, channel, customLintServer); }); } diff --git a/packages/custom_lint/test/server_test.dart b/packages/custom_lint/test/server_test.dart index 41ef8515..f7b04821 100644 --- a/packages/custom_lint/test/server_test.dart +++ b/packages/custom_lint/test/server_test.dart @@ -71,6 +71,44 @@ void fn() {} ); }); + test('Handles request before server initialized', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + + final app = createLintUsage( + source: { + 'lib/main.dart': ''' +void fn() {} + +void fn2() {} +''', + 'lib/another.dart': 'void fn() {}\n', + }, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + final runner = await startRunnerForApp(app, includeBuiltInLints: false); + final another = File(join(app.path, 'lib', 'another.dart')); + another.writeAsStringSync('test'); + await runner.channel.sendRequest( + AnalysisUpdateContentParams({ + another.path: AddContentOverlay(another.readAsStringSync()), + }), + ); + await runner.initialize; + + final initialRequest = (await runner.server.clientChannel)?.initialRequest; + // Request send before `SocketCustomLintServerToClientChannel.init` must delay, + // for ensure initialRequest is [PluginVersionCheckParams, AnalysisSetContextRootsParams] + expect(initialRequest, hasLength(2)); + expect( + initialRequest?.last.method, + AnalysisSetContextRootsParams([]).toRequest('test').method, + ); + + await runner.close(); + }); + test('Handles files getting deleted', () async { // Regression test for https://github.com/invertase/dart_custom_lint/issues/105 final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource);