From fc9b577466ea636502d83a03ce97d5c76f6effab Mon Sep 17 00:00:00 2001 From: ksss Date: Sat, 26 Aug 2023 23:42:18 +0900 Subject: [PATCH 1/4] Implement todo option The todo option excludes predefined output from prototype runtime. --- lib/rbs/cli.rb | 6 ++- lib/rbs/prototype/runtime.rb | 68 +++++++++++++++++++++++- sig/prototype/runtime.rbs | 40 +++++++++----- test/rbs/runtime_prototype_test.rb | 85 ++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 14 deletions(-) diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index 7264d8874..0049f7f5e 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -644,6 +644,7 @@ def run_prototype(args, options) require_libs = [] relative_libs = [] merge = false + todo = false owners_included = [] outline = false @@ -671,6 +672,9 @@ def run_prototype(args, options) opts.on("--merge", "Merge generated prototype RBS with existing RBS") do merge = true end + opts.on("--todo", "Generates only undefined methods compared to objects") do + todo = true + end opts.on("--method-owner CLASS", "Generate method prototypes if the owner of the method is [CLASS]") do |klass| owners_included << klass end @@ -690,7 +694,7 @@ def run_prototype(args, options) eval("require_relative(lib)", binding, "rbs") end - runtime = Prototype::Runtime.new(patterns: args, env: env, merge: merge, owners_included: owners_included) + runtime = Prototype::Runtime.new(patterns: args, env: env, merge: merge, todo: todo, owners_included: owners_included) runtime.outline = outline decls = runtime.decls diff --git a/lib/rbs/prototype/runtime.rb b/lib/rbs/prototype/runtime.rb index 66b10a001..ecddd5d7d 100644 --- a/lib/rbs/prototype/runtime.rb +++ b/lib/rbs/prototype/runtime.rb @@ -3,15 +3,60 @@ module RBS module Prototype class Runtime + class Todo + def initialize(builder:) + @builder = builder + end + + def skip_mixin?(type_name:, module_name:, mixin_class:) + return false unless @builder.env.module_class_entry(type_name.absolute!) + return false unless @builder.env.module_class_entry(module_name.absolute!) + + mixin_decls(type_name).any? do |decl| + decl.instance_of?(mixin_class) && decl.name == module_name.absolute! + end + end + + def skip_singleton_method?(module_name:, name:) + return false unless @builder.env.module_class_entry(module_name.absolute!) + + @builder.build_singleton(module_name.absolute!).methods.has_key?(name) + end + + def skip_instance_method?(module_name:, name:) + return false unless @builder.env.module_class_entry(module_name.absolute!) + + @builder.build_instance(module_name.absolute!).methods.has_key?(name) + end + + def skip_constant?(module_name:, name:) + namespace = Namespace.new(path: module_name.split('::').map(&:to_sym), absolute: true) + @builder.env.constant_decl?(TypeName.new(namespace: namespace, name: name)) + end + + private + + def mixin_decls(type_name) + type_name_absolute = type_name.absolute! + (@mixin_decls_cache ||= {}).fetch(type_name_absolute) do + @mixin_decls_cache[type_name_absolute] = @builder.env.class_decls[type_name_absolute].decls.flat_map do |d| + d.decl.members.select { |m| m.kind_of?(AST::Members::Mixin) } + end + end + end + end + private_constant :Todo + include Helpers attr_reader :patterns attr_reader :env attr_reader :merge + attr_reader :todo attr_reader :owners_included attr_accessor :outline - def initialize(patterns:, env:, merge:, owners_included: []) + def initialize(patterns:, env:, merge:, todo: false, owners_included: []) @patterns = patterns @decls = nil @modules = {} @@ -21,6 +66,7 @@ def initialize(patterns:, env:, merge:, owners_included: []) Object.const_get(name) end @outline = false + @todo = todo end def target?(const) @@ -36,6 +82,10 @@ def target?(const) end end + def todo_object + @todo_object ||= Todo.new(builder: builder) if todo + end + def builder @builder ||= DefinitionBuilder.new(env: env) end @@ -251,7 +301,10 @@ def target_method?(mod, instance: nil, singleton: nil) end def generate_methods(mod, module_name, members) + module_name_absolute = to_type_name(const_name!(mod), full_name: true).absolute! mod.singleton_methods.select {|name| target_method?(mod, singleton: name) }.sort.each do |name| + next if todo_object&.skip_singleton_method?(module_name: module_name_absolute, name: name) + method = mod.singleton_class.instance_method(name) if can_alias?(mod.singleton_class, method) @@ -288,6 +341,8 @@ def generate_methods(mod, module_name, members) members << AST::Members::Public.new(location: nil) public_instance_methods.sort.each do |name| + next if todo_object&.skip_instance_method?(module_name: module_name_absolute, name: name) + method = mod.instance_method(name) if can_alias?(mod, method) @@ -325,6 +380,8 @@ def generate_methods(mod, module_name, members) members << AST::Members::Private.new(location: nil) private_instance_methods.sort.each do |name| + next if todo_object&.skip_instance_method?(module_name: module_name_absolute, name: name) + method = mod.instance_method(name) if can_alias?(mod, method) @@ -369,7 +426,10 @@ def generate_methods(mod, module_name, members) end def generate_constants(mod, decls) + module_name = const_name!(mod) mod.constants(false).sort.each do |name| + next if todo_object&.skip_constant?(module_name: module_name, name: name) + begin value = mod.const_get(name) rescue StandardError, LoadError => e @@ -428,6 +488,7 @@ def generate_super_class(mod) end def generate_class(mod) + type_name_absolute = to_type_name(const_name!(mod), full_name: true).absolute! type_name = to_type_name(const_name!(mod)) outer_decls = ensure_outer_module_declarations(mod) @@ -451,6 +512,8 @@ def generate_class(mod) end each_mixined_module(type_name, mod) do |module_name, module_full_name, mixin_class| + next if todo_object&.skip_mixin?(type_name: type_name_absolute, module_name: module_full_name, mixin_class: mixin_class) + args = type_args(module_full_name) decl.members << mixin_class.new( name: module_name, @@ -474,6 +537,7 @@ def generate_module(mod) return end + type_name_absolute = to_type_name(name, full_name: true).absolute! type_name = to_type_name(name) outer_decls = ensure_outer_module_declarations(mod) @@ -497,6 +561,8 @@ def generate_module(mod) end each_mixined_module(type_name, mod) do |module_name, module_full_name, mixin_class| + next if todo_object&.skip_mixin?(type_name: type_name_absolute, module_name: module_full_name, mixin_class: mixin_class) + args = type_args(module_full_name) decl.members << mixin_class.new( name: module_name, diff --git a/sig/prototype/runtime.rbs b/sig/prototype/runtime.rbs index fad832170..82d73f5e8 100644 --- a/sig/prototype/runtime.rbs +++ b/sig/prototype/runtime.rbs @@ -1,6 +1,26 @@ module RBS module Prototype class Runtime + class Todo + @builder: DefinitionBuilder + + @mixin_decls_cache: Hash[TypeName, Array[untyped]] + + def initialize: (builder: DefinitionBuilder) -> void + + def skip_mixin?: (type_name: TypeName, module_name: TypeName, mixin_class: mixin_class) -> bool + + def skip_singleton_method?: (module_name: TypeName, name: Symbol) -> bool + + def skip_instance_method?: (module_name: TypeName, name: Symbol) -> bool + + def skip_constant?: (module_name: String, name: Symbol) -> bool + + private def mixin_decls: (TypeName type_name) -> Array[AST::Members::Include | AST::Members::Extend | AST::Members::Prepend] + end + + type mixin_class = singleton(AST::Members::Include) | singleton(AST::Members::Prepend) | singleton(AST::Members::Extend) + @decls: Array[AST::Declarations::t]? @modules: Hash[String, Module] @@ -10,6 +30,8 @@ module RBS @module_name_method: UnboundMethod @object_class: UnboundMethod + @todo_object: Todo? + include Helpers attr_reader patterns: Array[String] @@ -18,14 +40,18 @@ module RBS attr_reader merge: bool + attr_reader todo: bool + attr_accessor outline: bool attr_reader owners_included: Array[Module] - def initialize: (patterns: Array[String], env: Environment, merge: bool, ?owners_included: Array[Symbol]) -> void + def initialize: (patterns: Array[String], env: Environment, merge: bool, ?todo: bool, ?owners_included: Array[Symbol]) -> void def target?: (Module const) -> bool + def todo_object: () -> Todo? + def builder: () -> DefinitionBuilder def parse: (String file) -> void @@ -34,17 +60,7 @@ module RBS def to_type_name: (String name, ?full_name: bool) -> TypeName - interface _MixinFactory - def new: ( - name: TypeName, - args: Array[Types::t], - location: Location[untyped, untyped]?, - comment: AST::Comment?, - annotations: Array[AST::Annotation] - ) -> (AST::Members::Include | AST::Members::Prepend | AST::Members::Extend) - end - - def each_mixined_module: (TypeName type_name, Module mod) { (TypeName, TypeName, _MixinFactory) -> void } -> void + def each_mixined_module: (TypeName type_name, Module mod) { (TypeName, TypeName, mixin_class) -> void } -> void def each_mixined_module_one: (TypeName type_name, Module mod) { (TypeName, TypeName, bool) -> void } -> void diff --git a/test/rbs/runtime_prototype_test.rb b/test/rbs/runtime_prototype_test.rb index e6df1d228..85b53bacb 100644 --- a/test/rbs/runtime_prototype_test.rb +++ b/test/rbs/runtime_prototype_test.rb @@ -606,4 +606,89 @@ def baz: () -> untyped end end end + + class TodoClass + module MixinDefined + end + include MixinDefined + module MixinTodo + end + extend MixinTodo + + def public_defined; end + def public_todo; end + private def private_defined; end + private def private_todo; end + def self.singleton_defined; end + def self.singleton_todo; end + + CONST_DEFINED = 1 + CONST_TODO = 1 + end + + module TodoModule + def public_defined; end + def public_todo; end + end + + def test_todo + SignatureManager.new do |manager| + manager.files[Pathname("foo.rbs")] = <<~RBS + module RBS + class RuntimePrototypeTest < ::Test::Unit::TestCase + class TodoClass + module MixinDefined + end + include MixinDefined + def public_defined: () -> void + private def private_defined: () -> void + def self.singleton_defined: () -> void + CONST_DEFINED: Integer + end + module TodoModule + def public_defined: () -> void + end + end + end + RBS + + manager.build do |env| + p = Runtime.new(patterns: ["RBS::RuntimePrototypeTest::TodoClass"], env: env, merge: false, todo: true) + assert_write p.decls, <<~RBS + module RBS + class RuntimePrototypeTest < ::Test::Unit::TestCase + class TodoClass + extend RBS::RuntimePrototypeTest::TodoClass::MixinTodo + + def self.singleton_todo: () -> untyped + + public + + def public_todo: () -> untyped + + private + + def private_todo: () -> untyped + + CONST_TODO: Integer + end + end + end + RBS + + p = Runtime.new(patterns: ["RBS::RuntimePrototypeTest::TodoModule"], env: env, merge: false, todo: true) + assert_write p.decls, <<~RBS + module RBS + class RuntimePrototypeTest < ::Test::Unit::TestCase + module TodoModule + public + + def public_todo: () -> untyped + end + end + end + RBS + end + end + end end From bbd1b35c7747c4b9f9ed7b88d31309c0bd7674c6 Mon Sep 17 00:00:00 2001 From: ksss Date: Sat, 26 Aug 2023 23:51:17 +0900 Subject: [PATCH 2/4] Remove `private` if no entry. --- lib/rbs/prototype/runtime.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rbs/prototype/runtime.rb b/lib/rbs/prototype/runtime.rb index ecddd5d7d..1e56b716b 100644 --- a/lib/rbs/prototype/runtime.rb +++ b/lib/rbs/prototype/runtime.rb @@ -377,11 +377,13 @@ def generate_methods(mod, module_name, members) private_instance_methods = mod.private_instance_methods.select {|name| target_method?(mod, instance: name) } unless private_instance_methods.empty? + added = false members << AST::Members::Private.new(location: nil) private_instance_methods.sort.each do |name| next if todo_object&.skip_instance_method?(module_name: module_name_absolute, name: name) + added = true method = mod.instance_method(name) if can_alias?(mod, method) @@ -412,6 +414,8 @@ def generate_methods(mod, module_name, members) end end end + + members.pop unless added end end From 2cff37e6d70edc99d18fb211203a46dddcb396b3 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 15 Sep 2023 15:51:28 +0900 Subject: [PATCH 3/4] Add experimental warning --- lib/rbs/cli.rb | 1 + test/rbs/cli_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index 0049f7f5e..7d9532480 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -673,6 +673,7 @@ def run_prototype(args, options) merge = true end opts.on("--todo", "Generates only undefined methods compared to objects") do + Warning.warn("Geneating prototypes with `--todo` option is experimental\n", category: :experimental) todo = true end opts.on("--method-owner CLASS", "Generate method prototypes if the owner of the method is [CLASS]") do |klass| diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb index 8c88beed0..1b199e41a 100644 --- a/test/rbs/cli_test.rb +++ b/test/rbs/cli_test.rb @@ -681,6 +681,28 @@ module A end end + def test_prototype__runtime__todo + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + with_cli do |cli| + begin + old = $stderr + $stderr = cli.stderr + cli.run(%w(prototype runtime --todo ::Object)) + ensure + $stderr = old + end + + assert_equal <<~EOM, cli.stdout.string + EOM + + assert_match Regexp.new(Regexp.escape "Geneating prototypes with `--todo` option is experimental"), cli.stderr.string + end + end + end + end + + def test_test Dir.mktmpdir do |dir| dir = Pathname(dir) From 2ae64f89d4740af0f5168f6c460349bb3a88ca0f Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Fri, 15 Sep 2023 15:55:30 +0900 Subject: [PATCH 4/4] Fix test --- test/rbs/runtime_prototype_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rbs/runtime_prototype_test.rb b/test/rbs/runtime_prototype_test.rb index 85b53bacb..c4bcb33be 100644 --- a/test/rbs/runtime_prototype_test.rb +++ b/test/rbs/runtime_prototype_test.rb @@ -670,7 +670,7 @@ def public_todo: () -> untyped def private_todo: () -> untyped - CONST_TODO: Integer + CONST_TODO: ::Integer end end end