t-ruby 0.0.35 → 0.0.37

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.
@@ -38,29 +38,27 @@ module TRuby
38
38
  # Transform source to Ruby code
39
39
  output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
40
40
 
41
- out_dir = @config.out_dir
42
- FileUtils.mkdir_p(out_dir)
43
-
44
- base_filename = File.basename(input_path, ".trb")
45
- output_path = File.join(out_dir, base_filename + ".rb")
41
+ # Compute output path (respects preserve_structure setting)
42
+ output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
43
+ FileUtils.mkdir_p(File.dirname(output_path))
46
44
 
47
45
  File.write(output_path, output)
48
46
 
49
47
  # Generate .rbs file if enabled in config
50
48
  if @config.compiler["generate_rbs"]
51
- rbs_dir = @config.rbs_dir
52
- FileUtils.mkdir_p(rbs_dir)
49
+ rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
50
+ FileUtils.mkdir_p(File.dirname(rbs_path))
53
51
  if @use_ir && parser.ir_program
54
- generate_rbs_from_ir(base_filename, rbs_dir, parser.ir_program)
52
+ generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
55
53
  else
56
- generate_rbs_file(base_filename, rbs_dir, parse_result)
54
+ generate_rbs_file_to_path(rbs_path, parse_result)
57
55
  end
58
56
  end
59
57
 
60
58
  # Generate .d.trb file if enabled in config (legacy support)
61
59
  # TODO: Add compiler.generate_dtrb option in future
62
60
  if @config.compiler.key?("generate_dtrb") && @config.compiler["generate_dtrb"]
63
- generate_dtrb_file(input_path, out_dir)
61
+ generate_dtrb_file(input_path, @config.ruby_dir)
64
62
  end
65
63
 
66
64
  output_path
@@ -98,19 +96,19 @@ module TRuby
98
96
  {
99
97
  ruby: ruby_output,
100
98
  rbs: rbs_output,
101
- errors: []
99
+ errors: [],
102
100
  }
103
101
  rescue ParseError => e
104
102
  {
105
103
  ruby: "",
106
104
  rbs: "",
107
- errors: [e.message]
105
+ errors: [e.message],
108
106
  }
109
107
  rescue StandardError => e
110
108
  {
111
109
  ruby: "",
112
110
  rbs: "",
113
- errors: ["Compilation error: #{e.message}"]
111
+ errors: ["Compilation error: #{e.message}"],
114
112
  }
115
113
  end
116
114
 
@@ -161,8 +159,70 @@ module TRuby
161
159
  @optimizer&.stats
162
160
  end
163
161
 
162
+ # Compute output path for a source file
163
+ # @param input_path [String] path to source file
164
+ # @param output_dir [String] base output directory
165
+ # @param new_extension [String] new file extension (e.g., ".rb", ".rbs")
166
+ # @return [String] computed output path (always preserves directory structure)
167
+ def compute_output_path(input_path, output_dir, new_extension)
168
+ relative = compute_relative_path(input_path)
169
+ base = relative.sub(/\.[^.]+$/, new_extension)
170
+ File.join(output_dir, base)
171
+ end
172
+
173
+ # Compute relative path from source directory
174
+ # @param input_path [String] path to source file
175
+ # @return [String] relative path preserving directory structure
176
+ def compute_relative_path(input_path)
177
+ # Use realpath to resolve symlinks (e.g., /var vs /private/var on macOS)
178
+ absolute_input = resolve_path(input_path)
179
+ source_dirs = @config.source_include
180
+
181
+ # Check if file is inside any source_include directory
182
+ if source_dirs.size > 1
183
+ # Multiple source directories: include the source dir name in output
184
+ # src/models/user.trb → src/models/user.trb
185
+ source_dirs.each do |src_dir|
186
+ absolute_src = resolve_path(src_dir)
187
+ next unless absolute_input.start_with?("#{absolute_src}/")
188
+
189
+ # Return path relative to parent of source dir (includes src dir name)
190
+ parent_of_src = File.dirname(absolute_src)
191
+ return absolute_input.sub("#{parent_of_src}/", "")
192
+ end
193
+ else
194
+ # Single source directory: exclude the source dir name from output
195
+ # src/models/user.trb → models/user.trb
196
+ src_dir = source_dirs.first
197
+ if src_dir
198
+ absolute_src = resolve_path(src_dir)
199
+ if absolute_input.start_with?("#{absolute_src}/")
200
+ return absolute_input.sub("#{absolute_src}/", "")
201
+ end
202
+ end
203
+ end
204
+
205
+ # File outside source directories: use path relative to current working directory
206
+ # external/foo.trb → external/foo.trb
207
+ cwd = resolve_path(".")
208
+ if absolute_input.start_with?("#{cwd}/")
209
+ return absolute_input.sub("#{cwd}/", "")
210
+ end
211
+
212
+ # Absolute path from outside cwd: use basename only
213
+ File.basename(input_path)
214
+ end
215
+
164
216
  private
165
217
 
218
+ # Resolve path to absolute path, following symlinks
219
+ # Falls back to expand_path if realpath fails (e.g., file doesn't exist yet)
220
+ def resolve_path(path)
221
+ File.realpath(path)
222
+ rescue Errno::ENOENT
223
+ File.expand_path(path)
224
+ end
225
+
166
226
  def setup_declaration_paths
167
227
  # Add default declaration paths
168
228
  @declaration_loader.add_search_path(@config.out_dir)
@@ -197,30 +257,29 @@ module TRuby
197
257
  end
198
258
  end
199
259
 
200
- # Generate RBS from IR
201
- def generate_rbs_from_ir(base_filename, out_dir, ir_program)
260
+ # Generate RBS from IR to a specific path
261
+ def generate_rbs_from_ir_to_path(rbs_path, ir_program)
202
262
  generator = IR::RBSGenerator.new
203
263
  rbs_content = generator.generate(ir_program)
204
-
205
- rbs_path = File.join(out_dir, base_filename + ".rbs")
206
264
  File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
207
265
  end
208
266
 
209
- # Legacy RBS generation
210
- def generate_rbs_file(base_filename, out_dir, parse_result)
267
+ # Legacy RBS generation to a specific path
268
+ def generate_rbs_file_to_path(rbs_path, parse_result)
211
269
  generator = RBSGenerator.new
212
270
  rbs_content = generator.generate(
213
271
  parse_result[:functions] || [],
214
272
  parse_result[:type_aliases] || []
215
273
  )
216
-
217
- rbs_path = File.join(out_dir, base_filename + ".rbs")
218
274
  File.write(rbs_path, rbs_content) unless rbs_content.empty?
219
275
  end
220
276
 
221
277
  def generate_dtrb_file(input_path, out_dir)
278
+ dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION)
279
+ FileUtils.mkdir_p(File.dirname(dtrb_path))
280
+
222
281
  generator = DeclarationGenerator.new
223
- generator.generate_file(input_path, out_dir)
282
+ generator.generate_file_to_path(input_path, dtrb_path)
224
283
  end
225
284
 
226
285
  # Copy .rb file to output directory and generate .rbs signature
@@ -229,28 +288,25 @@ module TRuby
229
288
  raise ArgumentError, "File not found: #{input_path}"
230
289
  end
231
290
 
232
- out_dir = @config.out_dir
233
- FileUtils.mkdir_p(out_dir)
234
-
235
- base_filename = File.basename(input_path, ".rb")
236
- output_path = File.join(out_dir, base_filename + ".rb")
291
+ # Compute output path (respects preserve_structure setting)
292
+ output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
293
+ FileUtils.mkdir_p(File.dirname(output_path))
237
294
 
238
295
  # Copy the .rb file to output directory
239
296
  FileUtils.cp(input_path, output_path)
240
297
 
241
298
  # Generate .rbs file if enabled in config
242
299
  if @config.compiler["generate_rbs"]
243
- rbs_dir = @config.rbs_dir
244
- FileUtils.mkdir_p(rbs_dir)
245
- generate_rbs_from_ruby(base_filename, rbs_dir, input_path)
300
+ rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
301
+ FileUtils.mkdir_p(File.dirname(rbs_path))
302
+ generate_rbs_from_ruby_to_path(rbs_path, input_path)
246
303
  end
247
304
 
248
305
  output_path
249
306
  end
250
307
 
251
- # Generate RBS from Ruby file using rbs prototype
252
- def generate_rbs_from_ruby(base_filename, out_dir, input_path)
253
- rbs_path = File.join(out_dir, base_filename + ".rbs")
308
+ # Generate RBS from Ruby file using rbs prototype to a specific path
309
+ def generate_rbs_from_ruby_to_path(rbs_path, input_path)
254
310
  result = `rbs prototype rb #{input_path} 2>/dev/null`
255
311
  File.write(rbs_path, result) unless result.strip.empty?
256
312
  end
@@ -273,20 +329,20 @@ module TRuby
273
329
  result = source.dup
274
330
 
275
331
  # Collect type alias names to remove
276
- type_alias_names = program.declarations
277
- .select { |d| d.is_a?(IR::TypeAlias) }
278
- .map(&:name)
332
+ program.declarations
333
+ .select { |d| d.is_a?(IR::TypeAlias) }
334
+ .map(&:name)
279
335
 
280
336
  # Collect interface names to remove
281
- interface_names = program.declarations
282
- .select { |d| d.is_a?(IR::Interface) }
283
- .map(&:name)
337
+ program.declarations
338
+ .select { |d| d.is_a?(IR::Interface) }
339
+ .map(&:name)
284
340
 
285
341
  # Remove type alias definitions
286
- result = result.gsub(/^\s*type\s+\w+\s*=\s*.+?$\n?/, '')
342
+ result = result.gsub(/^\s*type\s+\w+\s*=\s*.+?$\n?/, "")
287
343
 
288
344
  # Remove interface definitions (multi-line)
289
- result = result.gsub(/^\s*interface\s+\w+.*?^\s*end\s*$/m, '')
345
+ result = result.gsub(/^\s*interface\s+\w+.*?^\s*end\s*$/m, "")
290
346
 
291
347
  # Remove parameter type annotations using IR info
292
348
  # Enhanced: Handle complex types (generics, unions, etc.)
@@ -296,9 +352,7 @@ module TRuby
296
352
  result = erase_return_types(result)
297
353
 
298
354
  # Clean up extra blank lines
299
- result = result.gsub(/\n{3,}/, "\n\n")
300
-
301
- result
355
+ result.gsub(/\n{3,}/, "\n\n")
302
356
  end
303
357
 
304
358
  private
@@ -308,11 +362,11 @@ module TRuby
308
362
  result = source.dup
309
363
 
310
364
  # Match function definitions and remove type annotations from parameters
311
- result.gsub!(/^(\s*def\s+\w+\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |match|
312
- indent = $1
313
- params = $2
314
- close_paren = $3
315
- ending = $4
365
+ result.gsub!(/^(\s*def\s+\w+\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
366
+ indent = ::Regexp.last_match(1)
367
+ params = ::Regexp.last_match(2)
368
+ close_paren = ::Regexp.last_match(3)
369
+ ending = ::Regexp.last_match(4)
316
370
 
317
371
  # Remove type annotations from each parameter
318
372
  cleaned_params = remove_param_types(params)
@@ -340,7 +394,7 @@ module TRuby
340
394
  depth -= 1
341
395
  current += char
342
396
  when ","
343
- if depth == 0
397
+ if depth.zero?
344
398
  params << clean_param(current.strip)
345
399
  current = ""
346
400
  else
@@ -358,7 +412,7 @@ module TRuby
358
412
  # Clean a single parameter (remove type annotation)
359
413
  def clean_param(param)
360
414
  # Match: name: Type or name
361
- if match = param.match(/^(\w+)\s*:/)
415
+ if (match = param.match(/^(\w+)\s*:/))
362
416
  match[1]
363
417
  else
364
418
  param
@@ -370,7 +424,7 @@ module TRuby
370
424
  result = source.dup
371
425
 
372
426
  # Remove return type: ): Type or ): Type<Foo> etc.
373
- result.gsub!(/\)\s*:\s*[^\n]+?(?=\s*$)/m) do |match|
427
+ result.gsub!(/\)\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
374
428
  ")"
375
429
  end
376
430
 
data/lib/t_ruby/config.rb CHANGED
@@ -14,13 +14,12 @@ module TRuby
14
14
  "source" => {
15
15
  "include" => ["src"],
16
16
  "exclude" => [],
17
- "extensions" => [".trb"]
17
+ "extensions" => [".trb"],
18
18
  },
19
19
  "output" => {
20
20
  "ruby_dir" => "build",
21
21
  "rbs_dir" => nil,
22
- "preserve_structure" => true,
23
- "clean_before_build" => false
22
+ "clean_before_build" => false,
24
23
  },
25
24
  "compiler" => {
26
25
  "strictness" => "standard",
@@ -30,15 +29,15 @@ module TRuby
30
29
  "checks" => {
31
30
  "no_implicit_any" => false,
32
31
  "no_unused_vars" => false,
33
- "strict_nil" => false
34
- }
32
+ "strict_nil" => false,
33
+ },
35
34
  },
36
35
  "watch" => {
37
36
  "paths" => [],
38
37
  "debounce" => 100,
39
38
  "clear_screen" => false,
40
- "on_success" => nil
41
- }
39
+ "on_success" => nil,
40
+ },
42
41
  }.freeze
43
42
 
44
43
  # Legacy keys for migration detection
@@ -72,12 +71,6 @@ module TRuby
72
71
  @output["rbs_dir"] || ruby_dir
73
72
  end
74
73
 
75
- # Check if source directory structure should be preserved in output
76
- # @return [Boolean] true if structure should be preserved
77
- def preserve_structure?
78
- @output["preserve_structure"] != false
79
- end
80
-
81
74
  # Check if output directory should be cleaned before build
82
75
  # @return [Boolean] true if should clean before build
83
76
  def clean_before_build?
@@ -249,7 +242,7 @@ module TRuby
249
242
  value = strictness
250
243
  return if VALID_STRICTNESS.include?(value)
251
244
 
252
- raise ConfigError, "Invalid compiler.strictness: '#{value}'. Must be one of: #{VALID_STRICTNESS.join(', ')}"
245
+ raise ConfigError, "Invalid compiler.strictness: '#{value}'. Must be one of: #{VALID_STRICTNESS.join(", ")}"
253
246
  end
254
247
 
255
248
  def load_raw_config(config_path)
@@ -308,8 +301,8 @@ module TRuby
308
301
  result = deep_dup(DEFAULT_CONFIG)
309
302
 
310
303
  # Migrate emit -> compiler.generate_rbs
311
- if raw_config["emit"]
312
- result["compiler"]["generate_rbs"] = raw_config["emit"]["rbs"] if raw_config["emit"].key?("rbs")
304
+ if raw_config["emit"]&.key?("rbs")
305
+ result["compiler"]["generate_rbs"] = raw_config["emit"]["rbs"]
313
306
  end
314
307
 
315
308
  # Migrate paths -> source.include and output.ruby_dir
@@ -344,8 +337,12 @@ module TRuby
344
337
  end
345
338
 
346
339
  def deep_dup(hash)
347
- hash.each_with_object({}) do |(key, value), result|
348
- result[key] = value.is_a?(Hash) ? deep_dup(value) : (value.is_a?(Array) ? value.dup : value)
340
+ hash.transform_values do |value|
341
+ if value.is_a?(Hash)
342
+ deep_dup(value)
343
+ else
344
+ (value.is_a?(Array) ? value.dup : value)
345
+ end
349
346
  end
350
347
  end
351
348
 
@@ -42,7 +42,7 @@ module TRuby
42
42
  end
43
43
 
44
44
  def satisfied?(value_type)
45
- @left_type == value_type || @right_type == value_type
45
+ [@left_type, @right_type].include?(value_type)
46
46
  end
47
47
  end
48
48
 
@@ -62,6 +62,7 @@ module TRuby
62
62
  return false unless value.is_a?(Numeric)
63
63
  return false if @min && value < @min
64
64
  return false if @max && value > @max
65
+
65
66
  true
66
67
  end
67
68
 
@@ -78,6 +79,7 @@ module TRuby
78
79
  return "#{@min}..#{@max}" if @min && @max
79
80
  return ">= #{@min}" if @min
80
81
  return "<= #{@max}" if @max
82
+
81
83
  ""
82
84
  end
83
85
  end
@@ -94,6 +96,7 @@ module TRuby
94
96
 
95
97
  def satisfied?(value)
96
98
  return false unless value.is_a?(String)
99
+
97
100
  @pattern.match?(value)
98
101
  end
99
102
 
@@ -145,10 +148,12 @@ module TRuby
145
148
 
146
149
  def satisfied?(value)
147
150
  return false unless value.respond_to?(:length)
151
+
148
152
  len = value.length
149
153
  return len == @exact_length if @exact_length
150
154
  return false if @min_length && len < @min_length
151
155
  return false if @max_length && len > @max_length
156
+
152
157
  true
153
158
  end
154
159
 
@@ -167,6 +172,7 @@ module TRuby
167
172
 
168
173
  def build_condition
169
174
  return "length == #{@exact_length}" if @exact_length
175
+
170
176
  parts = []
171
177
  parts << "length >= #{@min_length}" if @min_length
172
178
  parts << "length <= #{@max_length}" if @max_length
@@ -187,7 +193,7 @@ module TRuby
187
193
  def register(name, base_type:, constraints: [])
188
194
  @constraints[name] = {
189
195
  base_type: base_type,
190
- constraints: constraints
196
+ constraints: constraints,
191
197
  }
192
198
  end
193
199
 
@@ -213,7 +219,7 @@ module TRuby
213
219
  return {
214
220
  name: name,
215
221
  base_type: base_type,
216
- constraints: [BoundsConstraint.new(subtype: name, supertype: base_type)]
222
+ constraints: [BoundsConstraint.new(subtype: name, supertype: base_type)],
217
223
  }
218
224
  end
219
225
 
@@ -325,8 +331,8 @@ module TRuby
325
331
  end
326
332
 
327
333
  # Pattern: /regex/
328
- if condition.match?(/^\/(.+)\/$/)
329
- match = condition.match(/^\/(.+)\/$/)
334
+ if condition.match?(%r{^/(.+)/$})
335
+ match = condition.match(%r{^/(.+)/$})
330
336
  constraints << PatternConstraint.new(base_type: base_type, pattern: match[1])
331
337
  end
332
338
 
@@ -353,7 +359,7 @@ module TRuby
353
359
  # Predicate: positive?, negative?, empty?, etc.
354
360
  if condition.match?(/^(\w+)\?$/)
355
361
  match = condition.match(/^(\w+)\?$/)
356
- constraints << PredicateConstraint.new(base_type: base_type, predicate: "#{match[1]}?".to_sym)
362
+ constraints << PredicateConstraint.new(base_type: base_type, predicate: :"#{match[1]}?")
357
363
  end
358
364
 
359
365
  constraints
@@ -367,7 +373,7 @@ module TRuby
367
373
  when "Numeric" then value.is_a?(Numeric)
368
374
  when "Array" then value.is_a?(Array)
369
375
  when "Hash" then value.is_a?(Hash)
370
- when "Boolean" then value == true || value == false
376
+ when "Boolean" then [true, false].include?(value)
371
377
  when "Symbol" then value.is_a?(Symbol)
372
378
  else true # Unknown types pass through
373
379
  end
@@ -388,7 +394,7 @@ module TRuby
388
394
  @types[name] = {
389
395
  base_type: base_type,
390
396
  constraints: constraints,
391
- defined_at: caller_locations(1, 1).first
397
+ defined_at: caller_locations(1, 1).first,
392
398
  }
393
399
  @checker.register(name, base_type: base_type, constraints: constraints)
394
400
  end
@@ -64,6 +64,23 @@ module TRuby
64
64
  output_path
65
65
  end
66
66
 
67
+ # Generate declaration file to a specific output path
68
+ def generate_file_to_path(input_path, output_path)
69
+ unless File.exist?(input_path)
70
+ raise ArgumentError, "File not found: #{input_path}"
71
+ end
72
+
73
+ unless input_path.end_with?(".trb")
74
+ raise ArgumentError, "Expected .trb file, got: #{input_path}"
75
+ end
76
+
77
+ source = File.read(input_path)
78
+ content = generate(source)
79
+
80
+ File.write(output_path, content)
81
+ output_path
82
+ end
83
+
67
84
  private
68
85
 
69
86
  def generate_type_alias(type_alias)
@@ -195,7 +212,7 @@ module TRuby
195
212
  {
196
213
  type_aliases: @type_aliases,
197
214
  interfaces: @interfaces,
198
- functions: @functions
215
+ functions: @functions,
199
216
  }
200
217
  end
201
218
 
@@ -230,13 +247,13 @@ module TRuby
230
247
 
231
248
  @search_paths.each do |path|
232
249
  full_path = File.join(path, file_name)
233
- if File.exist?(full_path) && !@loaded_files.include?(full_path)
234
- parser = DeclarationParser.new
235
- parser.parse_file(full_path)
236
- @loaded_declarations.merge(parser)
237
- @loaded_files.add(full_path)
238
- return true
239
- end
250
+ next unless File.exist?(full_path) && !@loaded_files.include?(full_path)
251
+
252
+ parser = DeclarationParser.new
253
+ parser.parse_file(full_path)
254
+ @loaded_declarations.merge(parser)
255
+ @loaded_files.add(full_path)
256
+ return true
240
257
  end
241
258
 
242
259
  false