sorbet-baml 0.0.1 → 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 +4 -4
- data/.idea/.gitignore +8 -0
- data/.idea/inspectionProfiles/Project_Default.xml +5 -0
- data/README.md +417 -63
- data/docs/README.md +74 -24
- data/docs/advanced-usage.md +378 -36
- data/docs/getting-started.md +45 -8
- data/docs/troubleshooting.md +224 -14
- data/docs/type-mapping.md +144 -17
- data/lib/sorbet_baml/comment_extractor.rb +165 -0
- data/lib/sorbet_baml/converter.rb +153 -3
- data/lib/sorbet_baml/dependency_resolver.rb +99 -0
- data/lib/sorbet_baml/enum_extensions.rb +23 -0
- data/lib/sorbet_baml/struct_extensions.rb +23 -0
- data/lib/sorbet_baml/type_mapper.rb +49 -12
- data/lib/sorbet_baml/version.rb +1 -1
- data/lib/sorbet_baml.rb +7 -0
- metadata +7 -1
data/docs/troubleshooting.md
CHANGED
|
@@ -18,6 +18,16 @@ end
|
|
|
18
18
|
class User < T::Struct
|
|
19
19
|
const :name, String
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
# Generate BAML
|
|
23
|
+
User.to_baml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Generated BAML:**
|
|
27
|
+
```baml
|
|
28
|
+
class User {
|
|
29
|
+
name string
|
|
30
|
+
}
|
|
21
31
|
```
|
|
22
32
|
|
|
23
33
|
### Empty output
|
|
@@ -29,14 +39,44 @@ end
|
|
|
29
39
|
```ruby
|
|
30
40
|
class User < T::Struct
|
|
31
41
|
const :name, String # Add properties
|
|
42
|
+
const :age, Integer
|
|
32
43
|
end
|
|
44
|
+
|
|
45
|
+
User.to_baml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Generated BAML:**
|
|
49
|
+
```baml
|
|
50
|
+
class User {
|
|
51
|
+
name string
|
|
52
|
+
age int
|
|
53
|
+
}
|
|
33
54
|
```
|
|
34
55
|
|
|
35
|
-
###
|
|
56
|
+
### Self-referential types work fine
|
|
57
|
+
|
|
58
|
+
**Problem**: You think self-referential types aren't supported.
|
|
59
|
+
|
|
60
|
+
**Solution**: They actually work perfectly! Self-referential types are fully supported:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class Category < T::Struct
|
|
64
|
+
const :name, String
|
|
65
|
+
const :parent, T.nilable(Category)
|
|
66
|
+
const :children, T::Array[Category]
|
|
67
|
+
end
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
Category.to_baml
|
|
70
|
+
```
|
|
38
71
|
|
|
39
|
-
**
|
|
72
|
+
**Generated BAML:**
|
|
73
|
+
```baml
|
|
74
|
+
class Category {
|
|
75
|
+
name string
|
|
76
|
+
parent Category?
|
|
77
|
+
children Category[]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
40
80
|
|
|
41
81
|
## Type-Specific Issues
|
|
42
82
|
|
|
@@ -46,10 +86,23 @@ Ensure you're using the Sorbet array syntax:
|
|
|
46
86
|
|
|
47
87
|
```ruby
|
|
48
88
|
# ❌ Wrong
|
|
49
|
-
|
|
89
|
+
class User < T::Struct
|
|
90
|
+
const :items, Array # Generic Array won't work
|
|
91
|
+
end
|
|
50
92
|
|
|
51
93
|
# ✅ Correct
|
|
52
|
-
|
|
94
|
+
class User < T::Struct
|
|
95
|
+
const :items, T::Array[String]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
User.to_baml
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Generated BAML:**
|
|
102
|
+
```baml
|
|
103
|
+
class User {
|
|
104
|
+
items string[]
|
|
105
|
+
}
|
|
53
106
|
```
|
|
54
107
|
|
|
55
108
|
### Optional fields showing as required
|
|
@@ -58,24 +111,181 @@ Make sure to use `T.nilable`:
|
|
|
58
111
|
|
|
59
112
|
```ruby
|
|
60
113
|
# ❌ Wrong - will be required
|
|
61
|
-
|
|
114
|
+
class User < T::Struct
|
|
115
|
+
const :email, String
|
|
116
|
+
end
|
|
62
117
|
|
|
63
118
|
# ✅ Correct - will be optional
|
|
64
|
-
|
|
119
|
+
class User < T::Struct
|
|
120
|
+
const :email, T.nilable(String)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
User.to_baml
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Generated BAML:**
|
|
127
|
+
```baml
|
|
128
|
+
class User {
|
|
129
|
+
email string?
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Union types not working
|
|
134
|
+
|
|
135
|
+
Ensure you're using `T.any` for union types:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# ❌ Wrong
|
|
139
|
+
class Config < T::Struct
|
|
140
|
+
const :value, String || Integer # Ruby OR, not Sorbet union
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ✅ Correct
|
|
144
|
+
class Config < T::Struct
|
|
145
|
+
const :value, T.any(String, Integer)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Config.to_baml
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Generated BAML:**
|
|
152
|
+
```baml
|
|
153
|
+
class Config {
|
|
154
|
+
value string | int
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Hash types not mapping correctly
|
|
159
|
+
|
|
160
|
+
Use the full `T::Hash[K, V]` syntax:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# ❌ Wrong
|
|
164
|
+
class User < T::Struct
|
|
165
|
+
const :metadata, Hash # Generic Hash won't work
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# ✅ Correct
|
|
169
|
+
class User < T::Struct
|
|
170
|
+
const :metadata, T::Hash[String, T.any(String, Integer)]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
User.to_baml
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Generated BAML:**
|
|
177
|
+
```baml
|
|
178
|
+
class User {
|
|
179
|
+
metadata map<string, string | int>
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Dependency Issues
|
|
184
|
+
|
|
185
|
+
### Missing dependencies in output
|
|
186
|
+
|
|
187
|
+
Use `include_dependencies: true` to automatically include all referenced types:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class Address < T::Struct
|
|
191
|
+
const :street, String
|
|
192
|
+
const :city, String
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class User < T::Struct
|
|
196
|
+
const :name, String
|
|
197
|
+
const :address, Address
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# ❌ Only outputs User class
|
|
201
|
+
User.to_baml
|
|
202
|
+
|
|
203
|
+
# ✅ Outputs both Address and User in correct order
|
|
204
|
+
User.to_baml(include_dependencies: true)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Generated BAML (with dependencies):**
|
|
208
|
+
```baml
|
|
209
|
+
class Address {
|
|
210
|
+
street string
|
|
211
|
+
city string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class User {
|
|
215
|
+
name string
|
|
216
|
+
address Address
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Wrong dependency order
|
|
221
|
+
|
|
222
|
+
The gem automatically handles dependency ordering using topological sorting. Dependencies always come before the types that reference them.
|
|
223
|
+
|
|
224
|
+
## Enum Issues
|
|
225
|
+
|
|
226
|
+
### Enums not converting
|
|
227
|
+
|
|
228
|
+
Ensure you're using the correct T::Enum syntax:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
# ❌ Wrong
|
|
232
|
+
class Status
|
|
233
|
+
ACTIVE = 'active'
|
|
234
|
+
INACTIVE = 'inactive'
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# ✅ Correct
|
|
238
|
+
class Status < T::Enum
|
|
239
|
+
enums do
|
|
240
|
+
Active = new('active')
|
|
241
|
+
Inactive = new('inactive')
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
Status.to_baml
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Generated BAML:**
|
|
249
|
+
```baml
|
|
250
|
+
enum Status {
|
|
251
|
+
"active"
|
|
252
|
+
"inactive"
|
|
253
|
+
}
|
|
65
254
|
```
|
|
66
255
|
|
|
67
256
|
## Getting Help
|
|
68
257
|
|
|
69
|
-
1. Check the [Type Mapping Reference](./type-mapping.md)
|
|
70
|
-
2. Review
|
|
258
|
+
1. Check the [Type Mapping Reference](./type-mapping.md) for complete type support
|
|
259
|
+
2. Review examples in [Getting Started](./getting-started.md) and [Advanced Usage](./advanced-usage.md)
|
|
71
260
|
3. File an issue at https://github.com/vicentereig/sorbet-baml/issues
|
|
72
261
|
|
|
73
|
-
##
|
|
262
|
+
## Advanced Debugging
|
|
74
263
|
|
|
75
|
-
|
|
264
|
+
### Inspect Sorbet type information
|
|
76
265
|
|
|
77
266
|
```ruby
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
267
|
+
# See what Sorbet sees for your struct
|
|
268
|
+
MyStruct.props
|
|
269
|
+
# => {name: String, age: Integer, ...}
|
|
270
|
+
|
|
271
|
+
# Check if a class is a T::Struct
|
|
272
|
+
MyStruct < T::Struct
|
|
273
|
+
# => true
|
|
274
|
+
|
|
275
|
+
# See enum values
|
|
276
|
+
MyEnum.values
|
|
277
|
+
# => [#<MyEnum:0x... @serialize="value1">, ...]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Testing your BAML output
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Verify the output looks correct
|
|
284
|
+
baml = User.to_baml(include_dependencies: true)
|
|
285
|
+
puts baml
|
|
286
|
+
|
|
287
|
+
# Check that all expected types are included
|
|
288
|
+
expected_types = ['class User', 'class Address', 'enum Status']
|
|
289
|
+
expected_types.all? { |type| baml.include?(type) }
|
|
290
|
+
# => true
|
|
81
291
|
```
|
data/docs/type-mapping.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Type Mapping Reference
|
|
2
2
|
|
|
3
|
-
Complete mapping between Sorbet types and BAML output.
|
|
3
|
+
Complete mapping between Sorbet types and BAML output. All listed types are **fully supported**.
|
|
4
4
|
|
|
5
5
|
## Basic Types
|
|
6
6
|
|
|
@@ -12,54 +12,181 @@ Complete mapping between Sorbet types and BAML output.
|
|
|
12
12
|
| `T::Boolean` | `bool` | `active bool` |
|
|
13
13
|
| `NilClass` | `null` | `null` |
|
|
14
14
|
| `Symbol` | `string` | `status string` |
|
|
15
|
+
| `Date/DateTime/Time` | `string` | `created_at string` |
|
|
15
16
|
|
|
16
|
-
## Optional Types
|
|
17
|
+
## Optional Types (T.nilable)
|
|
17
18
|
|
|
18
19
|
| Sorbet Type | BAML Output | Example |
|
|
19
20
|
|-------------|-------------|---------|
|
|
20
21
|
| `T.nilable(String)` | `string?` | `email string?` |
|
|
21
22
|
| `T.nilable(Integer)` | `int?` | `age int?` |
|
|
23
|
+
| `T.nilable(MyStruct)` | `MyStruct?` | `address Address?` |
|
|
22
24
|
|
|
23
25
|
## Collection Types
|
|
24
26
|
|
|
25
27
|
| Sorbet Type | BAML Output | Example |
|
|
26
28
|
|-------------|-------------|---------|
|
|
27
29
|
| `T::Array[String]` | `string[]` | `tags string[]` |
|
|
28
|
-
| `T::Array[
|
|
30
|
+
| `T::Array[Integer]` | `int[]` | `scores int[]` |
|
|
31
|
+
| `T::Array[MyStruct]` | `MyStruct[]` | `addresses Address[]` |
|
|
29
32
|
|
|
30
|
-
##
|
|
33
|
+
## Hash/Map Types
|
|
34
|
+
|
|
35
|
+
| Sorbet Type | BAML Output | Example |
|
|
36
|
+
|-------------|-------------|---------|
|
|
37
|
+
| `T::Hash[String, String]` | `map<string, string>` | `metadata map<string, string>` |
|
|
38
|
+
| `T::Hash[String, Integer]` | `map<string, int>` | `counts map<string, int>` |
|
|
39
|
+
| `T::Hash[Symbol, String]` | `map<string, string>` | `config map<string, string>` |
|
|
40
|
+
|
|
41
|
+
## Union Types (T.any)
|
|
42
|
+
|
|
43
|
+
| Sorbet Type | BAML Output | Example |
|
|
44
|
+
|-------------|-------------|---------|
|
|
45
|
+
| `T.any(String, Integer)` | `string \| int` | `value string \| int` |
|
|
46
|
+
| `T.any(String, Integer, Float)` | `string \| int \| float` | `mixed string \| int \| float` |
|
|
47
|
+
| `T.nilable(T.any(String, Integer))` | `(string \| int)?` | `optional (string \| int)?` |
|
|
48
|
+
|
|
49
|
+
## Complex Collection Types
|
|
50
|
+
|
|
51
|
+
| Sorbet Type | BAML Output | Example |
|
|
52
|
+
|-------------|-------------|---------|
|
|
53
|
+
| `T::Array[T.any(String, Integer)]` | `(string \| int)[]` | `mixed_array (string \| int)[]` |
|
|
54
|
+
| `T::Hash[String, T.any(String, Integer)]` | `map<string, string \| int>` | `settings map<string, string \| int>` |
|
|
55
|
+
| `T::Hash[String, T::Array[String]]` | `map<string, string[]>` | `labels map<string, string[]>` |
|
|
56
|
+
|
|
57
|
+
## Structured Types
|
|
58
|
+
|
|
59
|
+
### T::Struct to BAML Classes
|
|
31
60
|
|
|
32
61
|
```ruby
|
|
33
|
-
# Input
|
|
34
62
|
class Address < T::Struct
|
|
35
63
|
const :street, String
|
|
36
64
|
const :city, String
|
|
65
|
+
const :postal_code, T.nilable(String)
|
|
37
66
|
end
|
|
38
67
|
|
|
39
68
|
class User < T::Struct
|
|
40
69
|
const :name, String
|
|
70
|
+
const :age, Integer
|
|
41
71
|
const :address, Address
|
|
72
|
+
const :tags, T::Array[String]
|
|
42
73
|
end
|
|
74
|
+
```
|
|
43
75
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
city string
|
|
48
|
-
}
|
|
76
|
+
```ruby
|
|
77
|
+
User.to_baml
|
|
78
|
+
```
|
|
49
79
|
|
|
80
|
+
**Generated BAML:**
|
|
81
|
+
```baml
|
|
50
82
|
class User {
|
|
51
83
|
name string
|
|
84
|
+
age int
|
|
52
85
|
address Address
|
|
86
|
+
tags string[]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### T::Enum to BAML Enums
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
class Status < T::Enum
|
|
94
|
+
enums do
|
|
95
|
+
Active = new('active')
|
|
96
|
+
Inactive = new('inactive')
|
|
97
|
+
Pending = new('pending')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class User < T::Struct
|
|
102
|
+
const :name, String
|
|
103
|
+
const :status, Status
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
[Status, User].map(&:to_baml).join("\n\n")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Generated BAML:**
|
|
112
|
+
```baml
|
|
113
|
+
enum Status {
|
|
114
|
+
"active"
|
|
115
|
+
"inactive"
|
|
116
|
+
"pending"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class User {
|
|
120
|
+
name string
|
|
121
|
+
status Status
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Complex Real-World Example
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
class Priority < T::Enum
|
|
129
|
+
enums do
|
|
130
|
+
Low = new('low')
|
|
131
|
+
Medium = new('medium')
|
|
132
|
+
High = new('high')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class Task < T::Struct
|
|
137
|
+
const :title, String
|
|
138
|
+
const :description, T.nilable(String)
|
|
139
|
+
const :priority, Priority
|
|
140
|
+
const :tags, T::Array[String]
|
|
141
|
+
const :metadata, T::Hash[String, T.any(String, Integer)]
|
|
142
|
+
const :subtasks, T::Array[Task]
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
[Priority, Task].map(&:to_baml).join("\n\n")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Generated BAML:**
|
|
151
|
+
```baml
|
|
152
|
+
enum Priority {
|
|
153
|
+
"low"
|
|
154
|
+
"medium"
|
|
155
|
+
"high"
|
|
53
156
|
}
|
|
157
|
+
|
|
158
|
+
class Task {
|
|
159
|
+
title string
|
|
160
|
+
description string?
|
|
161
|
+
priority Priority
|
|
162
|
+
tags string[]
|
|
163
|
+
metadata map<string, string | int>
|
|
164
|
+
subtasks Task[]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Advanced Features
|
|
169
|
+
|
|
170
|
+
### Dependency Management
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Dependencies automatically included with smart defaults
|
|
174
|
+
User.to_baml
|
|
175
|
+
# Outputs Address, then User in correct order
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Custom Formatting
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# Smart defaults include dependencies automatically
|
|
182
|
+
User.to_baml(indent_size: 4)
|
|
54
183
|
```
|
|
55
184
|
|
|
56
|
-
##
|
|
185
|
+
## Future Enhancements (Optional)
|
|
57
186
|
|
|
58
|
-
These
|
|
187
|
+
These are nice-to-have features for future versions:
|
|
59
188
|
|
|
60
|
-
- `T::Hash[K, V]` → `map<K, V>`
|
|
61
|
-
- `T.any(Type1, Type2)` → `Type1 | Type2`
|
|
62
|
-
- `T::Enum` subclasses → `enum Name { ... }`
|
|
63
189
|
- `T.type_alias` → `type Name = ...`
|
|
64
|
-
-
|
|
65
|
-
-
|
|
190
|
+
- Field descriptions from comments
|
|
191
|
+
- Custom naming strategies (snake_case ↔ camelCase)
|
|
192
|
+
- Self-referential type handling
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetBaml
|
|
5
|
+
# Extracts documentation comments from Sorbet struct and enum source files
|
|
6
|
+
class CommentExtractor
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T::Hash[String, T.nilable(String)]) }
|
|
10
|
+
def self.extract_field_comments(klass)
|
|
11
|
+
comments = {}
|
|
12
|
+
source_file = find_source_file(klass)
|
|
13
|
+
|
|
14
|
+
return comments unless source_file && File.exist?(source_file)
|
|
15
|
+
|
|
16
|
+
lines = File.readlines(source_file)
|
|
17
|
+
extract_comments_from_lines(lines, klass.name.split('::').last, comments)
|
|
18
|
+
|
|
19
|
+
comments
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(klass: T.class_of(T::Enum)).returns(T::Hash[String, T.nilable(String)]) }
|
|
23
|
+
def self.extract_enum_comments(klass)
|
|
24
|
+
comments = {}
|
|
25
|
+
source_file = find_source_file(klass)
|
|
26
|
+
|
|
27
|
+
return comments unless source_file && File.exist?(source_file)
|
|
28
|
+
|
|
29
|
+
lines = File.readlines(source_file)
|
|
30
|
+
extract_enum_comments_from_lines(lines, klass.name.split('::').last, comments)
|
|
31
|
+
|
|
32
|
+
comments
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
sig { params(klass: Class).returns(T.nilable(String)) }
|
|
38
|
+
def self.find_source_file(klass)
|
|
39
|
+
# Try to find where the class was defined
|
|
40
|
+
# This is a heuristic approach since Ruby doesn't provide reliable source location for classes
|
|
41
|
+
|
|
42
|
+
# Method 1: Check if any methods have source location
|
|
43
|
+
begin
|
|
44
|
+
if klass.respond_to?(:new) && klass.method(:new).respond_to?(:source_location)
|
|
45
|
+
location = klass.method(:new).source_location
|
|
46
|
+
return location[0] if location
|
|
47
|
+
end
|
|
48
|
+
rescue
|
|
49
|
+
# Ignore errors
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Method 2: Look at the current call stack for files that might contain the class
|
|
53
|
+
caller_locations.each do |location|
|
|
54
|
+
file_path = location.absolute_path || location.path
|
|
55
|
+
next unless file_path && File.exist?(file_path)
|
|
56
|
+
|
|
57
|
+
# Read the file and check if it contains the class definition
|
|
58
|
+
begin
|
|
59
|
+
content = File.read(file_path)
|
|
60
|
+
class_name = klass.name.split('::').last
|
|
61
|
+
if content.match(/class\s+#{Regexp.escape(class_name)}\s*</)
|
|
62
|
+
return file_path
|
|
63
|
+
end
|
|
64
|
+
rescue
|
|
65
|
+
# Ignore file read errors
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
73
|
+
def self.extract_comments_from_lines(lines, class_name, comments)
|
|
74
|
+
in_target_class = false
|
|
75
|
+
current_comment = T.let(nil, T.nilable(String))
|
|
76
|
+
brace_depth = 0
|
|
77
|
+
|
|
78
|
+
lines.each do |line|
|
|
79
|
+
stripped = line.strip
|
|
80
|
+
|
|
81
|
+
# Check if we're entering the target class
|
|
82
|
+
if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Struct/)
|
|
83
|
+
in_target_class = true
|
|
84
|
+
brace_depth = 0
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
next unless in_target_class
|
|
89
|
+
|
|
90
|
+
# Track brace depth to handle nested classes
|
|
91
|
+
brace_depth += stripped.count('{')
|
|
92
|
+
brace_depth -= stripped.count('}')
|
|
93
|
+
|
|
94
|
+
# Exit when we reach the end of the class
|
|
95
|
+
if stripped == 'end' && brace_depth == 0
|
|
96
|
+
break
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Extract comment
|
|
100
|
+
if stripped.start_with?('#')
|
|
101
|
+
comment_text = stripped[1..-1].strip
|
|
102
|
+
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
103
|
+
elsif stripped.match(/^const\s+:(\w+)/) && current_comment
|
|
104
|
+
field_name = stripped.match(/^const\s+:(\w+)/)[1]
|
|
105
|
+
comments[field_name] = current_comment
|
|
106
|
+
current_comment = nil
|
|
107
|
+
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
108
|
+
# Reset comment if we hit non-comment, non-const line
|
|
109
|
+
current_comment = nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
115
|
+
def self.extract_enum_comments_from_lines(lines, class_name, comments)
|
|
116
|
+
in_target_class = false
|
|
117
|
+
in_enums_block = false
|
|
118
|
+
current_comment = T.let(nil, T.nilable(String))
|
|
119
|
+
|
|
120
|
+
lines.each do |line|
|
|
121
|
+
stripped = line.strip
|
|
122
|
+
|
|
123
|
+
# Check if we're entering the target enum class
|
|
124
|
+
if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Enum/)
|
|
125
|
+
in_target_class = true
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
next unless in_target_class
|
|
130
|
+
|
|
131
|
+
# Check if we're in the enums block
|
|
132
|
+
if stripped == 'enums do'
|
|
133
|
+
in_enums_block = true
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Exit enums block
|
|
138
|
+
if in_enums_block && stripped == 'end'
|
|
139
|
+
in_enums_block = false
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Exit class
|
|
144
|
+
if stripped == 'end' && !in_enums_block
|
|
145
|
+
break
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
next unless in_enums_block
|
|
149
|
+
|
|
150
|
+
# Extract comment
|
|
151
|
+
if stripped.start_with?('#')
|
|
152
|
+
comment_text = stripped[1..-1].strip
|
|
153
|
+
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
154
|
+
elsif stripped.match(/^(\w+)\s*=\s*new/) && current_comment
|
|
155
|
+
enum_name = stripped.match(/^(\w+)\s*=\s*new/)[1]
|
|
156
|
+
comments[enum_name] = current_comment
|
|
157
|
+
current_comment = nil
|
|
158
|
+
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
159
|
+
# Reset comment if we hit non-comment, non-enum line
|
|
160
|
+
current_comment = nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|