Skip to content

Commit 9c4590f

Browse files
committed
refactor(compiler): always preserve directory structure in output
Remove preserve_structure option and implement smart path handling: - Single source_include: exclude source dir name from output (src/models/user.trb → build/models/user.rb) - Multiple source_include: include source dir name in output (src/models/user.trb → build/src/models/user.rb) - Files outside source directories: preserve relative path from cwd (external/foo.trb → build/external/foo.rb) Add resolve_path helper to handle macOS symlink paths (/var vs /private/var).
1 parent 6142231 commit 9c4590f

File tree

9 files changed

+307
-91
lines changed

9 files changed

+307
-91
lines changed

lib/t_ruby/cli.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ def init_project
105105
output:
106106
ruby_dir: #{build_dir}
107107
# rbs_dir: sig # Optional: separate directory for .rbs files
108-
preserve_structure: true
109108
# clean_before_build: false
110109
111110
compiler:

lib/t_ruby/compiler.rb

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,27 @@ def compile(input_path)
3838
# Transform source to Ruby code
3939
output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
4040

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))
4644

4745
File.write(output_path, output)
4846

4947
# Generate .rbs file if enabled in config
5048
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))
5351
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)
5553
else
56-
generate_rbs_file(base_filename, rbs_dir, parse_result)
54+
generate_rbs_file_to_path(rbs_path, parse_result)
5755
end
5856
end
5957

6058
# Generate .d.trb file if enabled in config (legacy support)
6159
# TODO: Add compiler.generate_dtrb option in future
6260
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)
6462
end
6563

6664
output_path
@@ -161,8 +159,70 @@ def optimization_stats
161159
@optimizer&.stats
162160
end
163161

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+
164216
private
165217

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+
166226
def setup_declaration_paths
167227
# Add default declaration paths
168228
@declaration_loader.add_search_path(@config.out_dir)
@@ -197,30 +257,29 @@ def transform_legacy(source, parse_result)
197257
end
198258
end
199259

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)
202262
generator = IR::RBSGenerator.new
203263
rbs_content = generator.generate(ir_program)
204-
205-
rbs_path = File.join(out_dir, "#{base_filename}.rbs")
206264
File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
207265
end
208266

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)
211269
generator = RBSGenerator.new
212270
rbs_content = generator.generate(
213271
parse_result[:functions] || [],
214272
parse_result[:type_aliases] || []
215273
)
216-
217-
rbs_path = File.join(out_dir, "#{base_filename}.rbs")
218274
File.write(rbs_path, rbs_content) unless rbs_content.empty?
219275
end
220276

221277
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+
222281
generator = DeclarationGenerator.new
223-
generator.generate_file(input_path, out_dir)
282+
generator.generate_file_to_path(input_path, dtrb_path)
224283
end
225284

226285
# Copy .rb file to output directory and generate .rbs signature
@@ -229,28 +288,25 @@ def copy_ruby_file(input_path)
229288
raise ArgumentError, "File not found: #{input_path}"
230289
end
231290

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))
237294

238295
# Copy the .rb file to output directory
239296
FileUtils.cp(input_path, output_path)
240297

241298
# Generate .rbs file if enabled in config
242299
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)
246303
end
247304

248305
output_path
249306
end
250307

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)
254310
result = `rbs prototype rb #{input_path} 2>/dev/null`
255311
File.write(rbs_path, result) unless result.strip.empty?
256312
end

lib/t_ruby/config.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class Config
1919
"output" => {
2020
"ruby_dir" => "build",
2121
"rbs_dir" => nil,
22-
"preserve_structure" => true,
2322
"clean_before_build" => false,
2423
},
2524
"compiler" => {
@@ -72,12 +71,6 @@ def rbs_dir
7271
@output["rbs_dir"] || ruby_dir
7372
end
7473

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-
8174
# Check if output directory should be cleaned before build
8275
# @return [Boolean] true if should clean before build
8376
def clean_before_build?

lib/t_ruby/declaration_generator.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ def generate_file(input_path, output_dir = nil)
6464
output_path
6565
end
6666

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+
6784
private
6885

6986
def generate_type_alias(type_alias)

spec/e2e/config_behavior_spec.rb

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,16 @@ def calculate(x: Integer, y: Integer): Integer
8888
end
8989
end
9090

91-
describe "output.preserve_structure" do
92-
context "when true (default)" do
93-
it "preserves directory structure in output" do
91+
describe "directory structure preservation" do
92+
context "with single source_include directory" do
93+
it "excludes source dir name from output path" do
9494
Dir.chdir(tmpdir) do
9595
create_config_file(<<~YAML)
9696
source:
9797
include:
9898
- src
9999
output:
100100
ruby_dir: build
101-
preserve_structure: true
102101
YAML
103102

104103
create_trb_file("src/models/user.trb", <<~TRB)
@@ -111,37 +110,36 @@ def find_user(id: Integer): String
111110
compiler = TRuby::Compiler.new(config)
112111
compiler.compile(File.join(tmpdir, "src/models/user.trb"))
113112

114-
# NOTE: Current compiler puts files in root of build dir
115-
# This test documents expected behavior, not current implementation
116-
expect(File.exist?(File.join(tmpdir, "build/user.rb"))).to be true
113+
# Single source_include: src/models/user.trb → build/models/user.rb
114+
expect(File.exist?(File.join(tmpdir, "build/models/user.rb"))).to be true
117115
end
118116
end
119117
end
120118

121-
# NOTE: preserve_structure: false is not yet implemented
122-
context "when false" do
123-
xit "flattens output to single directory" do
119+
context "with multiple source_include directories" do
120+
it "includes source dir name in output path" do
124121
Dir.chdir(tmpdir) do
125122
create_config_file(<<~YAML)
126123
source:
127124
include:
128125
- src
126+
- lib
129127
output:
130128
ruby_dir: build
131-
preserve_structure: false
132129
YAML
133130

134-
create_trb_file("src/deep/nested/file.trb", <<~TRB)
135-
def nested_func: void
136-
puts "hello"
131+
create_trb_file("src/models/user.trb", <<~TRB)
132+
def find_user(id: Integer): String
133+
"user"
137134
end
138135
TRB
139136

140137
config = TRuby::Config.new
141138
compiler = TRuby::Compiler.new(config)
142-
compiler.compile(File.join(tmpdir, "src/deep/nested/file.trb"))
139+
compiler.compile(File.join(tmpdir, "src/models/user.trb"))
143140

144-
expect(File.exist?(File.join(tmpdir, "build/file.rb"))).to be true
141+
# Multiple source_include: src/models/user.trb → build/src/models/user.rb
142+
expect(File.exist?(File.join(tmpdir, "build/src/models/user.rb"))).to be true
145143
end
146144
end
147145
end

spec/t_ruby/cli_init_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
expect(config["output"]).to be_a(Hash)
2929
expect(config["output"]["ruby_dir"]).to eq("build")
30-
expect(config["output"]["preserve_structure"]).to eq(true)
3130

3231
expect(config["compiler"]).to be_a(Hash)
3332
expect(config["compiler"]["generate_rbs"]).to eq(true)

spec/t_ruby/cli_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@
117117
File.write(input_file, "def hello; end")
118118

119119
allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir)
120+
allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir)
121+
allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir])
120122

121123
cli = TRuby::CLI.new([input_file])
122124
capture_stdout { cli.run }

0 commit comments

Comments
 (0)