diff --git a/lib/steep/server/type_check_worker.rb b/lib/steep/server/type_check_worker.rb index 69518c62c..07292fa9b 100644 --- a/lib/steep/server/type_check_worker.rb +++ b/lib/steep/server/type_check_worker.rb @@ -237,55 +237,58 @@ def handle_job(job) if job.guid == current_type_check_guid Steep.logger.info { "Processing ValidateAppSignature for guid=#{job.guid}, path=#{job.path}" } - formatter = Diagnostic::LSPFormatter.new({}, **{}) + reporting_typecheck_progress(job) do + formatter = Diagnostic::LSPFormatter.new({}, **{}) - diagnostics = service.validate_signature(path: project.relative_path(job.path), target: job.target) + diagnostics = service.validate_signature(path: project.relative_path(job.path), target: job.target) - typecheck_progress( - path: job.path, - guid: job.guid, - target: job.target, - diagnostics: diagnostics.filter_map { formatter.format(_1) } - ) + diagnostics.filter_map { formatter.format(_1) } + end end when ValidateLibrarySignatureJob if job.guid == current_type_check_guid Steep.logger.info { "Processing ValidateLibrarySignature for guid=#{job.guid}, path=#{job.path}" } - formatter = Diagnostic::LSPFormatter.new({}, **{}) - diagnostics = service.validate_signature(path: job.path, target: job.target) + reporting_typecheck_progress(job) do + formatter = Diagnostic::LSPFormatter.new({}, **{}) + diagnostics = service.validate_signature(path: job.path, target: job.target) - typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics.filter_map { formatter.format(_1) }) + diagnostics.filter_map { formatter.format(_1) } + end end when TypeCheckCodeJob if job.guid == current_type_check_guid Steep.logger.info { "Processing TypeCheckCodeJob for guid=#{job.guid}, path=#{job.path}, target=#{job.target.name}" } - group_target = project.group_for_source_path(job.path) || job.target - formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config) - relative_path = project.relative_path(job.path) - diagnostics = service.typecheck_source(path: relative_path, target: job.target) - typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics&.filter_map { formatter.format(_1) }) + reporting_typecheck_progress(job) do + group_target = project.group_for_source_path(job.path) || job.target + formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config) + relative_path = project.relative_path(job.path) + diagnostics = service.typecheck_source(path: relative_path, target: job.target) + diagnostics&.filter_map { formatter.format(_1) } + end end when TypeCheckInlineCodeJob if job.guid == current_type_check_guid Steep.logger.info { "Processing TypeCheckInlineCodeJob for guid=#{job.guid}, path=#{job.path}, target=#{job.target.name}" } - group_target = project.group_for_inline_source_path(job.path) || job.target - formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config) - relative_path = project.relative_path(job.path) - diagnostics = service.typecheck_source(path: relative_path, target: job.target) #: Array[Diagnostic::Ruby::Base | Diagnostic::Signature::Base] | nil - signature_diagnostics = service.validate_signature(path: relative_path, target: job.target) - if diagnostics - diagnostics.concat(signature_diagnostics) - else - unless signature_diagnostics.empty? - diagnostics = signature_diagnostics + reporting_typecheck_progress(job) do + group_target = project.group_for_inline_source_path(job.path) || job.target + formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config) + relative_path = project.relative_path(job.path) + diagnostics = service.typecheck_source(path: relative_path, target: job.target) #: Array[Diagnostic::Ruby::Base | Diagnostic::Signature::Base] | nil + signature_diagnostics = service.validate_signature(path: relative_path, target: job.target) + if diagnostics + diagnostics.concat(signature_diagnostics) + else + unless signature_diagnostics.empty? + diagnostics = signature_diagnostics + end end - end - typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics&.filter_map { formatter.format(_1) }) + diagnostics&.filter_map { formatter.format(_1) } + end end when WorkspaceSymbolJob @@ -314,6 +317,20 @@ def typecheck_progress(guid:, path:, target:, diagnostics:) writer.write(CustomMethods::TypeCheck__Progress.notification({ guid: guid, path: path.to_s, target: target.name.to_s, diagnostics: diagnostics })) end + # Reports the file to the master even when the given block raises an error + # + # The master waits for `$/steep/typeCheck/progress` of every file it assigned, and the type check session + # never finishes if a worker skips reporting a file, for example because building a definition + # from the RBS environment raised an error during type checking. + # + def reporting_typecheck_progress(job) + diagnostics = yield + typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: diagnostics) + rescue StandardError + typecheck_progress(path: job.path, guid: job.guid, target: job.target, diagnostics: nil) + raise + end + def workspace_symbol_result(query) Steep.measure "Generating workspace symbol list for query=`#{query}`" do provider = Index::SignatureSymbolProvider.new(project: project, assignment: assignment) diff --git a/sig/steep/server/type_check_worker.rbs b/sig/steep/server/type_check_worker.rbs index 7bdf0176b..802b8d04a 100644 --- a/sig/steep/server/type_check_worker.rbs +++ b/sig/steep/server/type_check_worker.rbs @@ -161,6 +161,14 @@ module Steep def typecheck_progress: (guid: String, path: Pathname, target: Project::Target, diagnostics: Array[LSPDiagnostic::json]?) -> void + # Reports the file to the master even when the given block raises an error + # + # The master waits for `$/steep/typeCheck/progress` of every file it assigned, and the type check session + # never finishes if a worker skips reporting a file, for example because building a definition + # from the RBS environment raised an error during type checking. + # + def reporting_typecheck_progress: (ValidateAppSignatureJob | ValidateLibrarySignatureJob | TypeCheckCodeJob | TypeCheckInlineCodeJob job) { () -> Array[LSPDiagnostic::json]? } -> void + def workspace_symbol_result: (untyped query) -> untyped def stats_result: () -> Array[StatsCalculator::stats] diff --git a/sig/test/type_check_worker_test.rbs b/sig/test/type_check_worker_test.rbs index 1ba6e1f54..b30e8a52c 100644 --- a/sig/test/type_check_worker_test.rbs +++ b/sig/test/type_check_worker_test.rbs @@ -50,6 +50,8 @@ class TypeCheckWorkerTest < Minitest::Test def test_handle_job_typecheck_code_diagnostics: () -> untyped + def test_handle_job_typecheck_code_error: () -> untyped + def test_handle_job_typecheck_skip: () -> untyped def test_handle_job_typecheck_inline__no_error: () -> untyped diff --git a/test/type_check_worker_test.rb b/test/type_check_worker_test.rb index 5b50d54fa..e79896c90 100644 --- a/test/type_check_worker_test.rb +++ b/test/type_check_worker_test.rb @@ -583,6 +583,62 @@ def world: () -> void end end + def test_handle_job_typecheck_code_error + in_tmpdir do + with_master_read_queue do |master_read_queue| + project = Project.new(steepfile_path: current_dir + "Steepfile") + Project::DSL.parse(project, <<~RUBY) + target :lib do + check "lib" + signature "sig" + end + RUBY + + worker = Server::TypeCheckWorker.new( + project: project, + assignment: assignment, + commandline_args: [], + reader: worker_reader, + writer: worker_writer + ) + + worker.instance_variable_set(:@current_type_check_guid, "guid") + + {}.tap do |changes| + changes[Pathname("lib/hello.rb")] = [Services::ContentChange.string(<<~RUBY)] + Hello.new.world() + RUBY + # `Module#ruby2_keywords` conflicts with the *private* method in core, and + # building a definition raises an error during type checking, not validation. + changes[Pathname("sig/hello.rbs")] = [Services::ContentChange.string(<<~RBS)] + class Hello + def world: () -> void + end + + class Module + def ruby2_keywords: (*Symbol) -> void + end + RBS + worker.handle_job(TypeCheckWorker::StartTypeCheckJob.new(guid: "guid", changes: changes)) + end + + job = TypeCheckWorker::TypeCheckCodeJob.new(guid: "guid", path: current_dir + "lib/hello.rb", target: project.targets[0]) + assert_raises(RBS::DuplicatedMethodDefinitionError) do + worker.handle_job(job) + end + + # The file is reported to the master even when the type check fails, so that + # the master doesn't wait for the file forever + master_read_queue.pop.tap do |message| + assert_equal TypeCheck__Progress::METHOD, message[:method] + assert_equal "guid", message[:params][:guid] + assert_equal (current_dir + "lib/hello.rb").to_s, message[:params][:path] + assert_nil message[:params][:diagnostics] + end + end + end + end + def test_handle_job_typecheck_skip in_tmpdir do with_master_read_queue do |master_read_queue|