rbs-trace 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -1
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +44 -24
  5. data/Rakefile +7 -0
  6. data/exe/rbs-trace +8 -0
  7. data/lib/rbs/trace/builder.rb +124 -0
  8. data/lib/rbs/trace/cli/inline.rb +62 -0
  9. data/lib/rbs/trace/cli/merge.rb +102 -0
  10. data/lib/rbs/trace/cli.rb +40 -0
  11. data/lib/rbs/trace/file.rb +66 -21
  12. data/lib/rbs/trace/helpers.rb +75 -0
  13. data/lib/rbs/trace/inline_comment_visitor.rb +63 -0
  14. data/lib/rbs/trace/overload_compact.rb +85 -0
  15. data/lib/rbs/trace/return_value_visitor.rb +63 -0
  16. data/lib/rbs/trace/version.rb +2 -2
  17. data/lib/rbs/trace.rb +153 -4
  18. data/rbs_collection.lock.yaml +0 -4
  19. data/sig/generated/rbs/trace/builder.rbs +33 -0
  20. data/sig/generated/rbs/trace/cli/inline.rbs +19 -0
  21. data/sig/generated/rbs/trace/cli/merge.rbs +24 -0
  22. data/sig/generated/rbs/trace/cli.rbs +17 -0
  23. data/sig/generated/rbs/trace/file.rbs +20 -9
  24. data/sig/generated/rbs/trace/helpers.rbs +33 -0
  25. data/sig/generated/rbs/trace/inline_comment_visitor.rbs +27 -0
  26. data/sig/generated/rbs/trace/overload_compact.rbs +21 -0
  27. data/sig/generated/rbs/trace/return_value_visitor.rbs +27 -0
  28. data/sig/generated/rbs/trace/version.rbs +1 -1
  29. data/sig/generated/rbs/trace.rbs +63 -1
  30. data/tmp/.keep +0 -0
  31. metadata +38 -11
  32. data/lib/rbs/trace/declaration.rb +0 -107
  33. data/lib/rbs/trace/definition.rb +0 -33
  34. data/lib/rbs/trace/method_tracing.rb +0 -182
  35. data/sig/generated/rbs/trace/declaration.rbs +0 -36
  36. data/sig/generated/rbs/trace/definition.rbs +0 -25
  37. data/sig/generated/rbs/trace/method_tracing.rbs +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e587e127bc2689a2d6e3c9f25dad32a5b75b49164b8666d0c0ff891d9c6e35f4
4
- data.tar.gz: 47cb65ce7cef30a14cc68984e7a76614ca593a1581a439b34341386937c65591
3
+ metadata.gz: 2e7d9ae59c5c4d3427c486422dcda9158bb65b1d5054d0ab5713a9714de9f39c
4
+ data.tar.gz: fc377a935ef972ac2e153d40f336154c03cb1e42b88dc732132f2e71bba83047
5
5
  SHA512:
6
- metadata.gz: 3ccd45b222189e14609f8ef30f207341dbf6a4ed863a4c94b5cf051d28504f84cd8d101a68905de787ac5661f9bc7b9432162b81abbda85868e40e14cc741ebd
7
- data.tar.gz: 158f74515cad425793edcc6f7d68c2ee8140afb36d9922ee80b6d1c21c020db748c9842c9b3bc6c15d330c2a12834681c6082b3e373c9ae669ccc379337ed626
6
+ metadata.gz: e3ff05d28fbef44e66500377dd38e74a0a3890da197d00bbf61b461358d404fef1933d43e30655a5dffce3850f1a71b4d535b88db8e209d2d1100ba7c1f502e9
7
+ data.tar.gz: 446d6c0d49e72fae943708bc146bf80365cd107bac3d9c509950a0184611f19982dc244f0398964ff50d4d462756a3004ce73e5d5d0488a1ae9e41257555a447
data/.rubocop.yml CHANGED
@@ -16,7 +16,7 @@ Style/Documentation:
16
16
  Enabled: false
17
17
 
18
18
  RSpec/ExampleLength:
19
- Max: 15
19
+ Max: 50
20
20
 
21
21
  Naming/MethodParameterName:
22
22
  Enabled: false
@@ -25,8 +25,18 @@ Naming/FileName:
25
25
  Exclude:
26
26
  - "lib/rbs-trace.rb"
27
27
 
28
+ Naming/VariableNumber:
29
+ Enabled: false
30
+
28
31
  Layout/LeadingCommentSpace:
29
32
  Enabled: false
30
33
 
31
34
  Style/AccessorGrouping:
32
35
  Enabled: false
36
+
37
+ RSpec/DescribeClass:
38
+ Exclude:
39
+ - "spec/smoke_spec.rb"
40
+
41
+ RSpec/MultipleExpectations:
42
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-02-23
4
+
5
+ - feat: Implement the function to save as RBS files
6
+ - feat: Add CLI commands to support parallel testing
7
+ - fix: Ignore if path is "inline template"
8
+
9
+ ### BREAKING CHANGE
10
+
11
+ - chore!: Re-design API
12
+
13
+ ```ruby
14
+ # before
15
+ tracing = RBS::Trace::MethodTracing.new
16
+ tracing.enable do
17
+ # something
18
+ end
19
+ tracing.insert_rbs
20
+
21
+ # after
22
+ trace = RBS::Trace.new
23
+ trace.enable do
24
+ # something
25
+ end
26
+ trace.save_comments
27
+ ```
28
+
3
29
  ## [0.3.2] - 2024-12-29
4
30
 
5
31
  - fix: Do not insert comments if comment `#:` exists
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/rbs-trace.svg)](https://badge.fury.io/rb/rbs-trace)
2
2
  [![Test](https://github.com/sinsoku/rbs-trace/actions/workflows/test.yml/badge.svg)](https://github.com/sinsoku/rbs-trace/actions/workflows/test.yml)
3
+ [![codecov](https://codecov.io/gh/sinsoku/rbs-trace/graph/badge.svg?token=rEsPe8Quyu)](https://codecov.io/gh/sinsoku/rbs-trace)
3
4
 
4
5
  # RBS::Trace
5
6
 
6
- RBS::Trace collects argument types and return value types using TracePoint, and inserts inline RBS type declarations into files.
7
+ RBS::Trace automatically collects argument and return types and saves RBS type declarations as RBS files or comments.
7
8
 
8
9
  ## Installation
9
10
 
@@ -36,21 +37,22 @@ class User
36
37
  end
37
38
  ```
38
39
 
39
- Call target methods within the `enable` method block, and call the `insert_rbs` method.
40
+ Call target methods within the `enable` method block, and call the `save_comments` method.
40
41
 
41
42
  ```ruby
42
- tracing = RBS::Trace::MethodTracing.new
43
+ trace = RBS::Trace.new
43
44
 
44
45
  # Collects the types of methods called in the block.
45
- tracing.enable do
46
+ trace.enable do
46
47
  user = User.new("Nanoha", "Takamachi")
47
48
  user.say_hello
48
49
  end
49
50
 
50
- tracing.insert_rbs
51
+ # Save RBS type declarations as embedded comments
52
+ trace.save_comments
51
53
  ```
52
54
 
53
- Automatically inserts inline RBS definitions into the file.
55
+ Automatically insert comments into the file.
54
56
 
55
57
  ```ruby
56
58
  class User
@@ -79,36 +81,28 @@ end
79
81
  Add the following code to `spec/support/rbs_trace.rb`.
80
82
 
81
83
  ```ruby
82
- return unless ENV["RBS_TRACE"]
83
-
84
84
  RSpec.configure do |config|
85
- tracing = RBS::Trace::MethodTracing.new
85
+ trace = RBS::Trace.new
86
86
 
87
- config.before(:suite) { tracing.enable }
87
+ config.before(:suite) { trace.enable }
88
88
  config.after(:suite) do
89
- tracing.disable
90
- tracing.insert_rbs
89
+ trace.disable
90
+ trace.save_comments
91
91
  end
92
92
  end
93
93
  ```
94
94
 
95
- Then run RSpec with the environment variables.
96
-
97
- ```console
98
- $ RBS_TRACE=1 bundle exec rspec
99
- ```
100
-
101
95
  ### Minitest
102
96
 
103
97
  Add the following code to `test_helper.rb`.
104
98
 
105
99
  ```ruby
106
- tracing = RBS::Trace::MethodTracing.new
107
- tracing.enable
100
+ trace = RBS::Trace.new
101
+ trace.enable
108
102
 
109
103
  Minitest.after_run do
110
- tracing.disable
111
- tracing.insert_rbs
104
+ trace.disable
105
+ trace.save_comments
112
106
  end
113
107
  ```
114
108
 
@@ -117,11 +111,37 @@ end
117
111
  ### Insert RBS declarations for specific files only
118
112
 
119
113
  ```ruby
120
- tracing.files.each do |path, file|
114
+ trace.files.each do |path, file|
121
115
  file.rewrite if path.include?("app/models/")
122
116
  end
123
117
  ```
124
118
 
119
+ ### Save RBS declarations as files
120
+
121
+ ```ruby
122
+ trace.save_files(out_dir: "sig/trace/")
123
+ ```
124
+
125
+ ### Parallel testing
126
+
127
+ If you are using a parallel testing gem such as [parallel_tests](https://github.com/grosser/parallel_tests) or [flatware](https://github.com/briandunn/flatware), first save the type definitions in RBS files.
128
+
129
+ ```ruby
130
+ trace.save_files(out_dir: "tmp/sig-#{ENV["TEST_ENV_NUMBER"]}")
131
+ ```
132
+
133
+ Then use `rbs-trace merge` to merge multiple RBS files into one.
134
+
135
+ ```bash
136
+ $ rbs-trace merge --sig-dir='tmp/sig-*' > sig/merged.rbs
137
+ ```
138
+
139
+ Finally, insert comments using the merged RBS files.
140
+
141
+ ```bash
142
+ $ rbs-trace inline --rb-dir=app --rb-dir=lib
143
+ ```
144
+
125
145
  ### Enable debug logging
126
146
 
127
147
  If you want to enable debug logging, specify the environment variable `RBS_TRACE_DEBUG`.
@@ -142,4 +162,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
142
162
 
143
163
  ## Code of Conduct
144
164
 
145
- Everyone interacting in the Rbs::Trace project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sinsoku/rbs-trace/blob/main/CODE_OF_CONDUCT.md).
165
+ Everyone interacting in the RBS::Trace project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sinsoku/rbs-trace/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -12,6 +12,13 @@ RuboCop::RakeTask.new
12
12
  desc "Generate rbs files"
13
13
  task :rbs_inline do
14
14
  sh "rbs-inline --output --opt-out lib"
15
+
16
+ # If the Ruby file is deleted, delete the RBS file
17
+ Dir.glob("sig/generated/**/*.rbs").each do |path|
18
+ rbs_path = Pathname(path)
19
+ rb_path = rbs_path.sub(%r{^sig/generated}, "lib").sub_ext(".rb")
20
+ rbs_path.delete unless File.exist?(rb_path)
21
+ end
15
22
  end
16
23
 
17
24
  desc "Run Steep"
data/exe/rbs-trace ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "rbs-trace"
8
+ RBS::Trace::CLI.new.run
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ class Trace
5
+ class Builder
6
+ include Helpers
7
+
8
+ GENERICS_SIZE = {
9
+ Array => 1,
10
+ Range => 1,
11
+ Hash => 2
12
+ }.freeze
13
+ private_constant :GENERICS_SIZE
14
+
15
+ # @rbs (bind: Binding, parameters: Array[__todo__], void: bool) -> void
16
+ def method_call(bind:, parameters:, void:)
17
+ method_type = parse_parameters(bind, parameters)
18
+ return_type = type_void if void
19
+
20
+ stack_traces << [method_type, return_type]
21
+ end
22
+
23
+ # @rbs (__todo__) -> AST::Members::MethodDefinition::Overload
24
+ def method_return(return_value)
25
+ method_type, return_type = stack_traces.pop
26
+
27
+ type = return_type || parse_object(return_value)
28
+ new_type = method_type.type.with_return_type(type)
29
+ method_type = method_type.update(type: new_type) # rubocop:disable Style/RedundantSelfAssignment
30
+
31
+ AST::Members::MethodDefinition::Overload.new(method_type:, annotations: [])
32
+ end
33
+
34
+ private
35
+
36
+ def stack_traces
37
+ @stack_traces ||= {} #: Hash[Thread, Array[__todo__]]
38
+ @stack_traces[Thread.current] ||= [] # steep:ignore UnannotatedEmptyCollection
39
+ end
40
+
41
+ # @rbs (Binding, Array[__todo__]) -> MethodType
42
+ def parse_parameters(bind, parameters) # rubocop:disable Metrics
43
+ fn = Types::Function.empty(type_void)
44
+
45
+ parameters.each do |kind, name| # rubocop:disable Metrics/BlockLength
46
+ case kind
47
+ when :req
48
+ value = bind.local_variable_get(name)
49
+ fn.required_positionals << Types::Function::Param.new(name: nil, type: parse_object(value))
50
+ when :opt
51
+ value = bind.local_variable_get(name)
52
+ fn.optional_positionals << Types::Function::Param.new(name: nil, type: parse_object(value))
53
+ when :rest
54
+ type = if name.nil? || name == :*
55
+ type_untyped
56
+ else
57
+ value = bind.local_variable_get(name)
58
+ parse_classes(value.map { |v| obj_to_class(v) }.uniq)
59
+ end
60
+ fn = fn.update(rest_positionals: Types::Function::Param.new(name: nil, type:)) # rubocop:disable Style/RedundantSelfAssignment
61
+ when :keyreq
62
+ value = bind.local_variable_get(name)
63
+ fn.required_keywords[name] = Types::Function::Param.new(name: nil, type: parse_object(value))
64
+ when :key
65
+ value = bind.local_variable_get(name)
66
+ fn.optional_keywords[name] = Types::Function::Param.new(name: nil, type: parse_object(value))
67
+ when :keyrest
68
+ type = if name.nil? || name == :**
69
+ type_untyped
70
+ else
71
+ value = bind.local_variable_get(name)
72
+ parse_classes(value.values.map { |v| obj_to_class(v) }.uniq)
73
+ end
74
+ fn = fn.update(rest_keywords: Types::Function::Param.new(name: nil, type:)) # rubocop:disable Style/RedundantSelfAssignment
75
+ when :block
76
+ # TODO: support block argument
77
+ next
78
+ end
79
+ end
80
+
81
+ MethodType.new(
82
+ type_params: [],
83
+ type: fn,
84
+ block: nil,
85
+ location: nil
86
+ )
87
+ end
88
+
89
+ # @rbs (Array[untyped]) -> Types::t
90
+ def parse_classes(classes)
91
+ types = classes.filter_map { |klass| parse_class(klass) unless klass == NilClass }.uniq
92
+ return type_nil if types.empty?
93
+
94
+ type = types.one? ? types.first : Types::Union.new(types:, location: nil) #: Types::t
95
+ if classes.include?(NilClass)
96
+ Types::Optional.new(type:, location: nil)
97
+ else
98
+ type
99
+ end
100
+ end
101
+
102
+ # @rbs (untyped) -> Types::t
103
+ def parse_class(klass) # rubocop:disable Metrics/MethodLength
104
+ if [TrueClass, FalseClass].include?(klass)
105
+ type_bool
106
+ elsif klass == NilClass
107
+ type_nil
108
+ elsif klass == Object || klass.name.nil?
109
+ type_untyped
110
+ else
111
+ size = GENERICS_SIZE[klass].to_i
112
+ args = Array.new(size) { type_untyped }
113
+ Types::ClassInstance.new(name: TypeName.parse(klass.name), args:, location: nil)
114
+ end
115
+ end
116
+
117
+ # @rbs (BasicObject) -> Types::t
118
+ def parse_object(object)
119
+ klass = obj_to_class(object)
120
+ parse_class(klass)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ class Trace
5
+ class CLI
6
+ class Inline
7
+ BANNER = <<~USAGE
8
+ Usage: rbs-trace inline --sig-dir=DIR --rb-dir=DIR
9
+
10
+ Insert RBS inline comments from RBS files.
11
+
12
+ Examples:
13
+ # Insert inline comments to `app/**/*.rb`.
14
+ $ rbs-trace inline --sig-dir=sig --rb-dir=app
15
+
16
+ # Generate RBS files with rbs-inline.
17
+ $ rbs-inline --output --opt-out app
18
+
19
+ # Remove method definitions that have been migrated to inline comments.
20
+ $ rbs subtract --write sig sig/generated/
21
+
22
+ Options:
23
+ USAGE
24
+
25
+ # @rbs (Array[String]) -> void
26
+ def run(args) # rubocop:disable Metrics
27
+ sig_dir = Pathname.pwd.join("sig").to_s
28
+ rb_dirs = [] #: Array[String]
29
+
30
+ opts = OptionParser.new(BANNER)
31
+ opts.on("--sig-dir DIR") { |dir| sig_dir = dir }
32
+ opts.on("--rb-dir DIR") { |dir| rb_dirs << dir }
33
+ opts.parse!(args)
34
+
35
+ if rb_dirs.empty?
36
+ puts opts.help
37
+ else
38
+ env = load_env(sig_dir) # steep:ignore ArgumentTypeMismatch
39
+ decls = env.class_decls.transform_values { |v| v.primary.decl }
40
+
41
+ rb_files = rb_dirs.flat_map { |rb_dir| Dir.glob("#{rb_dir}/**/*.rb") }
42
+ rb_files.each do |path|
43
+ file = File.new(path, decls)
44
+ file.rewrite
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # @rbs (String) -> Environment
52
+ def load_env(dir)
53
+ Environment.new.tap do |env|
54
+ loader = EnvironmentLoader.new(core_root: nil)
55
+ loader.add(path: Pathname(dir))
56
+ loader.load(env:)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ class Trace
5
+ class CLI
6
+ class Merge
7
+ BANNER = <<~USAGE
8
+ Usage: rbs-trace merge --sig-dir=DIR
9
+
10
+ Merge multiple RBS files into one.
11
+
12
+ Examples:
13
+ # Merge RBS files in `tmp/sig-1/` and `tmp/sig-2/`.
14
+ $ rbs-trace merge --sig-dir=tmp/sig-1 --sig-dir=tmp/sig-2
15
+
16
+ # Or you can specify a glob pattern.
17
+ $ rbs-trace merge --sig-dir=tmp/sig-*
18
+
19
+ Options:
20
+ USAGE
21
+
22
+ # @rbs (Array[String]) -> void
23
+ def run(args) # rubocop:disable Metrics
24
+ sig_dirs = [] #: Array[String]
25
+
26
+ opts = OptionParser.new(BANNER)
27
+ opts.on("--sig-dir DIR") { |dir| sig_dirs << dir }
28
+ opts.parse!(args)
29
+
30
+ if sig_dirs.empty?
31
+ puts opts.help
32
+ else
33
+ envs = sig_dirs.flat_map { |sig_dir| Dir.glob(sig_dir) }
34
+ .map { |dir| load_env(dir) }
35
+ env = merge_envs(envs)
36
+
37
+ out = StringIO.new
38
+ writer = Writer.new(out:)
39
+ writer.write(env.declarations)
40
+
41
+ puts out.string
42
+ end
43
+ end
44
+
45
+ # @rbs (Array[Environment]) -> Environment
46
+ def merge_envs(others) # rubocop:disable Metrics
47
+ Environment.new.tap do |env|
48
+ others.each do |other|
49
+ other.declarations.each do |decl|
50
+ next unless decl.is_a?(AST::Declarations::Class) || decl.is_a?(AST::Declarations::Module)
51
+
52
+ entry = env.module_class_entry(decl.name.absolute!)
53
+
54
+ if entry.is_a?(Environment::MultiEntry)
55
+ decl.members.each { |member| merge(entry.primary.decl, member) }
56
+ else
57
+ env << decl
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # @rbs (String) -> Environment
67
+ def load_env(dir)
68
+ Environment.new.tap do |env|
69
+ loader = EnvironmentLoader.new(core_root: nil)
70
+ loader.add(path: Pathname(dir))
71
+ loader.load(env:)
72
+ end
73
+ end
74
+
75
+ def merge(decl, member) # rubocop:disable Metrics
76
+ case member
77
+ when AST::Declarations::Class, AST::Declarations::Module
78
+ d = decl.members.find { |m| m.is_a?(member.class) && m.name == member.name }
79
+
80
+ if d
81
+ member.members.each { |m| merge(d, m) }
82
+ else
83
+ decl.members << member
84
+ end
85
+ when AST::Members::MethodDefinition
86
+ found = decl.members.find { |m| m.is_a?(member.class) && m.name == member.name && m.kind == member.kind }
87
+
88
+ if found
89
+ (member.overloads - found.overloads).each do |overload|
90
+ found.overloads << overload
91
+ end
92
+ else
93
+ decl.members << member
94
+ end
95
+ else
96
+ decl.members << member unless decl.members.include?(member)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ class Trace
5
+ class CLI
6
+ BANNER = <<~USAGE
7
+ Usage: rbs-trace <command> [<args>]
8
+
9
+ Available commands: inline, merge
10
+ USAGE
11
+
12
+ # @rbs (Array[String]) -> void
13
+ def run(args = ARGV)
14
+ opts = OptionParser.new(BANNER)
15
+ opts.version = RBS::Trace::VERSION
16
+ opts.order!(args)
17
+ command = args.shift&.to_sym
18
+
19
+ klass = command_class(command)
20
+ if klass
21
+ klass.new.run(args)
22
+ else
23
+ puts opts.help
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # @rbs (Symbol?) -> (singleton(Inline) | singleton(Merge))?
30
+ def command_class(command)
31
+ case command
32
+ when :inline
33
+ Inline
34
+ when :merge
35
+ Merge
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,29 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
3
5
  module RBS
4
- module Trace
6
+ class Trace
5
7
  class File
6
- # @rbs (String) -> void
7
- def initialize(path)
8
+ include Helpers
9
+
10
+ # @rbs (String, ?Hash[TypeName, AST::Declarations::t]) -> void
11
+ def initialize(path, decls = {})
8
12
  @path = path
13
+ @decls = decls
14
+ @members = {} #: Hash[TypeName, AST::Declarations::t]
9
15
  end
10
16
 
11
- # @rbs () -> Hash[String, Definition]
12
- def definitions
13
- @definitions ||= {}
17
+ # @rbs (untyped, Class, Symbol) -> AST::Members::MethodDefinition?
18
+ def find_or_new_method_definition(object, defined_class, name)
19
+ klass = defined_class.singleton_class? ? object : defined_class
20
+ receiver_decl = find_or_new_receiver_decl(klass)
21
+ return unless receiver_decl
22
+
23
+ kind = defined_class.singleton_class? ? :singleton : :instance
24
+ key = [receiver_decl.name, name, kind]
25
+ @members[key] ||= new_method_definition(name:, kind:).tap do |member| # steep:ignore ArgumentTypeMismatch
26
+ receiver_decl.members << member
27
+ end
14
28
  end
15
29
 
16
30
  # @rbs () -> String
17
31
  def with_rbs
18
- lines = ::File.readlines(@path)
19
- reverse_definitions.each do |d|
20
- next if skip_insert?(lines, d)
32
+ result = Prism.parse_file(@path)
33
+ comments = {} #: Hash[Integer, String]
34
+ result.value.accept(InlineCommentVisitor.new(@decls, comments))
21
35
 
22
- current = d.lineno - 1
23
- indent = lines[current]&.index("def")
24
- next unless indent
36
+ lines = result.source.source.lines
37
+ comments.keys.sort.reverse_each do |i|
38
+ next if skip_insert?(lines, i)
25
39
 
26
- lines.insert(current, d.rbs_comment(indent))
40
+ lines.insert(i, comments[i])
27
41
  end
28
42
  lines.join
29
43
  end
@@ -33,22 +47,53 @@ module RBS
33
47
  ::File.write(@path, with_rbs)
34
48
  end
35
49
 
50
+ # @rbs () -> String
51
+ def to_rbs
52
+ out = StringIO.new
53
+ writer = Writer.new(out:)
54
+ writer.write(@decls.values)
55
+
56
+ out.string
57
+ end
58
+
59
+ # @rbs (String) -> void
60
+ def save_rbs(out_dir)
61
+ rbs_path = calc_rbs_path(out_dir)
62
+
63
+ rbs_path.parent.mkpath unless rbs_path.parent.exist?
64
+ rbs_path.write(to_rbs)
65
+ end
66
+
36
67
  private
37
68
 
38
- # @rbs (Array[String], Definition) -> boolish
39
- def skip_insert?(lines, definition)
40
- current = definition.lineno - 1
69
+ # @rbs (Array[String], Integer) -> boolish
70
+ def skip_insert?(lines, current)
41
71
  prev = current - 1
42
72
 
43
- definition.decls.empty? ||
44
- lines[prev]&.include?("# @rbs") ||
73
+ lines[prev]&.include?("# @rbs") ||
45
74
  lines[prev]&.include?("#:") ||
46
75
  lines[current]&.include?("#:")
47
76
  end
48
77
 
49
- # @rbs () -> Enumerator[Definition, void]
50
- def reverse_definitions
51
- @definitions.values.sort_by { |d| -d.lineno }
78
+ # @rbs (Class | Module) -> (AST::Declarations::Class | AST::Declarations::Module)?
79
+ def find_or_new_receiver_decl(klass)
80
+ return unless klass.name
81
+ return if klass.name.is_a?(Symbol)
82
+
83
+ # Remove anonymous class names
84
+ class_name = klass.name.to_s.split("::").grep_v(/^#/).join("::")
85
+ name = TypeName.parse(class_name)
86
+
87
+ @decls[name.absolute!] ||= klass.is_a?(Class) ? new_class_decl(name:) : new_module_decl(name:)
88
+ end
89
+
90
+ # @rbs (String) -> Pathname
91
+ def calc_rbs_path(out_dir)
92
+ rb_path = Pathname(@path).sub(Pathname.pwd.to_s, "")
93
+ rb_path = rb_path.relative_path_from("/") if rb_path.absolute?
94
+ rbs_path = rb_path.sub_ext(".rbs")
95
+
96
+ Pathname(out_dir) + rbs_path
52
97
  end
53
98
  end
54
99
  end