From 179dbd0b8d00e1a5ff6374ab12fac5bff78a0637 Mon Sep 17 00:00:00 2001 From: Zev Blut Date: Wed, 7 Oct 2026 15:54:24 -0500 Subject: [PATCH 1/5] Pass arguments and keywords to IO.foreach, IO#each and IO#each_line enumerators Without a block these methods returned an Enumerator that passed keyword arguments back as a positional Hash, so iterating it raised TypeError (foreach, each_line) or ignored them (each). IO#each also dropped its positional arguments entirely. Co-Authored-By: Claude Opus 5.5 --- core/src/main/java/org/jruby/RubyEnumerator.java | 8 ++++++++ core/src/main/java/org/jruby/RubyIO.java | 14 +++++++------- spec/ruby/core/io/foreach_spec.rb | 6 ++++++ spec/ruby/core/io/shared/each.rb | 12 ++++++++++++ spec/tags/ruby/core/io/each_tags.txt | 1 - 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/jruby/RubyEnumerator.java b/core/src/main/java/org/jruby/RubyEnumerator.java index c8e43c5c6d6..431c45b450a 100644 --- a/core/src/main/java/org/jruby/RubyEnumerator.java +++ b/core/src/main/java/org/jruby/RubyEnumerator.java @@ -194,6 +194,14 @@ public static IRubyObject enumeratorize(Ruby runtime, IRubyObject object, String return new RubyEnumerator(runtime, runtime.getEnumerator(), object, runtime.fastNewSymbol(method), args); } + /** + * Like {@link #enumeratorize(Ruby, IRubyObject, String, IRubyObject...)}, but if keywords is true the last + * element of args is passed back to the method as keywords. + */ + public static IRubyObject enumeratorize(Ruby runtime, IRubyObject object, String method, IRubyObject[] args, boolean keywords) { + return new RubyEnumerator(runtime, runtime.getEnumerator(), object, runtime.fastNewSymbol(method), args, null, null, keywords); + } + public static IRubyObject enumeratorize(Ruby runtime, RubyClass type, IRubyObject object, String method) { return new RubyEnumerator(runtime, type, object, runtime.fastNewSymbol(method), NULL_ARRAY); } diff --git a/core/src/main/java/org/jruby/RubyIO.java b/core/src/main/java/org/jruby/RubyIO.java index 01767bc3ce8..f6b351cc0e8 100644 --- a/core/src/main/java/org/jruby/RubyIO.java +++ b/core/src/main/java/org/jruby/RubyIO.java @@ -3912,7 +3912,7 @@ public IRubyObject each(final ThreadContext context, final Block block) { @JRubyMethod(keywords = true) public IRubyObject each(final ThreadContext context, IRubyObject arg0, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each"); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", new IRubyObject[] { arg0 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, block, keywords); } @@ -3920,7 +3920,7 @@ public IRubyObject each(final ThreadContext context, IRubyObject arg0, final Blo @JRubyMethod(keywords = true) public IRubyObject each(final ThreadContext context, IRubyObject arg0, IRubyObject arg1, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each"); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", new IRubyObject[] { arg0, arg1 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, arg1, block, keywords); } @@ -3928,7 +3928,7 @@ public IRubyObject each(final ThreadContext context, IRubyObject arg0, IRubyObje @JRubyMethod(keywords = true) public IRubyObject each(final ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each"); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each", new IRubyObject[] { arg0, arg1, arg2 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, arg1, arg2, block, keywords); } @@ -3960,7 +3960,7 @@ public IRubyObject each_line(final ThreadContext context, final Block block) { public IRubyObject each_line(final ThreadContext context, IRubyObject arg0, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", new IRubyObject[] { arg0 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, block, keywords); } @@ -3969,7 +3969,7 @@ public IRubyObject each_line(final ThreadContext context, IRubyObject arg0, fina public IRubyObject each_line(final ThreadContext context, IRubyObject arg0, IRubyObject arg1, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0, arg1); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", new IRubyObject[] { arg0, arg1 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, arg1, block, keywords); } @@ -3978,7 +3978,7 @@ public IRubyObject each_line(final ThreadContext context, IRubyObject arg0, IRub public IRubyObject each_line(final ThreadContext context, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, final Block block) { boolean keywords = hasKeywords(ThreadContext.resetCallInfo(context)); - if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", arg0, arg1, arg2); + if (!block.isGiven()) return enumeratorize(context.runtime, this, "each_line", new IRubyObject[] { arg0, arg1, arg2 }, keywords); return Getline.getlineCall(context, GETLINE_YIELD, this, getReadEncoding(context), arg0, arg1, arg2, block, keywords); } @@ -4101,7 +4101,7 @@ public static IRubyObject foreach(final ThreadContext context, IRubyObject recv, Arity.checkArgumentCount(context, args.length - (keywords ? 1 : 0), 1, 4); - if (!block.isGiven()) return enumeratorize(context.runtime, recv, "foreach", args); + if (!block.isGiven()) return enumeratorize(context.runtime, recv, "foreach", args, keywords); return foreachInternal(context, recv, args, keywords, block); } diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb index 28d6fef7ae5..616574eac45 100644 --- a/spec/ruby/core/io/foreach_spec.rb +++ b/spec/ruby/core/io/foreach_spec.rb @@ -82,6 +82,12 @@ IO.foreach(@name).to_a.should == IOSpecs.lines end + it "returns an Enumerator that passes keyword arguments to IO.foreach" do + IO.foreach(@name, chomp: true).to_a.should == IOSpecs.lines_without_newline_characters + IO.foreach(@name, " ", chomp: true).to_a.should == IOSpecs.lines_space_separator_without_trailing_spaces + IO.foreach(@name, encoding: Encoding::BINARY).first.encoding.should == Encoding::BINARY + end + describe "returned Enumerator" do describe "size" do it "should return nil" do diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb index 0747f31b8a8..7378b0b938b 100644 --- a/spec/ruby/core/io/shared/each.rb +++ b/spec/ruby/core/io/shared/each.rb @@ -176,6 +176,10 @@ ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters end + it "returns an Enumerator that passes chomp to the method when no block is given" do + @io.send(@method, chomp: true).to_a.should == IOSpecs.lines_without_newline_characters + end + it "raises exception when options passed as Hash" do -> { @io.send(@method, { chomp: true }) { |s| } @@ -192,6 +196,10 @@ @io.send(@method, " ", chomp: true) { |s| ScratchPad << s } ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces end + + it "returns an Enumerator that passes the separator and chomp to the method when no block is given" do + @io.send(@method, " ", chomp: true).to_a.should == IOSpecs.lines_space_separator_without_trailing_spaces + end end describe "when passed chomp and empty line as a separator" do @@ -199,6 +207,10 @@ @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s } ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters end + + it "returns an Enumerator that passes the separator, limit and chomp to the method when no block is given" do + @io.send(@method, "", 1024, chomp: true).to_a.should == IOSpecs.paragraphs_without_trailing_new_line_characters + end end describe "when passed chomp and nil as a separator" do diff --git a/spec/tags/ruby/core/io/each_tags.txt b/spec/tags/ruby/core/io/each_tags.txt index 899582e417b..d594026613d 100644 --- a/spec/tags/ruby/core/io/each_tags.txt +++ b/spec/tags/ruby/core/io/each_tags.txt @@ -1,5 +1,4 @@ critical:IO#each with limit when limit is 0 raises an ArgumentError -fails:IO#each with both separator and limit when no block is given returns an Enumerator fails:IO#each when passed chomp raises exception when options passed as Hash windows:IO#each with no separator yields each line to the passed block windows:IO#each with no separator yields each line starting from the current position From 7fbb5e00dea6720d215196233311f7da38af4bef Mon Sep 17 00:00:00 2001 From: Zev Blut Date: Thu, 8 Oct 2026 12:00:44 -0500 Subject: [PATCH 2/5] Tag new IO enumerator specs as failing on Windows Windows checks out the spec fixtures with CRLF line endings, so these fail there like the block forms of the same specs, which are already tagged windows:. The separator-and-limit Enumerator spec for IO#each was tagged fails: until this PR and is tagged windows: for IO#each_line. Co-Authored-By: Claude Opus 5.5 --- spec/tags/ruby/core/io/each_line_tags.txt | 2 ++ spec/tags/ruby/core/io/each_tags.txt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/spec/tags/ruby/core/io/each_line_tags.txt b/spec/tags/ruby/core/io/each_line_tags.txt index 52310963efe..7b9b7b7817d 100644 --- a/spec/tags/ruby/core/io/each_line_tags.txt +++ b/spec/tags/ruby/core/io/each_line_tags.txt @@ -12,7 +12,9 @@ windows:IO#each_line with both separator and limit when a block is given when pa windows:IO#each_line with both separator and limit when a block is given when passed an empty String as a separator yields each paragraph windows:IO#each_line with both separator and limit when a block is given when passed an empty String as a separator discards leading newlines windows:IO#each_line when passed chomp and a separator yields each line without separator to the passed block +windows:IO#each_line when passed chomp and a separator returns an Enumerator that passes the separator and chomp to the method when no block is given windows:IO#each_line when passed chomp and empty line as a separator yields each paragraph without trailing new line characters +windows:IO#each_line when passed chomp and empty line as a separator returns an Enumerator that passes the separator, limit and chomp to the method when no block is given windows:IO#each_line when passed chomp and nil as a separator yields self's content windows:IO#each_line when passed chomp, nil as a separator, and a limit yields each line of limit size without truncating trailing new line character windows:IO#each_line uses $/ as the default line separator diff --git a/spec/tags/ruby/core/io/each_tags.txt b/spec/tags/ruby/core/io/each_tags.txt index d594026613d..0376e082db8 100644 --- a/spec/tags/ruby/core/io/each_tags.txt +++ b/spec/tags/ruby/core/io/each_tags.txt @@ -3,6 +3,7 @@ fails:IO#each when passed chomp raises exception when options passed as Hash windows:IO#each with no separator yields each line to the passed block windows:IO#each with no separator yields each line starting from the current position windows:IO#each with no separator when no block is given returns an Enumerator +windows:IO#each with both separator and limit when no block is given returns an Enumerator windows:IO#each when passed a String containing one space as a separator uses the passed argument as the line separator windows:IO#each when passed a String containing one space as a separator tries to convert the passed separator to a String using #to_str windows:IO#each when passed nil as a separator yields self's content starting from the current position when the passed separator is nil @@ -12,7 +13,9 @@ windows:IO#each with both separator and limit when a block is given when passed windows:IO#each with both separator and limit when a block is given when passed an empty String as a separator yields each paragraph windows:IO#each with both separator and limit when a block is given when passed an empty String as a separator discards leading newlines windows:IO#each when passed chomp and a separator yields each line without separator to the passed block +windows:IO#each when passed chomp and a separator returns an Enumerator that passes the separator and chomp to the method when no block is given windows:IO#each when passed chomp and empty line as a separator yields each paragraph without trailing new line characters +windows:IO#each when passed chomp and empty line as a separator returns an Enumerator that passes the separator, limit and chomp to the method when no block is given windows:IO#each when passed chomp and nil as a separator yields self's content windows:IO#each when passed chomp, nil as a separator, and a limit yields each line of limit size without truncating trailing new line character windows:IO#each uses $/ as the default line separator From 0e06fcf85a2987f7ca804155b02d7c26d3fd53e1 Mon Sep 17 00:00:00 2001 From: Zev Blut Date: Wed, 7 Oct 2026 18:48:20 -0500 Subject: [PATCH 3/5] Accept separator, limit and keywords in ARGF line methods ARGF.gets, readline, readlines, to_a, each and each_line counted the keyword Hash as a positional argument and only allowed one, so passing a separator and a limit, or a separator with chomp:, raised ArgumentError. ARGF#each and ARGF#to_a were not declared to take keywords, and the enumerators returned by each and each_line passed keywords back as a positional Hash, raising TypeError when iterated. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/org/jruby/RubyArgsFile.java | 49 ++++++++++--------- spec/ruby/core/argf/shared/each_line.rb | 41 ++++++++++++++++ spec/ruby/core/argf/shared/gets.rb | 18 +++++++ spec/ruby/core/argf/shared/readlines.rb | 13 +++++ 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/org/jruby/RubyArgsFile.java b/core/src/main/java/org/jruby/RubyArgsFile.java index c94959e056a..6bbc5c2a9aa 100644 --- a/core/src/main/java/org/jruby/RubyArgsFile.java +++ b/core/src/main/java/org/jruby/RubyArgsFile.java @@ -430,10 +430,10 @@ private static boolean isGenericInput(ThreadContext context, ArgsFileData data) /** Read a line. * */ - @JRubyMethod(name = "gets", optional = 1, keywords = true, checkArity = false, writes = LASTLINE) + @JRubyMethod(name = "gets", optional = 2, keywords = true, checkArity = false, writes = LASTLINE) public static IRubyObject gets(ThreadContext context, IRubyObject recv, IRubyObject[] args) { final int callInfo = ThreadContext.resetCallInfo(context); - Arity.checkArgumentCount(context, args, 0, 1); + checkGetlineArity(context, callInfo, args); return context.setLastLine(argf_getline(context, callInfo, args)); } @@ -441,7 +441,7 @@ public static IRubyObject gets(ThreadContext context, IRubyObject recv, IRubyObj /** Read a line. * */ - @JRubyMethod(name = "readline", optional = 1, keywords = true, checkArity = false, writes = LASTLINE) + @JRubyMethod(name = "readline", optional = 2, keywords = true, checkArity = false, writes = LASTLINE) public static IRubyObject readline(ThreadContext context, IRubyObject recv, IRubyObject[] args) { IRubyObject line = gets(context, recv, args); @@ -450,10 +450,10 @@ public static IRubyObject readline(ThreadContext context, IRubyObject recv, IRub return line; } - @JRubyMethod(optional = 1, keywords = true, checkArity = false) + @JRubyMethod(optional = 2, keywords = true, checkArity = false) public static IRubyObject readlines(ThreadContext context, IRubyObject recv, IRubyObject[] args) { final int callInfo = ThreadContext.resetCallInfo(context); - Arity.checkArgumentCount(context, args, 0, 1); + checkGetlineArity(context, callInfo, args); ArgsFileData data = ArgsFileData.getArgsFileData(context.runtime); @@ -472,21 +472,15 @@ public static IRubyObject readlines(ThreadContext context, IRubyObject recv, IRu return ary; } - @JRubyMethod(optional = 1, checkArity = false) + // MRI: to_a is an alias of readlines + @JRubyMethod(optional = 2, keywords = true, checkArity = false) public static IRubyObject to_a(ThreadContext context, IRubyObject recv, IRubyObject[] args) { - Arity.checkArgumentCount(context, args, 0, 1); - - ArgsFileData data = ArgsFileData.getArgsFileData(context.runtime); - - if (!data.next_argv(context)) return newEmptyArray(context); - if (!(data.currentFile instanceof RubyIO)) return data.currentFile.callMethod(context, "to_a", args); + return readlines(context, recv, args); + } - var ary = newArray(context); - IRubyObject line; - while ((line = argf_getline(context, 0, args)) != context.nil) { - ary.append(context, line); - } - return ary; + // MRI: rb_io_getline takes up to two positional arguments (separator, limit) plus keywords + private static void checkGetlineArity(ThreadContext context, int callInfo, IRubyObject[] args) { + Arity.checkArgumentCount(context, args.length - (hasKeywords(callInfo) ? 1 : 0), 0, 2); } @JRubyMethod @@ -584,12 +578,16 @@ public static IRubyObject codepoints(ThreadContext context, IRubyObject recv, Bl /** Invoke a block for each line. * */ - @JRubyMethod(name = "each_line", optional = 1, keywords = true, checkArity = false) + @JRubyMethod(name = "each_line", optional = 2, keywords = true, checkArity = false) public static IRubyObject each_line(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { - if (!block.isGiven()) return enumeratorize(context.runtime, recv, "each_line", args); - final int callInfo = ThreadContext.resetCallInfo(context); - Arity.checkArgumentCount(context, args, 0, 1); + if (!block.isGiven()) return enumeratorize(context.runtime, recv, "each_line", args, hasKeywords(callInfo)); + + return eachLineCommon(context, recv, callInfo, args, block); + } + + private static IRubyObject eachLineCommon(ThreadContext context, IRubyObject recv, int callInfo, IRubyObject[] args, Block block) { + checkGetlineArity(context, callInfo, args); ArgsFileData data = ArgsFileData.getArgsFileData(context.runtime); @@ -616,9 +614,12 @@ public static IRubyObject lines(ThreadContext context, IRubyObject recv, IRubyOb return each_line(context, recv, args, block); } - @JRubyMethod(name = "each", optional = 1, checkArity = false) + @JRubyMethod(name = "each", optional = 2, keywords = true, checkArity = false) public static IRubyObject each(final ThreadContext context, IRubyObject recv, IRubyObject[] args, final Block block) { - return block.isGiven() ? each_line(context, recv, args, block) : enumeratorize(context.runtime, recv, "each", args); + final int callInfo = ThreadContext.resetCallInfo(context); + if (!block.isGiven()) return enumeratorize(context.runtime, recv, "each", args, hasKeywords(callInfo)); + + return eachLineCommon(context, recv, callInfo, args, block); } @JRubyMethod(name = "file") diff --git a/spec/ruby/core/argf/shared/each_line.rb b/spec/ruby/core/argf/shared/each_line.rb index c0ef77dc543..330d133f40e 100644 --- a/spec/ruby/core/argf/shared/each_line.rb +++ b/spec/ruby/core/argf/shared/each_line.rb @@ -42,7 +42,48 @@ end end + describe "with a separator and a limit" do + it "yields each limited section of all streams" do + argf [@file1_name, @file2_name] do + lines = [] + @argf.send(@method, '.', 4) { |s| lines << s } + lines.should == (File.readlines(@file1_name, '.', 4) + File.readlines(@file2_name, '.', 4)) + end + end + end + + describe "when passed chomp" do + it "yields each line without the trailing separator" do + argf [@file1_name, @file2_name] do + lines = [] + @argf.send(@method, chomp: true) { |s| lines << s } + lines.should == @lines.map(&:chomp) + end + end + + it "yields each separated section without the separator when also passed a separator and a limit" do + argf [@file1_name, @file2_name] do + lines = [] + @argf.send(@method, '.', 8, chomp: true) { |s| lines << s } + lines.should == (File.readlines(@file1_name, '.', 8, chomp: true) + File.readlines(@file2_name, '.', 8, chomp: true)) + end + end + end + describe "when no block is given" do + it "returns an Enumerator that passes chomp to the method" do + argf [@file1_name, @file2_name] do + @argf.send(@method, chomp: true).to_a.should == @lines.map(&:chomp) + end + end + + it "returns an Enumerator that passes the separator, limit and chomp to the method" do + argf [@file1_name, @file2_name] do + @argf.send(@method, '.', 8, chomp: true).to_a.should == + (File.readlines(@file1_name, '.', 8, chomp: true) + File.readlines(@file2_name, '.', 8, chomp: true)) + end + end + it "returns an Enumerator" do argf [@file1_name, @file2_name] do @argf.send(@method).should be_an_instance_of(Enumerator) diff --git a/spec/ruby/core/argf/shared/gets.rb b/spec/ruby/core/argf/shared/gets.rb index 160d24c27bb..94c2cdf0d53 100644 --- a/spec/ruby/core/argf/shared/gets.rb +++ b/spec/ruby/core/argf/shared/gets.rb @@ -15,6 +15,24 @@ end end + it "reads one line of a file without the trailing separator when passed chomp" do + argf [@file1_name] do + @argf.send(@method, chomp: true).should == @file1.first.chomp + end + end + + it "reads up to the limit when passed a separator and a limit" do + argf [@file1_name] do + @argf.send(@method, ".", 4).should == "file" + end + end + + it "reads one separated section without the separator when passed a separator and chomp" do + argf [@file1_name] do + @argf.send(@method, ".", chomp: true).should == "file1" + end + end + it "reads all lines of a file" do argf [@file1_name] do lines = [] diff --git a/spec/ruby/core/argf/shared/readlines.rb b/spec/ruby/core/argf/shared/readlines.rb index 505fa94acbf..198f07bd7b6 100644 --- a/spec/ruby/core/argf/shared/readlines.rb +++ b/spec/ruby/core/argf/shared/readlines.rb @@ -13,6 +13,19 @@ end end + it "reads all lines of all files without the trailing separator when passed chomp" do + argf [@file1, @file2] do + @argf.send(@method, chomp: true).should == @lines.map(&:chomp) + end + end + + it "reads all limited sections of all files when passed a separator and a limit" do + argf [@file1, @file2] do + @argf.send(@method, ".", 4).should == + (File.readlines(@file1, ".", 4) + File.readlines(@file2, ".", 4)) + end + end + it "returns an empty Array when end of stream reached" do argf [@file1, @file2] do @argf.read From b2477d2bec0de831c97239c9c14fb271c4dc10b4 Mon Sep 17 00:00:00 2001 From: Zev Blut Date: Thu, 8 Oct 2026 00:39:10 -0500 Subject: [PATCH 4/5] Pass ARGF arguments and keywords on to a $stdin that is not an IO When $stdin is not an IO (for example a StringIO), ARGF#each and ARGF#each_line called $stdin.each with no arguments, dropping the separator, limit and keywords. ARGF.readlines passed keywords to $stdin.readlines as a positional Hash. Both now forward the arguments and keywords, and each/each_line call each_line as MRI's argf_each_line does. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/org/jruby/RubyArgsFile.java | 8 ++-- spec/ruby/core/argf/fixtures/classes.rb | 41 +++++++++++++++++++ spec/ruby/core/argf/shared/each_line.rb | 28 +++++++++++++ spec/ruby/core/argf/shared/readlines.rb | 24 +++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 spec/ruby/core/argf/fixtures/classes.rb diff --git a/core/src/main/java/org/jruby/RubyArgsFile.java b/core/src/main/java/org/jruby/RubyArgsFile.java index 6bbc5c2a9aa..a8e7c6999ed 100644 --- a/core/src/main/java/org/jruby/RubyArgsFile.java +++ b/core/src/main/java/org/jruby/RubyArgsFile.java @@ -460,7 +460,7 @@ public static IRubyObject readlines(ThreadContext context, IRubyObject recv, IRu if (!data.next_argv(context)) return newEmptyArray(context); if (!(data.currentFile instanceof RubyIO)) { - // TODO do we need to restore callInfo here? + context.callInfo = callInfo; // restore callInfo for kwargs return data.currentFile.callMethod(context, "readlines", args); } @@ -593,10 +593,10 @@ private static IRubyObject eachLineCommon(ThreadContext context, IRubyObject rec if (!data.next_argv(context)) return context.nil; + // MRI: argf_each_line calls each_line on the current file with the same arguments if (!(data.currentFile instanceof RubyIO)) { - if (!data.next_argv(context)) return recv; - - data.currentFile.callMethod(context, "each", NULL_ARRAY, block); + context.callInfo = callInfo; // restore callInfo for kwargs + data.currentFile.callMethod(context, "each_line", args, block); data.next_p = NextFile; } diff --git a/spec/ruby/core/argf/fixtures/classes.rb b/spec/ruby/core/argf/fixtures/classes.rb new file mode 100644 index 00000000000..3c8abbb180f --- /dev/null +++ b/spec/ruby/core/argf/fixtures/classes.rb @@ -0,0 +1,41 @@ +require 'stringio' + +module ARGFSpecs + # An object that is not an IO, for assigning to $stdin. It reads from a + # StringIO and records the arguments and keywords each method receives. + class Stdin + attr_reader :calls + + def initialize(string) + @io = StringIO.new(string) + @calls = [] + end + + def gets(*args, **kw) + @calls << [:gets, args, kw] + @io.gets(*args, **kw) + end + + def each_line(*args, **kw, &block) + @calls << [:each_line, args, kw] + @io.each_line(*args, **kw, &block) + self + end + + def each(*args, **kw, &block) + @calls << [:each, args, kw] + @io.each(*args, **kw, &block) + self + end + + def readlines(*args, **kw) + @calls << [:readlines, args, kw] + @io.readlines(*args, **kw) + end + + def to_a(*args, **kw) + @calls << [:to_a, args, kw] + @io.to_a(*args, **kw) + end + end +end diff --git a/spec/ruby/core/argf/shared/each_line.rb b/spec/ruby/core/argf/shared/each_line.rb index 330d133f40e..671a80ebc69 100644 --- a/spec/ruby/core/argf/shared/each_line.rb +++ b/spec/ruby/core/argf/shared/each_line.rb @@ -1,3 +1,5 @@ +require_relative '../fixtures/classes' + describe :argf_each_line, shared: true do before :each do @file1_name = fixture __FILE__, "file1.txt" @@ -70,6 +72,32 @@ end end + describe "when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + $stdin = ARGFSpecs::Stdin.new("a\nb\n") + @stdin_argf = ARGF.class.new + end + + after :each do + $stdin = @stdin + end + + it "calls each_line on $stdin and yields each line" do + lines = [] + @stdin_argf.send(@method) { |s| lines << s } + lines.should == ["a\n", "b\n"] + $stdin.calls.should == [[:each_line, [], {}]] + end + + it "passes the separator, limit and keywords to $stdin.each_line" do + lines = [] + @stdin_argf.send(@method, "\n", 1, chomp: true) { |s| lines << s } + lines.should == ["a", "", "b", ""] + $stdin.calls.should == [[:each_line, ["\n", 1], { chomp: true }]] + end + end + describe "when no block is given" do it "returns an Enumerator that passes chomp to the method" do argf [@file1_name, @file2_name] do diff --git a/spec/ruby/core/argf/shared/readlines.rb b/spec/ruby/core/argf/shared/readlines.rb index 198f07bd7b6..67124636ec7 100644 --- a/spec/ruby/core/argf/shared/readlines.rb +++ b/spec/ruby/core/argf/shared/readlines.rb @@ -1,3 +1,5 @@ +require_relative '../fixtures/classes' + describe :argf_readlines, shared: true do before :each do @file1 = fixture __FILE__, "file1.txt" @@ -26,6 +28,28 @@ end end + describe "when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + $stdin = ARGFSpecs::Stdin.new("a\nb\n") + @stdin_argf = ARGF.class.new + end + + after :each do + $stdin = @stdin + end + + it "calls readlines on $stdin" do + @stdin_argf.send(@method).should == ["a\n", "b\n"] + $stdin.calls.should == [[:readlines, [], {}]] + end + + it "passes the separator, limit and keywords to $stdin.readlines" do + @stdin_argf.send(@method, "\n", 1, chomp: true).should == ["a", "", "b", ""] + $stdin.calls.should == [[:readlines, ["\n", 1], { chomp: true }]] + end + end + it "returns an empty Array when end of stream reached" do argf [@file1, @file2] do @argf.read From 85ccd4a079e190e4c07aabe1a2156055a76f4969 Mon Sep 17 00:00:00 2001 From: Zev Blut Date: Thu, 8 Oct 2026 12:00:56 -0500 Subject: [PATCH 5/5] Tag new ARGF separator and limit specs as failing on Windows Windows checks out the spec fixtures with CRLF line endings, so the chomp results keep a "\r" and these fail there, like the IO line specs that are already tagged windows:. Co-Authored-By: Claude Opus 5.5 --- spec/tags/ruby/core/argf/each_line_tags.txt | 2 ++ spec/tags/ruby/core/argf/each_tags.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 spec/tags/ruby/core/argf/each_line_tags.txt create mode 100644 spec/tags/ruby/core/argf/each_tags.txt diff --git a/spec/tags/ruby/core/argf/each_line_tags.txt b/spec/tags/ruby/core/argf/each_line_tags.txt new file mode 100644 index 00000000000..e0a5e4b2e30 --- /dev/null +++ b/spec/tags/ruby/core/argf/each_line_tags.txt @@ -0,0 +1,2 @@ +windows:ARGF.each_line when passed chomp yields each separated section without the separator when also passed a separator and a limit +windows:ARGF.each_line when no block is given returns an Enumerator that passes the separator, limit and chomp to the method diff --git a/spec/tags/ruby/core/argf/each_tags.txt b/spec/tags/ruby/core/argf/each_tags.txt new file mode 100644 index 00000000000..e72eae1f70b --- /dev/null +++ b/spec/tags/ruby/core/argf/each_tags.txt @@ -0,0 +1,2 @@ +windows:ARGF.each when passed chomp yields each separated section without the separator when also passed a separator and a limit +windows:ARGF.each when no block is given returns an Enumerator that passes the separator, limit and chomp to the method