yarbs 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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/Rakefile +14 -0
- data/exe/yarbs +6 -0
- data/lib/yarbs/annotator.rb +274 -0
- data/lib/yarbs/cli.rb +59 -0
- data/lib/yarbs/generator.rb +72 -0
- data/lib/yarbs/prototype_builder.rb +22 -0
- data/lib/yarbs/type_converter.rb +98 -0
- data/lib/yarbs/version.rb +5 -0
- data/lib/yarbs/watcher.rb +92 -0
- data/lib/yarbs.rb +28 -0
- data/sig/yarbs/annotator.rbs +75 -0
- data/sig/yarbs/cli.rbs +10 -0
- data/sig/yarbs/generator.rbs +35 -0
- data/sig/yarbs/prototype_builder.rbs +14 -0
- data/sig/yarbs/type_converter.rbs +36 -0
- data/sig/yarbs/version.rbs +3 -0
- data/sig/yarbs/watcher.rbs +40 -0
- data/sig/yarbs.rbs +14 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 14192c2d967c20c40a4ed4e9d24d810b6bcaf8fa957261c27d904301f5d6e980
|
|
4
|
+
data.tar.gz: 7a2bc9f86103deaf360be3c6fabcd5895d6bca0280ab1f0c95a426921852448f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 50961f4fa46908f161a8748602fa2a282cbb00a018a44d7f701b96043e7ceb1332359526318359efd7d86cdce8fa944aac2a442d7daedd70a7caa5eb0e24b74e
|
|
7
|
+
data.tar.gz: 5f59a649d92e618223e4b60bd23d05b7e2a7f68a08818a70083bf17b5981e2e11cf2bc2a55f9821528fc1359c36da04d976cb4b1db021209b79d98dc119e16d3
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tobidelius
|
|
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,131 @@
|
|
|
1
|
+
# Yarbs
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tobidelius/yarbs/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
*This is still experimental.*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Generate [RBS](https://github.com/ruby/rbs) type signatures from your
|
|
10
|
+
existing [YARD](https://yardoc.org) documentation.
|
|
11
|
+
|
|
12
|
+
Most Ruby projects that adopt RBS end up writing types twice: once as
|
|
13
|
+
`@param`/`@return` comments for humans, and again as `.rbs` files for the
|
|
14
|
+
type checker. yarbs closes that gap. It uses `rbs prototype rb` to work out
|
|
15
|
+
the real *structure* of your code (arity, visibility, `attr_*`, mixins,
|
|
16
|
+
inheritance — all parsed straight from your source, not guessed), then
|
|
17
|
+
reads the YARD docs you've already written to fill in the *types*, and
|
|
18
|
+
writes the result to `sig/`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Greeter
|
|
22
|
+
# @param name [String] the person's name
|
|
23
|
+
# @param count [Integer, nil] how many times to greet
|
|
24
|
+
# @return [Boolean] whether it worked
|
|
25
|
+
def greet(name, count: nil)
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ yarbs "lib/**/*.rb"
|
|
33
|
+
wrote sig/greeter.rbs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```rbs
|
|
37
|
+
class Greeter
|
|
38
|
+
def greet: (String name, ?count: Integer | nil) -> bool
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
No YARD tag for a given parameter or return value? It's just left `untyped`
|
|
43
|
+
— nothing breaks, and everything else in the signature still gets its real
|
|
44
|
+
type.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Install the gem and add it to the application's Gemfile by executing:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle add yarbs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If bundler is not being used to manage dependencies, install the gem by
|
|
55
|
+
executing:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gem install yarbs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
Point yarbs at your source files with one or more glob patterns; it writes
|
|
64
|
+
one `.rbs` file per source file into `sig/` (or `-o DIR` to choose a
|
|
65
|
+
different output directory), mirroring the source layout with a leading
|
|
66
|
+
`lib/`/`app/` stripped:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
yarbs "lib/**/*.rb" "app/**/*.rb"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Other flags:
|
|
73
|
+
|
|
74
|
+
- `-o`, `--output DIR` — write into `DIR` instead of `sig/`
|
|
75
|
+
- `-w`, `--watch` — regenerate automatically whenever a matched file changes
|
|
76
|
+
(built on [listen](https://github.com/guard/listen)); handy to run
|
|
77
|
+
alongside your editor while developing
|
|
78
|
+
- `-s`, `--strict` — raise instead of silently falling back to `untyped`
|
|
79
|
+
when a YARD type can't be converted or a file has invalid syntax (not
|
|
80
|
+
combinable with `--watch`, since a strict watcher would crash on every
|
|
81
|
+
keystroke mid-edit)
|
|
82
|
+
|
|
83
|
+
It's also available as a plain Ruby API:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
require "yarbs"
|
|
87
|
+
|
|
88
|
+
Yarbs.generate(["lib/**/*.rb"], output_dir: "sig", strict: true)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Docs
|
|
92
|
+
|
|
93
|
+
For the full picture of what yarbs understands and how to document it —
|
|
94
|
+
worked examples generated by actually running the tool, not hand-written —
|
|
95
|
+
see:
|
|
96
|
+
|
|
97
|
+
- [docs/param.md](docs/param.md) — `@param`, including `*args`, `**kwargs`,
|
|
98
|
+
and `@option` (typed as an RBS record)
|
|
99
|
+
- [docs/return.md](docs/return.md) — `@return`, including `void` and the
|
|
100
|
+
`#initialize` special case
|
|
101
|
+
- [docs/blocks.md](docs/blocks.md) — `yield`/`&block`
|
|
102
|
+
(`@yieldparam`/`@yieldreturn`), and typing a `Proc`/lambda parameter
|
|
103
|
+
- [docs/classes.md](docs/classes.md) — classes, modules, nesting,
|
|
104
|
+
inheritance, mixins, `attr_*`, constants, and visibility
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
After checking out the repo, run `bundle install`, then
|
|
109
|
+
`bundle exec rbs collection install` to fetch the RBS signatures Steep needs
|
|
110
|
+
for yarbs' own dependencies. From there:
|
|
111
|
+
|
|
112
|
+
- `bundle exec rake` runs the full check: tests (`rake test`), lint
|
|
113
|
+
(`rake standard`), and type checking (`rake steep`)
|
|
114
|
+
- `bundle exec exe/yarbs "lib/**/*.rb" -o sig` regenerates yarbs' own `sig/`
|
|
115
|
+
from its own docs (it's self-hosting)
|
|
116
|
+
|
|
117
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
118
|
+
To release a new version, update the version number in `version.rb`, and
|
|
119
|
+
then run `bundle exec rake release`, which will create a git tag for the
|
|
120
|
+
version, push git commits and the created tag, and push the `.gem` file to
|
|
121
|
+
[rubygems.org](https://rubygems.org).
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
126
|
+
https://github.com/tobidelius/yarbs.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
The gem is available as open source under the terms of the
|
|
131
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
Minitest::TestTask.create
|
|
7
|
+
|
|
8
|
+
require "standard/rake"
|
|
9
|
+
|
|
10
|
+
task :steep do
|
|
11
|
+
sh "bundle exec steep check"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task default: %i[test standard steep]
|
data/exe/yarbs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
require "yard"
|
|
5
|
+
|
|
6
|
+
module Yarbs
|
|
7
|
+
# Walks an +RBS::AST::Declarations+ tree produced by {PrototypeBuilder} and
|
|
8
|
+
# fills in its +untyped+ placeholders with types read from the matching
|
|
9
|
+
# YARD documentation (already loaded into +YARD::Registry+).
|
|
10
|
+
class Annotator
|
|
11
|
+
# Fills in the +untyped+ placeholders in a decls tree using YARD documentation.
|
|
12
|
+
#
|
|
13
|
+
# @param decls [Array<RBS::AST::Declarations::t>]
|
|
14
|
+
# @param strict [Boolean] raise instead of falling back to +untyped+
|
|
15
|
+
# when a YARD type can't be converted
|
|
16
|
+
# @return [Array<RBS::AST::Declarations::t>] a new tree with YARD-derived types merged in
|
|
17
|
+
def self.annotate(decls, strict: false)
|
|
18
|
+
new(strict).visit_all(decls, [])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(strict)
|
|
22
|
+
@strict = strict
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visit_all(decls, namespace)
|
|
26
|
+
decls.map { |decl| visit(decl, namespace) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit(decl, namespace)
|
|
30
|
+
case decl
|
|
31
|
+
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
|
|
32
|
+
inner_namespace = namespace + [decl.name.to_s]
|
|
33
|
+
decl.update(members: visit_all(decl.members, inner_namespace))
|
|
34
|
+
when RBS::AST::Declarations::Constant
|
|
35
|
+
annotate_constant(decl, namespace)
|
|
36
|
+
when RBS::AST::Members::MethodDefinition
|
|
37
|
+
annotate_method(decl, namespace)
|
|
38
|
+
when RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor
|
|
39
|
+
annotate_attribute(decl, namespace)
|
|
40
|
+
else
|
|
41
|
+
decl
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def annotate_constant(decl, namespace)
|
|
48
|
+
yard_object = YARD::Registry.at("#{namespace.join("::")}::#{decl.name}")
|
|
49
|
+
types = return_types_of(yard_object)
|
|
50
|
+
return decl if types.empty?
|
|
51
|
+
|
|
52
|
+
RBS::AST::Declarations::Constant.new(
|
|
53
|
+
name: decl.name,
|
|
54
|
+
type: convert(types),
|
|
55
|
+
location: decl.location,
|
|
56
|
+
comment: decl.comment,
|
|
57
|
+
annotations: decl.annotations
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def annotate_method(member, namespace)
|
|
62
|
+
base = namespace.join("::")
|
|
63
|
+
yard_method = YARD::Registry.at("#{base}##{member.name}") || YARD::Registry.at("#{base}.#{member.name}")
|
|
64
|
+
return member unless yard_method
|
|
65
|
+
|
|
66
|
+
overloads = member.overloads.map { |overload| annotate_overload(overload, yard_method) }
|
|
67
|
+
member.update(overloads: overloads)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def annotate_attribute(member, namespace)
|
|
71
|
+
sep = (member.kind == :singleton) ? "." : "#"
|
|
72
|
+
# attr_writer has no reader, so YARD only ever registers the `name=`
|
|
73
|
+
# method (attr_accessor has both, but they share one @return tag, so
|
|
74
|
+
# the reader's plain path works fine for it too).
|
|
75
|
+
suffix = member.is_a?(RBS::AST::Members::AttrWriter) ? "=" : ""
|
|
76
|
+
yard_attr = YARD::Registry.at("#{namespace.join("::")}#{sep}#{member.name}#{suffix}")
|
|
77
|
+
types = return_types_of(yard_attr)
|
|
78
|
+
return member if types.empty?
|
|
79
|
+
|
|
80
|
+
member.update(type: convert(types))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def annotate_overload(overload, yard_method)
|
|
84
|
+
method_type = overload.method_type
|
|
85
|
+
new_type = method_type.update(
|
|
86
|
+
type: annotate_function(method_type.type, yard_method),
|
|
87
|
+
block: method_type.block && annotate_block(method_type.block, yard_method)
|
|
88
|
+
)
|
|
89
|
+
overload.update(method_type: new_type)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def annotate_function(function, yard_method)
|
|
93
|
+
params = param_types_by_name(yard_method)
|
|
94
|
+
options = option_records_by_name(yard_method)
|
|
95
|
+
|
|
96
|
+
# @option only substitutes a record type onto a plain (non-rest) Hash
|
|
97
|
+
# parameter -- for *args/**kwargs, RBS's rest type describes a single
|
|
98
|
+
# element/value, and there's no way to express "these specific
|
|
99
|
+
# keyword names have these specific types" through it, so @option is
|
|
100
|
+
# left to apply to the base @param type there instead (see param.md).
|
|
101
|
+
RBS::Types::Function.new(
|
|
102
|
+
required_positionals: function.required_positionals.map { |param| annotate_param(param, params, options) },
|
|
103
|
+
optional_positionals: function.optional_positionals.map { |param| annotate_param(param, params, options) },
|
|
104
|
+
rest_positionals: annotate_param(function.rest_positionals, params, {}, unwrap: :array),
|
|
105
|
+
trailing_positionals: function.trailing_positionals.map { |param| annotate_param(param, params, options) },
|
|
106
|
+
required_keywords: annotate_keywords(function.required_keywords, params, options),
|
|
107
|
+
optional_keywords: annotate_keywords(function.optional_keywords, params, options),
|
|
108
|
+
rest_keywords: annotate_param(function.rest_keywords, params, {}, unwrap: :hash),
|
|
109
|
+
return_type: return_type_for(function, yard_method),
|
|
110
|
+
forwarding: function.forwarding
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def annotate_block(block, yard_method)
|
|
115
|
+
yield_params = yard_method.tags(:yieldparam)
|
|
116
|
+
yieldreturn = yard_method.tag(:yieldreturn)
|
|
117
|
+
new_return_type = ->(current) { yieldreturn&.types&.any? ? convert(yieldreturn.types) : current }
|
|
118
|
+
|
|
119
|
+
# `rbs prototype rb` can only infer a block's arity from literal
|
|
120
|
+
# `yield x, y` call sites in the method body. A block that's only
|
|
121
|
+
# ever `.call`ed (e.g. `def foo(&block); block.call(x); end`) gives
|
|
122
|
+
# it nothing to go on, so it produces an `UntypedFunction` (`(?) ->
|
|
123
|
+
# untyped`) instead of a full `Function` -- handle both.
|
|
124
|
+
new_function = case (function = block.type)
|
|
125
|
+
when RBS::Types::Function
|
|
126
|
+
annotate_block_function(function, yield_params, new_return_type)
|
|
127
|
+
when RBS::Types::UntypedFunction
|
|
128
|
+
annotate_untyped_block_function(function, yield_params, new_return_type)
|
|
129
|
+
else
|
|
130
|
+
function
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
RBS::Types::Block.new(type: new_function, required: block.required, self_type: block.self_type)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def annotate_block_function(function, yield_params, new_return_type)
|
|
137
|
+
new_required_positionals = function.required_positionals.each_with_index.map do |param, index|
|
|
138
|
+
tag = yield_params[index]
|
|
139
|
+
next param unless tag&.types
|
|
140
|
+
|
|
141
|
+
RBS::Types::Function::Param.new(name: param.name || safe_symbol(tag.name), type: convert(tag.types))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
RBS::Types::Function.new(
|
|
145
|
+
required_positionals: new_required_positionals,
|
|
146
|
+
optional_positionals: function.optional_positionals,
|
|
147
|
+
rest_positionals: function.rest_positionals,
|
|
148
|
+
trailing_positionals: function.trailing_positionals,
|
|
149
|
+
required_keywords: function.required_keywords,
|
|
150
|
+
optional_keywords: function.optional_keywords,
|
|
151
|
+
rest_keywords: function.rest_keywords,
|
|
152
|
+
return_type: new_return_type.call(function.return_type),
|
|
153
|
+
forwarding: function.forwarding
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# There's no arity to preserve here, but if `@yieldparam` tags exist we
|
|
158
|
+
# at least know the documented shape, so build a full positional-only
|
|
159
|
+
# `Function` from them instead of leaving the block's params untyped.
|
|
160
|
+
def annotate_untyped_block_function(function, yield_params, new_return_type)
|
|
161
|
+
return function.map_type { new_return_type.call(function.return_type) } if yield_params.empty?
|
|
162
|
+
|
|
163
|
+
positionals = yield_params.map do |tag|
|
|
164
|
+
RBS::Types::Function::Param.new(name: safe_symbol(tag.name), type: convert(tag.types))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
RBS::Types::Function.new(
|
|
168
|
+
required_positionals: positionals,
|
|
169
|
+
optional_positionals: [],
|
|
170
|
+
rest_positionals: nil,
|
|
171
|
+
trailing_positionals: [],
|
|
172
|
+
required_keywords: {},
|
|
173
|
+
optional_keywords: {},
|
|
174
|
+
rest_keywords: nil,
|
|
175
|
+
return_type: new_return_type.call(function.return_type),
|
|
176
|
+
forwarding: nil
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Applies the type documented for a single structural parameter.
|
|
181
|
+
#
|
|
182
|
+
# @param unwrap [Symbol, nil] for a rest parameter (`*args`/`**kwargs`),
|
|
183
|
+
# YARD conventionally documents the *collected* type (`Array<String>`,
|
|
184
|
+
# `Hash{Symbol => String}`), but RBS's `*`/`**` types describe a single
|
|
185
|
+
# element instead, so one layer of `Array`/`Hash` is stripped back off.
|
|
186
|
+
def annotate_param(param, params, options, unwrap: nil)
|
|
187
|
+
return param unless param&.name
|
|
188
|
+
|
|
189
|
+
name = param.name.to_s
|
|
190
|
+
return param.map_type { options[name] } if options[name]
|
|
191
|
+
|
|
192
|
+
types = params[name]
|
|
193
|
+
types ? param.map_type { unwrap_rest_type(convert(types), unwrap) } : param
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def unwrap_rest_type(type, unwrap)
|
|
197
|
+
return type unless type.is_a?(RBS::Types::ClassInstance)
|
|
198
|
+
|
|
199
|
+
case unwrap
|
|
200
|
+
when :array
|
|
201
|
+
type.args.first if type.name.to_s == "Array" && type.args.size == 1
|
|
202
|
+
when :hash
|
|
203
|
+
type.args.last if type.name.to_s == "Hash" && type.args.size == 2
|
|
204
|
+
end || type
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def annotate_keywords(keywords, params, options)
|
|
208
|
+
keywords.each_with_object({}) do |(key, param), result|
|
|
209
|
+
name = key.to_s
|
|
210
|
+
|
|
211
|
+
if options[name]
|
|
212
|
+
result[key] = param.map_type { options[name] }
|
|
213
|
+
next
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
types = params[name]
|
|
217
|
+
result[key] = types ? param.map_type { convert(types) } : param
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def param_types_by_name(yard_method)
|
|
222
|
+
yard_method.tags(:param).each_with_object({}) do |tag, result|
|
|
223
|
+
next unless tag.name && tag.types
|
|
224
|
+
|
|
225
|
+
result[tag.name.to_s.sub(/\A[*&]+/, "")] = tag.types
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Builds an `RBS::Types::Record` (`{ key: Type, ?key2: Type2 }`) for each
|
|
230
|
+
# parameter documented with YARD `@option` tags, keyed by that
|
|
231
|
+
# parameter's name.
|
|
232
|
+
#
|
|
233
|
+
# e.g. `@option opts [String] :subject` contributes a `:subject` field
|
|
234
|
+
# to the record built for the `opts` parameter. Every field is
|
|
235
|
+
# optional, since that's what an options hash means: you're never
|
|
236
|
+
# required to pass any particular key.
|
|
237
|
+
def option_records_by_name(yard_method)
|
|
238
|
+
groups = yard_method.tags(:option).group_by { |tag| tag.name.to_s }
|
|
239
|
+
|
|
240
|
+
groups.filter_map do |name, tags|
|
|
241
|
+
fields = tags.each_with_object({}) do |tag, result|
|
|
242
|
+
pair = tag.pair
|
|
243
|
+
next unless pair&.name
|
|
244
|
+
|
|
245
|
+
key = pair.name.to_s.sub(/\A:/, "").to_sym
|
|
246
|
+
result[key] = [convert(pair.types), false]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
[name, RBS::Types::Record.new(all_fields: fields, location: nil)] unless fields.empty?
|
|
250
|
+
end.to_h
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def return_type_for(function, yard_method)
|
|
254
|
+
return function.return_type if yard_method.name == :initialize && yard_method.scope == :instance
|
|
255
|
+
|
|
256
|
+
types = return_types_of(yard_method)
|
|
257
|
+
types.empty? ? function.return_type : convert(types)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def return_types_of(yard_object)
|
|
261
|
+
return [] unless yard_object
|
|
262
|
+
|
|
263
|
+
yard_object.tags(:return).flat_map { |tag| tag.types || [] }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def convert(types)
|
|
267
|
+
TypeConverter.convert(types, strict: @strict)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def safe_symbol(name)
|
|
271
|
+
(name.nil? || name.empty?) ? nil : name.to_sym
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
data/lib/yarbs/cli.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Yarbs
|
|
6
|
+
# The `yarbs` command line executable.
|
|
7
|
+
module CLI
|
|
8
|
+
# Parses ARGV and runs yarbs from the command line.
|
|
9
|
+
#
|
|
10
|
+
# @param argv [Array<String>]
|
|
11
|
+
# @return [void]
|
|
12
|
+
def self.run(argv)
|
|
13
|
+
output_dir = "sig"
|
|
14
|
+
watch = false
|
|
15
|
+
strict = false
|
|
16
|
+
|
|
17
|
+
parser = OptionParser.new do |opts|
|
|
18
|
+
opts.banner = "Usage: yarbs [options] GLOB [GLOB ...]"
|
|
19
|
+
opts.on("-o", "--output DIR", "Directory to write .rbs files into (default: sig)") do |dir|
|
|
20
|
+
output_dir = dir
|
|
21
|
+
end
|
|
22
|
+
opts.on("-w", "--watch", "Watch the matched files and regenerate on change") do
|
|
23
|
+
watch = true
|
|
24
|
+
end
|
|
25
|
+
opts.on("-s", "--strict", "Raise instead of falling back to `untyped` on a YARD documentation error") do
|
|
26
|
+
strict = true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
patterns = parser.parse(argv)
|
|
31
|
+
|
|
32
|
+
if patterns.empty?
|
|
33
|
+
warn parser.help
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if strict && watch
|
|
38
|
+
warn "yarbs: --strict cannot be combined with --watch"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return Watcher.new(patterns, output_dir: output_dir).run if watch
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
written = Yarbs.generate(patterns, output_dir: output_dir, strict: strict)
|
|
46
|
+
rescue Error => e
|
|
47
|
+
warn e.message
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if written.empty?
|
|
52
|
+
warn "yarbs: no files matched #{patterns.join(" ")}"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
written.each { |path| puts "wrote #{path}" }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "rbs"
|
|
5
|
+
require "yard"
|
|
6
|
+
|
|
7
|
+
module Yarbs
|
|
8
|
+
# Orchestrates a full run: expands the glob patterns, loads YARD
|
|
9
|
+
# documentation for the matched files, and writes an annotated +.rbs+ file
|
|
10
|
+
# per source file into the output directory.
|
|
11
|
+
class Generator
|
|
12
|
+
# Runs a full generation pass for the given glob patterns.
|
|
13
|
+
#
|
|
14
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
15
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
16
|
+
# @param strict [Boolean] raise instead of skipping a file or falling
|
|
17
|
+
# back to +untyped+ when something can't be converted
|
|
18
|
+
# @return [Array<Pathname>] paths of the files written
|
|
19
|
+
def self.run(patterns, output_dir: "sig", strict: false)
|
|
20
|
+
new(output_dir, strict).run(patterns)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(output_dir, strict = false)
|
|
24
|
+
@output_dir = Pathname.new(output_dir)
|
|
25
|
+
@strict = strict
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(patterns)
|
|
29
|
+
files = patterns.flat_map { |pattern| Dir.glob(pattern) }.uniq.select { |file| File.file?(file) }
|
|
30
|
+
return [] if files.empty?
|
|
31
|
+
|
|
32
|
+
# `YARD.parse` only populates the in-memory Registry: unlike
|
|
33
|
+
# `YARD::Registry.load`, it neither reads nor writes a `.yardoc` cache
|
|
34
|
+
# directory, so each run reflects exactly the files we were given.
|
|
35
|
+
YARD::Registry.clear
|
|
36
|
+
YARD.parse(files)
|
|
37
|
+
|
|
38
|
+
files.filter_map { |file| generate_file(file) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# A single file with a (possibly transient, e.g. mid-edit) syntax error
|
|
44
|
+
# shouldn't stop the rest of the batch from generating: skip it and warn,
|
|
45
|
+
# unless running in strict mode, where it's treated as a hard failure.
|
|
46
|
+
def generate_file(file)
|
|
47
|
+
decls = PrototypeBuilder.build(File.read(file))
|
|
48
|
+
annotated = Annotator.annotate(decls, strict: @strict)
|
|
49
|
+
|
|
50
|
+
out_path = @output_dir.join(relative_sig_path(file))
|
|
51
|
+
out_path.dirname.mkpath
|
|
52
|
+
out_path.open("w") { |io| RBS::Writer.new(out: io).write(annotated) }
|
|
53
|
+
|
|
54
|
+
out_path
|
|
55
|
+
rescue SyntaxError => e
|
|
56
|
+
raise Error, "yarbs: syntax error in #{file} (#{e.message})" if @strict
|
|
57
|
+
|
|
58
|
+
warn "yarbs: skipping #{file} (#{e.message})"
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Mirrors the source file's path under the output directory, stripping a
|
|
63
|
+
# leading `lib/` or `app/` (the conventional Ruby load-path roots) so
|
|
64
|
+
# `lib/foo/bar.rb` becomes `<output_dir>/foo/bar.rbs`.
|
|
65
|
+
def relative_sig_path(file)
|
|
66
|
+
parts = Pathname.new(file).cleanpath.each_filename.to_a
|
|
67
|
+
parts.shift if %w[lib app].include?(parts.first)
|
|
68
|
+
parts[-1] = parts[-1].sub(/\.rb\z/, ".rbs")
|
|
69
|
+
Pathname.new(File.join(*parts))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
module Yarbs
|
|
6
|
+
# Parses Ruby source into an +RBS::AST::Declarations+ tree with the
|
|
7
|
+
# correct structure (arity, visibility, singleton vs. instance, etc.) but
|
|
8
|
+
# every type left as +untyped+.
|
|
9
|
+
#
|
|
10
|
+
# This is exactly what `rbs prototype rb` does under the hood.
|
|
11
|
+
module PrototypeBuilder
|
|
12
|
+
# Parses Ruby source into its structural (untyped) RBS declarations.
|
|
13
|
+
#
|
|
14
|
+
# @param source [String] Ruby source code
|
|
15
|
+
# @return [Array<RBS::AST::Declarations::t>]
|
|
16
|
+
def self.build(source)
|
|
17
|
+
builder = RBS::Prototype::RB.new
|
|
18
|
+
builder.parse(source)
|
|
19
|
+
builder.decls
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
module Yarbs
|
|
6
|
+
# Converts YARD type strings (e.g. "Array<String>", "Boolean") into RBS
|
|
7
|
+
# types, falling back to +untyped+ for anything that doesn't map cleanly.
|
|
8
|
+
module TypeConverter
|
|
9
|
+
UNTYPED = RBS::Types::Bases::Any.new(location: nil)
|
|
10
|
+
|
|
11
|
+
# Converts a YARD tag's type list into a single RBS type.
|
|
12
|
+
#
|
|
13
|
+
# @param yard_types [Array<String>, nil] the raw strings from a YARD
|
|
14
|
+
# tag's +types+ (e.g. +["String", "nil"]+ for +@param [String, nil]+)
|
|
15
|
+
# @param strict [Boolean] raise instead of falling back to +untyped+
|
|
16
|
+
# when a type can't be converted
|
|
17
|
+
# @return [RBS::Types::t] the best matching RBS type, or +untyped+
|
|
18
|
+
def self.convert(yard_types, strict: false)
|
|
19
|
+
return UNTYPED if yard_types.nil? || yard_types.empty?
|
|
20
|
+
|
|
21
|
+
source = yard_types.map { |type| rewrite(type) }.join(" | ")
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
RBS::Parser.parse_type(source, require_eof: true) || UNTYPED
|
|
25
|
+
rescue RBS::ParsingError => e
|
|
26
|
+
message = "yarbs: could not convert YARD type `#{yard_types.join(", ")}` to RBS (#{e.message})"
|
|
27
|
+
raise Error, message if strict
|
|
28
|
+
|
|
29
|
+
warn "#{message}; using untyped"
|
|
30
|
+
UNTYPED
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `Proc<(ArgType, ...), ReturnType>` -> `^(ArgType, ...) -> ReturnType`,
|
|
35
|
+
# RBS's proc-literal type (a real, typed callable signature).
|
|
36
|
+
#
|
|
37
|
+
# YARD's own tag parser mangles a literal `->` written inside
|
|
38
|
+
# `@param [...]` brackets, so this convention avoids the arrow entirely.
|
|
39
|
+
PROC_TYPE = /\AProc<\((?<args>.*)\),\s*(?<return_type>.+)>\z/m
|
|
40
|
+
private_constant :PROC_TYPE
|
|
41
|
+
|
|
42
|
+
# Rewrites a single YARD type string into RBS syntax, best-effort.
|
|
43
|
+
#
|
|
44
|
+
# @param type [String] a single YARD type string
|
|
45
|
+
# @return [String] the same type rewritten using RBS syntax, best-effort
|
|
46
|
+
def self.rewrite(type)
|
|
47
|
+
type = type.strip
|
|
48
|
+
return "untyped" if type.start_with?("#")
|
|
49
|
+
|
|
50
|
+
if (match = PROC_TYPE.match(type))
|
|
51
|
+
args = split_top_level(match[:args]).map { |arg| rewrite(arg) }
|
|
52
|
+
return "^(#{args.join(", ")}) -> #{rewrite(match[:return_type])}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
type = type.gsub(/\bBool(?:ean)?\b/, "bool")
|
|
56
|
+
type = type.gsub(/\bClass<(.+)>/) { "singleton(#{Regexp.last_match(1)})" }
|
|
57
|
+
type = type.gsub(/\bHash\{\s*(.+?)\s*=>\s*(.+?)\s*\}/m) { "Hash[#{Regexp.last_match(1)}, #{Regexp.last_match(2)}]" }
|
|
58
|
+
type.tr("<>", "[]")
|
|
59
|
+
end
|
|
60
|
+
private_class_method :rewrite
|
|
61
|
+
|
|
62
|
+
# Splits on top-level commas only, respecting `<>`/`{}`/`()`/`[]`
|
|
63
|
+
# nesting, so e.g. `Array<String>, Integer` splits into two, not three.
|
|
64
|
+
#
|
|
65
|
+
# @param str [String]
|
|
66
|
+
# @return [Array<String>]
|
|
67
|
+
def self.split_top_level(str)
|
|
68
|
+
return [] if str.strip.empty?
|
|
69
|
+
|
|
70
|
+
depth = 0
|
|
71
|
+
parts = []
|
|
72
|
+
current = +""
|
|
73
|
+
|
|
74
|
+
str.each_char do |char|
|
|
75
|
+
case char
|
|
76
|
+
when "<", "{", "(", "["
|
|
77
|
+
depth += 1
|
|
78
|
+
current << char
|
|
79
|
+
when ">", "}", ")", "]"
|
|
80
|
+
depth -= 1
|
|
81
|
+
current << char
|
|
82
|
+
when ","
|
|
83
|
+
if depth.zero?
|
|
84
|
+
parts << current.strip
|
|
85
|
+
current = +""
|
|
86
|
+
else
|
|
87
|
+
current << char
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
current << char
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
parts << current.strip
|
|
95
|
+
end
|
|
96
|
+
private_class_method :split_top_level
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "listen"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Yarbs
|
|
7
|
+
# Watches the files matched by a set of glob patterns (using the `listen`
|
|
8
|
+
# gem) and regenerates their RBS signatures whenever one of them changes.
|
|
9
|
+
#
|
|
10
|
+
# Used by `yarbs --watch` while developing an app with Yarbs support.
|
|
11
|
+
class Watcher
|
|
12
|
+
# Sets up a watcher for the given glob patterns.
|
|
13
|
+
#
|
|
14
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
15
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
16
|
+
def initialize(patterns, output_dir:)
|
|
17
|
+
@patterns = patterns
|
|
18
|
+
@output_dir = output_dir
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Starts watching and blocks until interrupted (Ctrl-C).
|
|
22
|
+
#
|
|
23
|
+
# @return [void]
|
|
24
|
+
def run
|
|
25
|
+
generate("starting up")
|
|
26
|
+
|
|
27
|
+
directories = watch_directories
|
|
28
|
+
if directories.empty?
|
|
29
|
+
warn "yarbs: nothing to watch for #{@patterns.join(" ")}"
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
listener = Listen.to(*directories) { |modified, added, removed| on_change(modified + added + removed) }
|
|
34
|
+
listener.start
|
|
35
|
+
|
|
36
|
+
wait_for_interrupt
|
|
37
|
+
ensure
|
|
38
|
+
listener&.stop
|
|
39
|
+
puts "yarbs: stopped watching"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def wait_for_interrupt
|
|
45
|
+
stopped = false
|
|
46
|
+
trap("INT") { stopped = true }
|
|
47
|
+
sleep 0.2 until stopped
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def on_change(paths)
|
|
51
|
+
changed = paths.select { |path| matches_patterns?(path) }
|
|
52
|
+
return if changed.empty?
|
|
53
|
+
|
|
54
|
+
generate("#{changed.size} file#{"s" unless changed.size == 1} changed")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generate(reason)
|
|
58
|
+
puts "yarbs: #{reason}, regenerating..."
|
|
59
|
+
written = Yarbs.generate(@patterns, output_dir: @output_dir)
|
|
60
|
+
puts "yarbs: wrote #{written.size} file#{"s" unless written.size == 1} to #{@output_dir}"
|
|
61
|
+
rescue => e
|
|
62
|
+
warn "yarbs: #{e.class}: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def matches_patterns?(path)
|
|
66
|
+
relative = relative_path(path)
|
|
67
|
+
@patterns.any? { |pattern| File.fnmatch?(pattern, relative, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def relative_path(path)
|
|
71
|
+
Pathname.new(path).relative_path_from(Pathname.pwd).to_s
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
path
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def watch_directories
|
|
77
|
+
@patterns.filter_map { |pattern| base_directory(pattern) }.uniq.select { |dir| Dir.exist?(dir) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The directory to hand to `Listen.to` for a glob pattern: everything
|
|
81
|
+
# before its first glob character, e.g. "lib/**/*.rb" -> "lib".
|
|
82
|
+
def base_directory(pattern)
|
|
83
|
+
glob_index = pattern.index(/[*?{\[]/)
|
|
84
|
+
return File.dirname(pattern) unless glob_index
|
|
85
|
+
|
|
86
|
+
prefix = pattern[0...glob_index]
|
|
87
|
+
return "." if prefix.empty?
|
|
88
|
+
|
|
89
|
+
prefix.end_with?("/") ? prefix.chomp("/") : File.dirname(prefix)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/yarbs.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yard"
|
|
4
|
+
require "rbs"
|
|
5
|
+
|
|
6
|
+
require_relative "yarbs/version"
|
|
7
|
+
require_relative "yarbs/type_converter"
|
|
8
|
+
require_relative "yarbs/prototype_builder"
|
|
9
|
+
require_relative "yarbs/annotator"
|
|
10
|
+
require_relative "yarbs/generator"
|
|
11
|
+
require_relative "yarbs/watcher"
|
|
12
|
+
require_relative "yarbs/cli"
|
|
13
|
+
|
|
14
|
+
module Yarbs
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Generates .rbs signature files for the Ruby source files matched by
|
|
18
|
+
# +patterns+, writing them into +output_dir+.
|
|
19
|
+
#
|
|
20
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
21
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
22
|
+
# @param strict [Boolean] raise a {Yarbs::Error} instead of skipping a file
|
|
23
|
+
# or falling back to +untyped+ when something can't be converted
|
|
24
|
+
# @return [Array<Pathname>] paths of the files written
|
|
25
|
+
def self.generate(patterns, output_dir: "sig", strict: false)
|
|
26
|
+
Generator.run(Array(patterns), output_dir: output_dir, strict: strict)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
# Walks an +RBS::AST::Declarations+ tree produced by {PrototypeBuilder} and
|
|
3
|
+
# fills in its +untyped+ placeholders with types read from the matching
|
|
4
|
+
# YARD documentation (already loaded into +YARD::Registry+).
|
|
5
|
+
class Annotator
|
|
6
|
+
@strict: untyped
|
|
7
|
+
|
|
8
|
+
# Fills in the +untyped+ placeholders in a decls tree using YARD documentation.
|
|
9
|
+
#
|
|
10
|
+
# @param decls [Array<RBS::AST::Declarations::t>]
|
|
11
|
+
# @param strict [Boolean] raise instead of falling back to +untyped+
|
|
12
|
+
# when a YARD type can't be converted
|
|
13
|
+
# @return [Array<RBS::AST::Declarations::t>] a new tree with YARD-derived types merged in
|
|
14
|
+
def self.annotate: (Array[RBS::AST::Declarations::t] decls, ?strict: bool) -> Array[RBS::AST::Declarations::t]
|
|
15
|
+
|
|
16
|
+
def initialize: (untyped strict) -> void
|
|
17
|
+
|
|
18
|
+
def visit_all: (untyped decls, untyped namespace) -> untyped
|
|
19
|
+
|
|
20
|
+
def visit: (untyped decl, untyped namespace) -> untyped
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def annotate_constant: (untyped decl, untyped namespace) -> untyped
|
|
25
|
+
|
|
26
|
+
def annotate_method: (untyped member, untyped namespace) -> untyped
|
|
27
|
+
|
|
28
|
+
def annotate_attribute: (untyped member, untyped namespace) -> untyped
|
|
29
|
+
|
|
30
|
+
def annotate_overload: (untyped overload, untyped yard_method) -> untyped
|
|
31
|
+
|
|
32
|
+
def annotate_function: (untyped function, untyped yard_method) -> untyped
|
|
33
|
+
|
|
34
|
+
def annotate_block: (untyped block, untyped yard_method) -> untyped
|
|
35
|
+
|
|
36
|
+
def annotate_block_function: (untyped function, untyped yield_params, untyped new_return_type) -> untyped
|
|
37
|
+
|
|
38
|
+
# There's no arity to preserve here, but if `@yieldparam` tags exist we
|
|
39
|
+
# at least know the documented shape, so build a full positional-only
|
|
40
|
+
# `Function` from them instead of leaving the block's params untyped.
|
|
41
|
+
def annotate_untyped_block_function: (untyped function, untyped yield_params, untyped new_return_type) -> untyped
|
|
42
|
+
|
|
43
|
+
# Applies the type documented for a single structural parameter.
|
|
44
|
+
#
|
|
45
|
+
# @param unwrap [Symbol, nil] for a rest parameter (`*args`/`**kwargs`),
|
|
46
|
+
# YARD conventionally documents the *collected* type (`Array<String>`,
|
|
47
|
+
# `Hash{Symbol => String}`), but RBS's `*`/`**` types describe a single
|
|
48
|
+
# element instead, so one layer of `Array`/`Hash` is stripped back off.
|
|
49
|
+
def annotate_param: (untyped param, untyped params, untyped options, ?unwrap: Symbol | nil) -> untyped
|
|
50
|
+
|
|
51
|
+
def unwrap_rest_type: (untyped type, untyped unwrap) -> untyped
|
|
52
|
+
|
|
53
|
+
def annotate_keywords: (untyped keywords, untyped params, untyped options) -> untyped
|
|
54
|
+
|
|
55
|
+
def param_types_by_name: (untyped yard_method) -> untyped
|
|
56
|
+
|
|
57
|
+
# Builds an `RBS::Types::Record` (`{ key: Type, ?key2: Type2 }`) for each
|
|
58
|
+
# parameter documented with YARD `@option` tags, keyed by that
|
|
59
|
+
# parameter's name.
|
|
60
|
+
#
|
|
61
|
+
# e.g. `@option opts [String] :subject` contributes a `:subject` field
|
|
62
|
+
# to the record built for the `opts` parameter. Every field is
|
|
63
|
+
# optional, since that's what an options hash means: you're never
|
|
64
|
+
# required to pass any particular key.
|
|
65
|
+
def option_records_by_name: (untyped yard_method) -> untyped
|
|
66
|
+
|
|
67
|
+
def return_type_for: (untyped function, untyped yard_method) -> untyped
|
|
68
|
+
|
|
69
|
+
def return_types_of: (untyped yard_object) -> (::Array[untyped] | untyped)
|
|
70
|
+
|
|
71
|
+
def convert: (untyped types) -> untyped
|
|
72
|
+
|
|
73
|
+
def safe_symbol: (untyped name) -> (nil | untyped)
|
|
74
|
+
end
|
|
75
|
+
end
|
data/sig/yarbs/cli.rbs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
# Orchestrates a full run: expands the glob patterns, loads YARD
|
|
3
|
+
# documentation for the matched files, and writes an annotated +.rbs+ file
|
|
4
|
+
# per source file into the output directory.
|
|
5
|
+
class Generator
|
|
6
|
+
@output_dir: untyped
|
|
7
|
+
|
|
8
|
+
@strict: untyped
|
|
9
|
+
|
|
10
|
+
# Runs a full generation pass for the given glob patterns.
|
|
11
|
+
#
|
|
12
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
13
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
14
|
+
# @param strict [Boolean] raise instead of skipping a file or falling
|
|
15
|
+
# back to +untyped+ when something can't be converted
|
|
16
|
+
# @return [Array<Pathname>] paths of the files written
|
|
17
|
+
def self.run: (untyped patterns, ?output_dir: ::String, ?strict: bool) -> untyped
|
|
18
|
+
|
|
19
|
+
def initialize: (untyped output_dir, ?bool strict) -> void
|
|
20
|
+
|
|
21
|
+
def run: (untyped patterns) -> (::Array[untyped] | untyped)
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# A single file with a (possibly transient, e.g. mid-edit) syntax error
|
|
26
|
+
# shouldn't stop the rest of the batch from generating: skip it and warn,
|
|
27
|
+
# unless running in strict mode, where it's treated as a hard failure.
|
|
28
|
+
def generate_file: (untyped file) -> untyped
|
|
29
|
+
|
|
30
|
+
# Mirrors the source file's path under the output directory, stripping a
|
|
31
|
+
# leading `lib/` or `app/` (the conventional Ruby load-path roots) so
|
|
32
|
+
# `lib/foo/bar.rb` becomes `<output_dir>/foo/bar.rbs`.
|
|
33
|
+
def relative_sig_path: (untyped file) -> untyped
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
# Parses Ruby source into an +RBS::AST::Declarations+ tree with the
|
|
3
|
+
# correct structure (arity, visibility, singleton vs. instance, etc.) but
|
|
4
|
+
# every type left as +untyped+.
|
|
5
|
+
#
|
|
6
|
+
# This is exactly what `rbs prototype rb` does under the hood.
|
|
7
|
+
module PrototypeBuilder
|
|
8
|
+
# Parses Ruby source into its structural (untyped) RBS declarations.
|
|
9
|
+
#
|
|
10
|
+
# @param source [String] Ruby source code
|
|
11
|
+
# @return [Array<RBS::AST::Declarations::t>]
|
|
12
|
+
def self.build: (String source) -> Array[RBS::AST::Declarations::t]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
# Converts YARD type strings (e.g. "Array<String>", "Boolean") into RBS
|
|
3
|
+
# types, falling back to +untyped+ for anything that doesn't map cleanly.
|
|
4
|
+
module TypeConverter
|
|
5
|
+
UNTYPED: untyped
|
|
6
|
+
|
|
7
|
+
# Converts a YARD tag's type list into a single RBS type.
|
|
8
|
+
#
|
|
9
|
+
# @param yard_types [Array<String>, nil] the raw strings from a YARD
|
|
10
|
+
# tag's +types+ (e.g. +["String", "nil"]+ for +@param [String, nil]+)
|
|
11
|
+
# @param strict [Boolean] raise instead of falling back to +untyped+
|
|
12
|
+
# when a type can't be converted
|
|
13
|
+
# @return [RBS::Types::t] the best matching RBS type, or +untyped+
|
|
14
|
+
def self.convert: (Array[String] | nil yard_types, ?strict: bool) -> RBS::Types::t
|
|
15
|
+
|
|
16
|
+
# `Proc<(ArgType, ...), ReturnType>` -> `^(ArgType, ...) -> ReturnType`,
|
|
17
|
+
# RBS's proc-literal type (a real, typed callable signature).
|
|
18
|
+
#
|
|
19
|
+
# YARD's own tag parser mangles a literal `->` written inside
|
|
20
|
+
# `@param [...]` brackets, so this convention avoids the arrow entirely.
|
|
21
|
+
PROC_TYPE: ::Regexp
|
|
22
|
+
|
|
23
|
+
# Rewrites a single YARD type string into RBS syntax, best-effort.
|
|
24
|
+
#
|
|
25
|
+
# @param type [String] a single YARD type string
|
|
26
|
+
# @return [String] the same type rewritten using RBS syntax, best-effort
|
|
27
|
+
def self.rewrite: (String type) -> String
|
|
28
|
+
|
|
29
|
+
# Splits on top-level commas only, respecting `<>`/`{}`/`()`/`[]`
|
|
30
|
+
# nesting, so e.g. `Array<String>, Integer` splits into two, not three.
|
|
31
|
+
#
|
|
32
|
+
# @param str [String]
|
|
33
|
+
# @return [Array<String>]
|
|
34
|
+
def self.split_top_level: (String str) -> Array[String]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
# Watches the files matched by a set of glob patterns (using the `listen`
|
|
3
|
+
# gem) and regenerates their RBS signatures whenever one of them changes.
|
|
4
|
+
#
|
|
5
|
+
# Used by `yarbs --watch` while developing an app with Yarbs support.
|
|
6
|
+
class Watcher
|
|
7
|
+
@patterns: untyped
|
|
8
|
+
|
|
9
|
+
@output_dir: untyped
|
|
10
|
+
|
|
11
|
+
# Sets up a watcher for the given glob patterns.
|
|
12
|
+
#
|
|
13
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
14
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
15
|
+
def initialize: (Array[String] patterns, output_dir: String) -> void
|
|
16
|
+
|
|
17
|
+
# Starts watching and blocks until interrupted (Ctrl-C).
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def run: () -> void
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def wait_for_interrupt: () -> untyped
|
|
25
|
+
|
|
26
|
+
def on_change: (untyped paths) -> (nil | untyped)
|
|
27
|
+
|
|
28
|
+
def generate: (untyped reason) -> untyped
|
|
29
|
+
|
|
30
|
+
def matches_patterns?: (untyped path) -> bool
|
|
31
|
+
|
|
32
|
+
def relative_path: (untyped path) -> untyped
|
|
33
|
+
|
|
34
|
+
def watch_directories: () -> untyped
|
|
35
|
+
|
|
36
|
+
# The directory to hand to `Listen.to` for a glob pattern: everything
|
|
37
|
+
# before its first glob character, e.g. "lib/**/*.rb" -> "lib".
|
|
38
|
+
def base_directory: (untyped pattern) -> (untyped | ".")
|
|
39
|
+
end
|
|
40
|
+
end
|
data/sig/yarbs.rbs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Yarbs
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
# Generates .rbs signature files for the Ruby source files matched by
|
|
6
|
+
# +patterns+, writing them into +output_dir+.
|
|
7
|
+
#
|
|
8
|
+
# @param patterns [Array<String>] glob patterns for Ruby source files
|
|
9
|
+
# @param output_dir [String] directory to write generated .rbs files into
|
|
10
|
+
# @param strict [Boolean] raise a {Yarbs::Error} instead of skipping a file
|
|
11
|
+
# or falling back to +untyped+ when something can't be converted
|
|
12
|
+
# @return [Array<Pathname>] paths of the files written
|
|
13
|
+
def self.generate: (Array[String] patterns, ?output_dir: String, ?strict: bool) -> Array[Pathname]
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: yarbs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- tobidelius
|
|
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: listen
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.9'
|
|
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.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: yard
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
54
|
+
email:
|
|
55
|
+
- tobidelius@gmail.com
|
|
56
|
+
executables:
|
|
57
|
+
- yarbs
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- LICENSE.txt
|
|
62
|
+
- README.md
|
|
63
|
+
- Rakefile
|
|
64
|
+
- exe/yarbs
|
|
65
|
+
- lib/yarbs.rb
|
|
66
|
+
- lib/yarbs/annotator.rb
|
|
67
|
+
- lib/yarbs/cli.rb
|
|
68
|
+
- lib/yarbs/generator.rb
|
|
69
|
+
- lib/yarbs/prototype_builder.rb
|
|
70
|
+
- lib/yarbs/type_converter.rb
|
|
71
|
+
- lib/yarbs/version.rb
|
|
72
|
+
- lib/yarbs/watcher.rb
|
|
73
|
+
- sig/yarbs.rbs
|
|
74
|
+
- sig/yarbs/annotator.rbs
|
|
75
|
+
- sig/yarbs/cli.rbs
|
|
76
|
+
- sig/yarbs/generator.rbs
|
|
77
|
+
- sig/yarbs/prototype_builder.rbs
|
|
78
|
+
- sig/yarbs/type_converter.rbs
|
|
79
|
+
- sig/yarbs/version.rbs
|
|
80
|
+
- sig/yarbs/watcher.rbs
|
|
81
|
+
homepage: https://github.com/tobidelius/yarbs
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata: {}
|
|
85
|
+
rdoc_options: []
|
|
86
|
+
require_paths:
|
|
87
|
+
- lib
|
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: 3.3.0
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubygems_version: 4.0.16
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Generate RBS files from YARD documentation.
|
|
102
|
+
test_files: []
|