module Steep module Services class SignatureService attr_reader :status class SyntaxErrorStatus attr_reader :files, :changed_paths, :diagnostics, :last_builder def initialize(files:, changed_paths:, diagnostics:, last_builder:) @files = files @changed_paths = changed_paths @diagnostics = diagnostics @last_builder = last_builder end def rbs_index @rbs_index ||= Index::RBSIndex.new().tap do |index| builder = Index::RBSIndex::Builder.new(index: index) builder.env(last_builder.env) end end end class AncestorErrorStatus attr_reader :files, :changed_paths, :diagnostics, :last_builder def initialize(files:, changed_paths:, diagnostics:, last_builder:) @files = files @changed_paths = changed_paths @diagnostics = diagnostics @last_builder = last_builder end def rbs_index @rbs_index ||= Index::RBSIndex.new().tap do |index| builder = Index::RBSIndex::Builder.new(index: index) builder.env(last_builder.env) end end end class LoadedStatus attr_reader :files, :builder def initialize(files:, builder:) @files = files @builder = builder end def subtyping @subtyping ||= Subtyping::Check.new(factory: AST::Types::Factory.new(builder: builder)) end def rbs_index @rbs_index ||= Index::RBSIndex.new().tap do |index| builder = Index::RBSIndex::Builder.new(index: index) builder.env(self.builder.env) end end end FileStatus = Struct.new(:path, :content, :decls, keyword_init: true) def initialize(env:) builder = RBS::DefinitionBuilder.new(env: env) @status = LoadedStatus.new(builder: builder, files: {}) end def self.load_from(loader) env = RBS::Environment.from_loader(loader).resolve_type_names new(env: env) end def env_rbs_paths @env_rbs_paths ||= latest_env.buffers.each.with_object(Set[]) do |buffer, set| set << Pathname(buffer.name) end end def each_rbs_path(&block) if block env_rbs_paths.each do |path| unless files.key?(path) yield path end end files.each_key(&block) else enum_for :each_rbs_path end end def files status.files end def pending_changed_paths case status when LoadedStatus Set[] when SyntaxErrorStatus, AncestorErrorStatus Set.new(status.changed_paths) end end def latest_env latest_builder.env end def latest_builder case status when LoadedStatus status.builder when SyntaxErrorStatus, AncestorErrorStatus status.last_builder end end def latest_rbs_index status.rbs_index end def current_subtyping if status.is_a?(LoadedStatus) status.subtyping end end def apply_changes(files, changes) Steep.logger.tagged "#apply_changes" do Steep.measure2 "Applying change" do |sampler| changes.each.with_object({}) do |(path, cs), update| sampler.sample "#{path}" do old_text = files[path]&.content content = cs.inject(old_text || "") {|text, change| change.apply_to(text) } buffer = RBS::Buffer.new(name: path, content: content) update[path] = begin FileStatus.new(path: path, content: content, decls: RBS::Parser.parse_signature(buffer)) rescue ArgumentError => exn error = Diagnostic::Signature::UnexpectedError.new( message: exn.message, location: RBS::Location.new(buffer: buffer, start_pos: 0, end_pos: content.size) ) FileStatus.new(path: path, content: content, decls: error) rescue RBS::ParsingError => exn FileStatus.new(path: path, content: content, decls: exn) end end end end end end def update(changes) Steep.logger.tagged "#update" do updates = apply_changes(files, changes) paths = Set.new(updates.each_key) paths.merge(pending_changed_paths) if updates.each_value.any? {|file| !file.decls.is_a?(Array) } diagnostics = [] updates.each_value do |file| unless file.decls.is_a?(Array) diagnostic = if file.decls.is_a?(Diagnostic::Signature::Base) file.decls else # factory is not used here because the error is a syntax error. Diagnostic::Signature.from_rbs_error(file.decls, factory: nil) end diagnostics << diagnostic end end @status = SyntaxErrorStatus.new( files: self.files.merge(updates), diagnostics: diagnostics, last_builder: latest_builder, changed_paths: paths ) else files = self.files.merge(updates) updated_files = paths.each.with_object({}) do |path, hash| hash[path] = files[path] end result = Steep.measure "#update_env with updated #{paths.size} files" do update_env(updated_files, paths: paths) end @status = case result when Array AncestorErrorStatus.new( changed_paths: paths, last_builder: latest_builder, diagnostics: result, files: files ) when RBS::DefinitionBuilder::AncestorBuilder builder2 = update_builder(ancestor_builder: result, paths: paths) LoadedStatus.new(builder: builder2, files: files) end end end end def update_env(updated_files, paths:) Steep.logger.tagged "#update_env" do errors = [] new_decls = Set[].compare_by_identity env = Steep.measure "Deleting out of date decls" do latest_env.reject do |decl| if decl.location paths.include?(decl.location.buffer.name) end end end Steep.measure "Loading new decls" do updated_files.each_value do |content| case decls = content.decls when RBS::ErrorBase errors << content.decls else begin content.decls.each do |decl| env << decl new_decls << decl end rescue RBS::LoadingError => exn errors << exn end end end end Steep.measure "validate type params" do begin env.validate_type_params rescue RBS::LoadingError => exn errors << exn end end unless errors.empty? return errors.map {|error| # Factory will not be used because of the possible error types. Diagnostic::Signature.from_rbs_error(error, factory: nil) } end Steep.measure "resolve type names with #{new_decls.size} top-level decls" do env = env.resolve_type_names(only: new_decls) end builder = RBS::DefinitionBuilder::AncestorBuilder.new(env: env) Steep.measure("Pre-loading one ancestors") do builder.env.class_decls.each_key do |type_name| rescue_rbs_error(errors) { builder.one_instance_ancestors(type_name) } rescue_rbs_error(errors) { builder.one_singleton_ancestors(type_name) } end builder.env.interface_decls.each_key do |type_name| rescue_rbs_error(errors) { builder.one_interface_ancestors(type_name) } end end unless errors.empty? # Builder won't be used. factory = AST::Types::Factory.new(builder: nil) return errors.map {|error| Diagnostic::Signature.from_rbs_error(error, factory: factory) } end builder end end def rescue_rbs_error(errors) begin yield rescue RBS::ErrorBase => exn errors << exn end end def update_builder(ancestor_builder:, paths:) Steep.measure "#update_builder with #{paths.size} files" do changed_names = Set[] old_definition_builder = latest_builder old_env = old_definition_builder.env old_names = type_names(paths: paths, env: old_env) old_ancestor_builder = old_definition_builder.ancestor_builder old_graph = RBS::AncestorGraph.new(env: old_env, ancestor_builder: old_ancestor_builder) add_descendants(graph: old_graph, names: old_names, set: changed_names) add_nested_decls(env: old_env, names: old_names, set: changed_names) new_env = ancestor_builder.env new_ancestor_builder = ancestor_builder new_names = type_names(paths: paths, env: new_env) new_graph = RBS::AncestorGraph.new(env: new_env, ancestor_builder: new_ancestor_builder) add_descendants(graph: new_graph, names: new_names, set: changed_names) add_nested_decls(env: new_env, names: new_names, set: changed_names) old_definition_builder.update( env: new_env, ancestor_builder: new_ancestor_builder, except: changed_names ) end end def type_names(paths:, env:) env.declarations.each.with_object(Set[]) do |decl, set| if decl.location if paths.include?(Pathname(decl.location.buffer.name)) type_name_from_decl(decl, set: set) end end end end def const_decls(paths:, env:) env.constant_decls.filter do |_, entry| if location = entry.decl.location paths.include?(Pathname(location.buffer.name)) end end end def global_decls(paths:, env: latest_env) env.global_decls.filter do |_, entry| if location = entry.decl.location paths.include?(Pathname(location.buffer.name)) end end end def type_name_from_decl(decl, set:) case decl when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module, RBS::AST::Declarations::Interface set << decl.name decl.members.each do |member| if member.is_a?(RBS::AST::Declarations::Base) type_name_from_decl(member, set: set) end end when RBS::AST::Declarations::Alias set << decl.name end end def add_descendants(graph:, names:, set:) set.merge(names) names.each do |name| case when name.interface? graph.each_descendant(RBS::AncestorGraph::InstanceNode.new(type_name: name)) do |node| set << node.type_name end when name.class? graph.each_descendant(RBS::AncestorGraph::InstanceNode.new(type_name: name)) do |node| set << node.type_name end graph.each_descendant(RBS::AncestorGraph::SingletonNode.new(type_name: name)) do |node| set << node.type_name end end end end def add_nested_decls(env:, names:, set:) tops = names.each.with_object(Set[]) do |name, tops| unless name.namespace.empty? tops << name.namespace.path[0] end end env.class_decls.each_key do |name| unless name.namespace.empty? if tops.include?(name.namespace.path[0]) set << name end end end env.interface_decls.each_key do |name| unless name.namespace.empty? if tops.include?(name.namespace.path[0]) set << name end end end end end end end