rbs-inline-annotator 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b957db6fbd29cfa5e3b4b5613386beeeb3aebf40f7a576e01a95e31fa88267b3
4
+ data.tar.gz: 7d6a684f4c95daec4926e3f2500d55b294d33b423cda307c1af0276db2400390
5
+ SHA512:
6
+ metadata.gz: 6bd9cabe241ca29a157fc3bf91cbe962fe182d629d9417c571a98db6117f61f331327bd4d713340272a63d0d228aa467596da3d9a5478c2e24ad09910d46d1ed
7
+ data.tar.gz: 0a432b269bb9d2ce12618bfa5eb766db14ea987b68135115f7c636c6d4cba2390b04707c1edbac6df96ad2f270e979d9b21c831e2965ac8208d419d46036c0f1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-09-29
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 ksss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # RBS::Inline::Annotator
2
+
3
+ Add rbs-inline annotation to ruby code by rbs code.
4
+
5
+ ```rb
6
+ # lib/foo.rb
7
+ class Foo
8
+ def foo(a)
9
+ a.to_s
10
+ end
11
+ end
12
+ ```
13
+
14
+ \+
15
+
16
+ ```rbs
17
+ # sig/foo.rbs
18
+ class Foo
19
+ def foo: (Integer) -> String
20
+ end
21
+ ```
22
+
23
+ =
24
+
25
+ ```rb
26
+ # lib/foo.rb
27
+ class Foo
28
+ # @rbs a: Integer
29
+ # @rbs return: String
30
+ def foo(a)
31
+ a.to_s
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
39
+
40
+ Install the gem and add to the application's Gemfile by executing:
41
+
42
+ ```bash
43
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
44
+ ```
45
+
46
+ If bundler is not being used to manage dependencies, install the gem by executing:
47
+
48
+ ```bash
49
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Add rbs-inline annotation to ruby file
55
+
56
+ ```shell
57
+ $ bundle exec rbs-inline-annotator -I sig target_dir_or_file
58
+ ```
59
+
60
+ ### Print result code to stdout only
61
+
62
+ ```shell
63
+ $ bundle exec rbs-inline-annotator -I sig --mode print-only target_dir_or_file
64
+ ```
65
+
66
+ ## Development
67
+
68
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
69
+
70
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ksss/rbs-inline-annotator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ksss/rbs-inline-annotator/blob/main/CODE_OF_CONDUCT.md).
75
+
76
+ ## License
77
+
78
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
79
+
80
+ ## Code of Conduct
81
+
82
+ Everyone interacting in the Rbs::Inline::Annotator project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ksss/rbs-inline-annotator/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rbs/inline/annotator"
5
+
6
+ exit RBS::Inline::Annotator::CLI.new(ARGV).run
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module RBS::Inline::Annotator
6
+ class CLI
7
+ class Spinner
8
+ DOTS = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
9
+
10
+ def initialize
11
+ @index = 0
12
+ end
13
+
14
+ def tick
15
+ @index = (@index + 1) % DOTS.size
16
+ DOTS[@index]
17
+ end
18
+ end
19
+
20
+ Options = Struct.new(:mode)
21
+
22
+ def initialize(argv)
23
+ @loader = RBS::EnvironmentLoader.new(core_root: nil)
24
+ @argv = argv
25
+ @options = Options.new(
26
+ mode: 'write'
27
+ )
28
+ OptionParser.new do |opt|
29
+ opt.on("-I DIR", "Load RBS files from the directory") do |dir|
30
+ @loader.add(path: Pathname(dir))
31
+ end
32
+ opt.on("-m", "--mode MODE", "Mode [quiet, print-only, write] (default: write)") do |mode|
33
+ @options.mode = mode
34
+ end
35
+ end.parse!(@argv)
36
+ end
37
+
38
+ def run
39
+ env = RBS::Environment.from_loader(@loader)
40
+ targets = @argv.flat_map { Pathname.glob(_1) }.flat_map do |path|
41
+ if path.directory?
42
+ pattern = path / "**/*.rb"
43
+ Pathname.glob(pattern.to_s)
44
+ else
45
+ path
46
+ end
47
+ end
48
+
49
+ targets.sort!
50
+ targets.uniq!
51
+
52
+ Spinner.new.tap do |spinner|
53
+ print "\e[?25l" if @options.mode == 'write'
54
+ targets.each do |target|
55
+ annotated_code = Processor.new(target:, env:).process
56
+ case @options.mode
57
+ when 'write'
58
+ File.write(target, annotated_code)
59
+ when 'print-only'
60
+ puts annotated_code
61
+ when 'quiet'
62
+ # do nothing
63
+ else
64
+ raise "invalid mode: #{@options.mode}"
65
+ end
66
+ print "\r#{spinner.tick}" if @options.mode == 'write'
67
+ end
68
+ ensure
69
+ print "\e[?25h" if @options.mode == 'write'
70
+ end
71
+
72
+ puts "\rDone!" if @options.mode == 'write'
73
+
74
+ 0 # exit code
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ module RBS::Inline::Annotator
2
+ class Processor
3
+ class Result
4
+ attr_reader :writer, :prism_result, :actions, :diagnostics
5
+
6
+ def initialize(writer:, prism_result:)
7
+ @writer = writer
8
+ @prism_result = prism_result
9
+ @diagnostics = []
10
+ end
11
+ end
12
+
13
+ attr_reader :target, :env
14
+
15
+ def initialize(target:, env:)
16
+ @target = target
17
+ @env = env
18
+ end
19
+
20
+ def process
21
+ absolute_path = Pathname.pwd + target
22
+ source = absolute_path.read
23
+ writer = Writer.new(source)
24
+ result = Result.new(writer:, prism_result: Prism.parse_file(absolute_path.to_s))
25
+ result.prism_result.value.accept(Visitor.new(env:, result:))
26
+ return if result.writer.actions.empty?
27
+
28
+ writer.process
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ module Inline
5
+ module Annotator
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,439 @@
1
+ module RBS::Inline::Annotator
2
+ class Visitor < Prism::Visitor
3
+ def initialize(env:, result:)
4
+ @env = env
5
+ @result = result
6
+ @stack = []
7
+ @kind = :instance
8
+ super()
9
+ end
10
+
11
+ def insert_before(range, text)
12
+ @result.writer.insert_before(range:, text:)
13
+ end
14
+
15
+ def insert_after(range, text)
16
+ @result.writer.insert_after(range:, text:)
17
+ end
18
+
19
+ def replace(range, text)
20
+ @result.writer.replace(range:, text:)
21
+ end
22
+
23
+ def remove(range)
24
+ replace(range, "")
25
+ end
26
+
27
+ def node_range(node)
28
+ Range.new(
29
+ node.location.start_offset,
30
+ node.location.end_offset,
31
+ )
32
+ end
33
+
34
+ def visit_singleton_class_node(node)
35
+ if @kind == :singleton
36
+ warn "nested singleton class detected"
37
+ return
38
+ end
39
+
40
+ @kind = :singleton
41
+ visit_child_nodes(node)
42
+ ensure
43
+ @kind = :instance
44
+ end
45
+
46
+ def module_class_entry
47
+ @env.module_class_entry(type_name)
48
+ end
49
+
50
+ def visit_class_node(node)
51
+ push_type_name(node) do
52
+ with_superclass(node.superclass)
53
+ # TODO: Which file should we write to?
54
+ # with_embedding_rbs(header_node(node), node)
55
+ # with_variables(header_node(node), node)
56
+ visit_child_nodes(node)
57
+ end
58
+ end
59
+
60
+ def visit_module_node(node)
61
+ push_type_name(node) do
62
+ with_module_self(node)
63
+ # TODO: Which file should we write to?
64
+ # with_embedding_rbs(header_node(node), node)
65
+ # with_variables(header_node(node), node)
66
+ visit_child_nodes(node)
67
+ end
68
+ end
69
+
70
+ def header_node(node)
71
+ case node
72
+ when Prism::ClassNode
73
+ node.superclass ? node.superclass : node.constant_path
74
+ when Prism::ModuleNode
75
+ node.constant_path
76
+ end
77
+ end
78
+
79
+ def with_embedding_rbs(header_node, node)
80
+ entry = module_class_entry or return
81
+ indent = " " * (node.body ? node.body.location.start_column : node.location.start_column + 2)
82
+ embedding_rbs = []
83
+ entry.each_decl do |decl|
84
+ decl.members.each do |member|
85
+ case member
86
+ when RBS::AST::Declarations::Interface, RBS::AST::Declarations::TypeAlias
87
+ embedding_rbs << member
88
+ end
89
+ end
90
+ end
91
+
92
+ if embedding_rbs.any?
93
+ header_range = node_range(header_node)
94
+ insert_after(header_range, "\n#{indent}# @rbs!\n")
95
+ embedding_rbs.each do |member|
96
+ case member
97
+ when RBS::AST::Declarations::Interface
98
+ insert_after(header_range, "#{indent}# interface #{member.name}\n")
99
+ member.members.each do |m|
100
+ insert_after(header_range, "#{indent}# #{m.location.source.strip}\n")
101
+ end
102
+ insert_after(header_range, "#{indent}# end\n")
103
+ when RBS::AST::Declarations::TypeAlias
104
+ insert_after(header_range, "#{indent}# type #{member.name} = #{member.type}\n")
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def with_superclass(node)
111
+ node or return
112
+ entry = module_class_entry or return
113
+ super_class_decl = entry.primary_decl.super_class or return
114
+ return unless super_class_decl.args.any?
115
+
116
+ args = super_class_decl.args.join(", ")
117
+ insert_after(node_range(node), " #[#{args}]")
118
+ end
119
+
120
+ def with_variables(header_node, node)
121
+ indent = " " * (node.body ? node.body.location.start_column : node.location.start_column + 2)
122
+ entry = module_class_entry or return
123
+ added = false
124
+ entry.each_decl do |decl|
125
+ decl.members.each do |member|
126
+ case member
127
+ when RBS::AST::Members::InstanceVariable
128
+ insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
129
+ added = true
130
+ when RBS::AST::Members::ClassVariable
131
+ insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
132
+ added = true
133
+ when RBS::AST::Members::ClassInstanceVariable
134
+ insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
135
+ added = true
136
+ end
137
+ end
138
+ end
139
+ if added && node.body&.body&.first
140
+ insert_before(node_range(node.body.body.first), "\n#{indent}")
141
+ end
142
+ end
143
+
144
+ def with_module_self(node)
145
+ entry = module_class_entry or return
146
+ self_types = entry.primary_decl.self_types
147
+ if self_types.any?
148
+ indent = " " * node.location.start_column
149
+ insert_before(node_range(node), "# @rbs module-self #{self_types.join(", ")}\n#{indent}")
150
+ end
151
+ end
152
+
153
+ def visit_def_node(node)
154
+ module_class_entry&.each_decl do |decl|
155
+ decl.members.each do |member|
156
+ case member
157
+ when RBS::AST::Members::MethodDefinition
158
+ next unless node.name == member.name
159
+
160
+ if node.receiver.nil? && @kind == :instance
161
+ # def foo
162
+ next unless member.instance?
163
+ elsif node.receiver.is_a?(Prism::SelfNode) || @kind == :singleton
164
+ # def self.foo
165
+ next unless member.singleton?
166
+ else
167
+ next
168
+ end
169
+
170
+ add_rbs_inline_annotation_for_def_node(node:, method_definition: member)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def add_rbs_inline_annotation_for_def_node(node:, method_definition:)
177
+ indent = " " * node.location.start_column
178
+
179
+ if method_definition.annotations.any?
180
+ method_definition.annotations.each do |a|
181
+ insert_before(node_range(node), "# @rbs %a{#{a.string}}\n#{indent}")
182
+ end
183
+ end
184
+
185
+ if method_definition.overloading
186
+ insert_before(node_range(node), "# @rbs overload\n#{indent}")
187
+ return
188
+ end
189
+
190
+ if method_definition.overloads.length > 1
191
+ # multiple overloads
192
+ method_definition.overloads.each_with_index do |overload, index|
193
+ text = if index == 0
194
+ "# @rbs #{overload.method_type}\n#{indent}"
195
+ else
196
+ "# | #{overload.method_type}\n#{indent}"
197
+ end
198
+
199
+ insert_before(node_range(node), text)
200
+ end
201
+ else
202
+ overload = method_definition.overloads.first
203
+ method_type = overload.method_type
204
+ func = method_type.type
205
+ indent = " " * node.location.start_column
206
+ new_annotation = lambda { |name, type|
207
+ insert_before(node_range(node), "# @rbs #{name}: #{type}\n#{indent}")
208
+ }
209
+ for_positional_params = lambda { |orig_sig, orig_ruby|
210
+ sig = orig_sig.dup or break
211
+ ruby = orig_ruby.dup or break
212
+ sig.each do |rbs|
213
+ rb = ruby.shift
214
+ if rbs.name
215
+ name = rbs.name
216
+ else
217
+ if rb.nil?
218
+ s = (node.receiver.is_a?(Prism::SelfNode) || @kind == :singleton) ? "." : "#"
219
+ warn "Parameter mismatch #{type_name}#{s}#{node.name}"
220
+ break
221
+ end
222
+ name = rb.name
223
+ end
224
+
225
+ type = rbs.type.to_s
226
+ next if type == "untyped"
227
+
228
+ new_annotation.call(name, type)
229
+ end
230
+ }
231
+ for_keyword_params = lambda { |orig_sig, _orig_ruby|
232
+ sig = orig_sig.dup or break
233
+ # ruby = orig_ruby.dup or break
234
+ sig.each do |name, param|
235
+ type = param.type.to_s
236
+ next if type == "untyped"
237
+
238
+ new_annotation.call(name, type)
239
+ end
240
+ }
241
+ for_rest_param = lambda { |prefix, sig, ruby|
242
+ name = sig.name || ruby.name
243
+ type = sig.type.to_s
244
+ next if type == "untyped"
245
+
246
+ new_annotation.call("#{prefix}#{name}", type)
247
+ }
248
+ for_return_param = lambda { |return_type|
249
+ return_type = return_type.to_s
250
+ break if return_type == "untyped"
251
+
252
+ new_annotation.call("return", return_type)
253
+ }
254
+ if node.parameters
255
+ for_positional_params.call(func.required_positionals, node.parameters.requireds)
256
+ for_positional_params.call(func.optional_positionals, node.parameters.optionals)
257
+ for_rest_param.call("*", func.rest_positionals, node.parameters.rest) if func.rest_positionals
258
+ for_positional_params.call(func.trailing_positionals, node.parameters.posts) if func.trailing_positionals
259
+ for_keyword_params.call(func.required_keywords, node.parameters.keywords.grep(Prism::RequiredKeywordParameterNode))
260
+ for_keyword_params.call(func.optional_keywords, node.parameters.keywords.grep(Prism::OptionalKeywordParameterNode))
261
+ for_rest_param.call("**", func.rest_keywords, node.parameters.keyword_rest) if func.rest_keywords
262
+ end
263
+ if method_type.block
264
+ name = node.parameters&.block&.name
265
+ block_source = method_type.block.location.source
266
+ # "?{ (Integer) -> Integer } -> void"
267
+ # -> "? (Integer) -> Integer"
268
+ block_source = block_source.gsub(/[{}]/, "").strip
269
+ new_annotation.call("&#{name}", block_source)
270
+ end
271
+ for_return_param.call(func.return_type)
272
+ end
273
+ end
274
+
275
+ def visit_call_node(node)
276
+ return if @stack.empty?
277
+
278
+ case node.name
279
+ when :attr_reader, :attr_writer, :attr_accessor
280
+ when_attribute_node(node)
281
+ when :include, :extend, :prepend
282
+ when_mixin_node(node)
283
+ end
284
+ end
285
+
286
+ def when_attribute_node(node)
287
+ case node.receiver
288
+ when nil, Prism::SelfNode
289
+ if node.arguments.arguments.length == 1
290
+ first_arg_node = node.arguments.arguments.first
291
+ return unless first_arg_node.is_a?(Prism::SymbolNode)
292
+
293
+ value = first_arg_node.value
294
+ module_class_entry&.each_decl do |decl|
295
+ decl.members.each do |member|
296
+ next unless (node.name == :attr_reader && member.is_a?(RBS::AST::Members::AttrReader)) ||
297
+ (node.name == :attr_writer && member.is_a?(RBS::AST::Members::AttrWriter)) ||
298
+ (node.name == :attr_accessor && member.is_a?(RBS::AST::Members::AttrAccessor))
299
+ next unless member.name == value.to_sym
300
+
301
+ type = member.type.to_s
302
+ next if type == "untyped"
303
+
304
+ insert_after(node_range(node), " #: #{type}")
305
+ end
306
+ end
307
+ else
308
+ replaced_count = 0
309
+ node.arguments.arguments.each do |arg|
310
+ next unless arg.is_a?(Prism::SymbolNode)
311
+
312
+ value = arg.value
313
+ module_class_entry&.each_decl do |decl|
314
+ decl.members.each do |member|
315
+ next unless (node.name == :attr_reader && member.is_a?(RBS::AST::Members::AttrReader)) ||
316
+ (node.name == :attr_writer && member.is_a?(RBS::AST::Members::AttrWriter)) ||
317
+ (node.name == :attr_accessor && member.is_a?(RBS::AST::Members::AttrAccessor))
318
+ next unless member.name == value.to_sym
319
+
320
+ indent = replaced_count == 0 ? "" : " " * node.location.start_column
321
+ insert_before(node_range(node), "#{indent}#{node.name} :#{value} #: #{member.type}\n")
322
+ replaced_count += 1
323
+ end
324
+ end
325
+ end
326
+
327
+ if replaced_count == node.arguments.arguments.length
328
+ range = node_range(node)
329
+ # Remove attr_*
330
+ remove(range)
331
+ # Remove the last newline
332
+ remove(Range.new(range.end, range.end + 1))
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ def when_mixin_node(node)
339
+ case node.receiver
340
+ when nil, Prism::SelfNode
341
+ if node.arguments.arguments.length == 1
342
+ first_arg_node = node.arguments.arguments.first
343
+ return unless first_arg_node.is_a?(Prism::ConstantReadNode)
344
+
345
+ name = first_arg_node.name
346
+ module_class_entry&.each_decl do |decl|
347
+ decl.members.each do |member|
348
+ next unless (node.name == :include && member.is_a?(RBS::AST::Members::Include)) ||
349
+ (node.name == :extend && member.is_a?(RBS::AST::Members::Extend)) ||
350
+ (node.name == :prepend && member.is_a?(RBS::AST::Members::Prepend))
351
+ next unless member.name.to_s == name.to_s
352
+ next unless member.args.any?
353
+
354
+ type = member.args.join(", ")
355
+ insert_after(node_range(node), " #[#{type}]")
356
+ end
357
+ end
358
+ else
359
+ replaced_count = 0
360
+ node.arguments.arguments.each do |arg|
361
+ next unless arg.is_a?(Prism::ConstantReadNode)
362
+
363
+ name = arg.name
364
+ module_class_entry&.each_decl do |decl|
365
+ decl.members.each do |member|
366
+ next unless (node.name == :include && member.is_a?(RBS::AST::Members::Include)) ||
367
+ (node.name == :extend && member.is_a?(RBS::AST::Members::Extend)) ||
368
+ (node.name == :prepend && member.is_a?(RBS::AST::Members::Prepend))
369
+ next unless member.name.to_s == name.to_s
370
+ next unless member.args.any?
371
+
372
+ indent = replaced_count == 0 ? "" : " " * node.location.start_column
373
+ insert_before(node_range(node), "#{indent}#{node.name} #{name} #[#{member.args.join(", ")}]\n")
374
+ replaced_count += 1
375
+ end
376
+ end
377
+ end
378
+
379
+ if replaced_count > 0 && replaced_count == node.arguments.arguments.length
380
+ range = node_range(node)
381
+ # Remove include *
382
+ remove(range)
383
+ # Remove the last newline
384
+ remove(Range.new(range.end, range.end + 1))
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ def visit_constant_write_node(node)
391
+ constant_type_name = RBS::TypeName.new(
392
+ name: node.name,
393
+ namespace: type_name.to_namespace
394
+ )
395
+ entry = @env.constant_decls[constant_type_name] or return
396
+ type = entry.decl.type.to_s
397
+ return if type == "untyped"
398
+
399
+ add_rbs_inline_annotation_for_trailing(node:, type:)
400
+ end
401
+
402
+ def add_rbs_inline_annotation_for_trailing(node:, type:)
403
+ return if type == "untyped"
404
+
405
+ insert_after(node_range(node), " #: #{type}")
406
+ end
407
+
408
+ def push_type_name(node)
409
+ parts = node.constant_path.full_name_parts.dup
410
+ @stack.push(parts)
411
+
412
+ yield
413
+ ensure
414
+ @stack.pop
415
+ end
416
+
417
+ def type_name
418
+ if @stack.empty?
419
+ nil
420
+ else
421
+ absolute = false
422
+ names = []
423
+ @stack.reverse_each do |parts|
424
+ names.unshift(*parts)
425
+ if names.first == :""
426
+ absolute = true
427
+ names.shift
428
+ break
429
+ end
430
+ end
431
+ *path, name = names.map(&:to_sym)
432
+ RBS::TypeName.new(
433
+ name: name,
434
+ namespace: RBS::Namespace.new(path:, absolute:)
435
+ ).absolute!
436
+ end
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,97 @@
1
+ module RBS::Inline::Annotator
2
+ # action:
3
+ # insert_before(range, "# @rbs foo: Integer")
4
+ # insert_before(range, "# @rbs bar: Integer")
5
+ # expect:
6
+ # # @rbs foo: Integer
7
+ # # @rbs bar: Integer
8
+ # def foo(foo, bar)
9
+ class Writer
10
+ class Action
11
+ attr_reader :range, :text
12
+
13
+ def initialize(range:, text:)
14
+ @range = range
15
+ @text = text
16
+ end
17
+ end
18
+
19
+ class InsertBefore < Action
20
+ def process(writer)
21
+ if writer.before_pos < range.begin
22
+ writer.slice << writer.source[writer.before_pos...range.begin]
23
+ writer.before_pos = range.begin
24
+ elsif writer.before_pos == range.begin
25
+ # do nothing
26
+ else
27
+ raise "invalid range: #{range}, before_pos: #{writer.before_pos}"
28
+ end
29
+ writer.slice << text
30
+ end
31
+ end
32
+
33
+ class InsertAfter < Action
34
+ def process(writer)
35
+ if writer.before_pos < range.end
36
+ writer.slice << writer.source[writer.before_pos...range.end]
37
+ writer.before_pos = range.end
38
+ elsif writer.before_pos == range.end - 1
39
+ # do nothing
40
+ else
41
+ raise "invalid range: #{range}, before_pos: #{writer.before_pos}"
42
+ end
43
+ writer.slice << text
44
+ end
45
+ end
46
+
47
+ class Replace < Action
48
+ def process(writer)
49
+ if writer.before_pos < range.begin
50
+ writer.slice << writer.source[writer.before_pos...range.begin]
51
+ writer.before_pos = range.end
52
+ elsif writer.before_pos == range.begin
53
+ writer.before_pos = range.end
54
+ else
55
+ raise "invalid range: #{range}, before_pos: #{writer.before_pos}"
56
+ end
57
+ writer.slice << text
58
+ end
59
+ end
60
+
61
+ attr_reader :source, :actions, :slice
62
+ attr_accessor :before_pos
63
+
64
+ def initialize(source)
65
+ @source = source
66
+ @actions = []
67
+ @slice = []
68
+ @before_pos = 0
69
+ end
70
+
71
+ def insert_before(range:, text:)
72
+ actions << InsertBefore.new(range:, text:)
73
+ end
74
+
75
+ def insert_after(range:, text:)
76
+ actions << InsertAfter.new(range:, text:)
77
+ end
78
+
79
+ def replace(range:, text:)
80
+ actions << Replace.new(range:, text:)
81
+ end
82
+
83
+ def process
84
+ actions.sort_by { |action|
85
+ action.range.begin
86
+ }.each do |action|
87
+ action.process(self)
88
+ end
89
+
90
+ if before_pos < source.length
91
+ slice << source[before_pos..]
92
+ end
93
+
94
+ slice.join
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "rbs"
5
+ require "parser"
6
+
7
+ require_relative "annotator/version"
8
+ require_relative "annotator/cli"
9
+ require_relative "annotator/processor"
10
+ require_relative "annotator/visitor"
11
+ require_relative "annotator/writer"
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbs-inline-annotator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ksss
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rbs
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0.dev.4
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 4.0.0.dev.4
40
+ description: RBS inline annotator from RBS
41
+ email:
42
+ - co000ri@gmail.com
43
+ executables:
44
+ - rbs-inline-annotator
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - CODE_OF_CONDUCT.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - exe/rbs-inline-annotator
53
+ - lib/rbs/inline/annotator.rb
54
+ - lib/rbs/inline/annotator/cli.rb
55
+ - lib/rbs/inline/annotator/processor.rb
56
+ - lib/rbs/inline/annotator/version.rb
57
+ - lib/rbs/inline/annotator/visitor.rb
58
+ - lib/rbs/inline/annotator/writer.rb
59
+ homepage: https://github.com/ksss/rbs-inline-annotator
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/ksss/rbs-inline-annotator
64
+ source_code_uri: https://github.com/ksss/rbs-inline-annotator
65
+ changelog_uri: https://github.com/ksss/rbs-inline-annotator
66
+ rubygems_mfa_required: 'true'
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 3.2.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.6.9
82
+ specification_version: 4
83
+ summary: RBS inline annotator from RBS
84
+ test_files: []