Skip to content

Add type restrictions to io #15698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/io.cr
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ abstract class IO
# reader.gets # => "hello"
# reader.gets # => "world"
# ```
def self.pipe(read_blocking = false, write_blocking = false) : {IO::FileDescriptor, IO::FileDescriptor}
def self.pipe(read_blocking : Bool = false, write_blocking : Bool = false) : {IO::FileDescriptor, IO::FileDescriptor}
Crystal::System::FileDescriptor.pipe(read_blocking, write_blocking)
end

Expand Down Expand Up @@ -173,7 +173,7 @@ abstract class IO
# io << "Crystal"
# io.to_s # => "1-Crystal"
# ```
def <<(obj) : self
def <<(obj : _) : self
obj.to_s self
self
end
Expand Down Expand Up @@ -264,12 +264,12 @@ abstract class IO

# Writes a formatted string to this IO.
# For details on the format string, see top-level `::printf`.
def printf(format_string, *args) : Nil
def printf(format_string : String, *args) : Nil
printf format_string, args
end

# :ditto:
def printf(format_string, args : Array | Tuple) : Nil
def printf(format_string : String, args : Array | Tuple) : Nil
String::Formatter(typeof(args)).new(format_string, args, self).format
end

Expand Down Expand Up @@ -414,7 +414,7 @@ abstract class IO
#
# "你".bytes # => [228, 189, 160]
# ```
def read_utf8(slice : Bytes)
def read_utf8(slice : Bytes) : Int32
if decoder = decoder()
decoder.read_utf8(self, slice)
else
Expand Down Expand Up @@ -594,7 +594,7 @@ abstract class IO
# io.gets # => "foo"
# io.gets # => nil
# ```
def gets(chomp = true) : String?
def gets(chomp : Bool = true) : String?
gets '\n', chomp: chomp
end

Expand All @@ -610,7 +610,7 @@ abstract class IO
# io.gets(3) # => "ld"
# io.gets(3) # => nil
# ```
def gets(limit : Int, chomp = false) : String?
def gets(limit : Int, chomp : Bool = false) : String?
gets '\n', limit: limit, chomp: chomp
end

Expand All @@ -624,7 +624,7 @@ abstract class IO
# io.gets('z') # => "ld"
# io.gets('w') # => nil
# ```
def gets(delimiter : Char, chomp = false) : String?
def gets(delimiter : Char, chomp : Bool = false) : String?
gets delimiter, Int32::MAX, chomp: chomp
end

Expand Down Expand Up @@ -910,7 +910,7 @@ abstract class IO
# io.rewind
# io.gets(4) # => "\u{4}\u{3}\u{2}\u{1}"
# ```
def write_bytes(object, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) : Nil
def write_bytes(object : _, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) : Nil
object.to_io(self, format)
end

Expand All @@ -928,7 +928,7 @@ abstract class IO
# io.rewind
# io.read_bytes(Int32, IO::ByteFormat::LittleEndian) # => 0x01020304
# ```
def read_bytes(type, format : IO::ByteFormat = IO::ByteFormat::SystemEndian)
def read_bytes(type : T.class, format : IO::ByteFormat = IO::ByteFormat::SystemEndian) : T forall T
type.from_io(self, format)
end

Expand Down Expand Up @@ -1170,7 +1170,7 @@ abstract class IO
#
# io2.to_s # => "hello"
# ```
def self.copy(src, dst) : Int64
def self.copy(src : IO, dst : IO) : Int64
buffer = uninitialized UInt8[DEFAULT_BUFFER_SIZE]
count = 0_i64
while (len = src.read(buffer.to_slice).to_i32) > 0
Expand All @@ -1190,7 +1190,7 @@ abstract class IO
#
# io2.to_s # => "hel"
# ```
def self.copy(src, dst, limit : Int) : Int64
def self.copy(src : IO, dst : IO, limit : Int) : Int64
raise ArgumentError.new("Negative limit") if limit < 0

limit = limit.to_i64
Expand Down
20 changes: 10 additions & 10 deletions src/io/buffered.cr
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module IO::Buffered
end

# :nodoc:
def skip(bytes_count) : Nil
def skip(bytes_count : Int) : Nil
check_open

if bytes_count <= @in_buffer_rem.size
Expand Down Expand Up @@ -172,7 +172,7 @@ module IO::Buffered
end

# :nodoc:
def write_byte(byte : UInt8)
def write_byte(byte : UInt8) : Nil
check_open

if sync?
Expand Down Expand Up @@ -215,9 +215,9 @@ module IO::Buffered
# Turns on/off `IO` **write** buffering. When *sync* is set to `true`, no buffering
# will be done (that is, writing to this `IO` is immediately synced to the
# underlying `IO`).
def sync=(sync)
def sync=(sync : Bool) : Bool
flush if sync && !@sync
@sync = !!sync
@sync = sync
end

# Determines if this `IO` does write buffering. If `true`, no buffering is done.
Expand All @@ -226,8 +226,8 @@ module IO::Buffered
end

# Turns on/off `IO` **read** buffering.
def read_buffering=(read_buffering)
@read_buffering = !!read_buffering
def read_buffering=(read_buffering : Bool) : Bool
@read_buffering = read_buffering
end

# Determines whether this `IO` buffers reads.
Expand All @@ -236,8 +236,8 @@ module IO::Buffered
end

# Turns on/off flushing the underlying `IO` when a newline is written.
def flush_on_newline=(flush_on_newline)
@flush_on_newline = !!flush_on_newline
def flush_on_newline=(flush_on_newline : Bool) : Bool
@flush_on_newline = flush_on_newline
end

# Determines if this `IO` flushes automatically when a newline is written.
Expand All @@ -246,7 +246,7 @@ module IO::Buffered
end

# Flushes any buffered data and the underlying `IO`. Returns `self`.
def flush
def flush : self
unbuffered_write(Slice.new(out_buffer, @out_count)) if @out_count > 0
unbuffered_flush
@out_count = 0
Expand All @@ -261,7 +261,7 @@ module IO::Buffered
end

# Rewinds the underlying `IO`. Returns `self`.
def rewind
def rewind : self
unbuffered_rewind
@in_buffer_rem = Bytes.empty
self
Expand Down
2 changes: 1 addition & 1 deletion src/io/delimited.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class IO::Delimited < IO
# byte sequence *read_delimiter* (interpreted as UTF-8) is found. If
# *sync_close* is set, calling `#close` calls `#close` on the underlying
# `IO`.
def self.new(io : IO, read_delimiter : String, sync_close : Bool = false)
def self.new(io : IO, read_delimiter : String, sync_close : Bool = false) : self
new(io, read_delimiter.to_slice, sync_close)
end

Expand Down
22 changes: 11 additions & 11 deletions src/io/encoding.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class IO
EncodingOptions.check_invalid(invalid)
end

def self.check_invalid(invalid) : Nil
def self.check_invalid(invalid : Symbol?) : Nil
Copy link
Contributor

@ysbaddaden ysbaddaden May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought (non-blocking): we should refactor to use an enum instead of a symbol. That would validate the value at comptime, instead of delaying to runtime, and it would be backward compatible, since symbols are automatically transformed into enums.

class IO
  struct EncodingOptions
    enum Invalid
      Skip
    end

    getter name : String
    getter invalid : Invalid?

    def initialize(@name : String, @invalid : Invalid?)
    end
  end

if invalid && invalid != :skip
raise ArgumentError.new "Valid values for `invalid` option are `nil` and `:skip`, not #{invalid.inspect}"
end
Expand All @@ -32,7 +32,7 @@ class IO
@closed = false
end

def write(io, slice : Bytes) : Nil
def write(io : IO, slice : Bytes) : Nil
inbuf_ptr = slice.to_unsafe
inbytesleft = LibC::SizeT.new(slice.size)
outbuf = uninitialized UInt8[1024]
Expand All @@ -53,7 +53,7 @@ class IO
@iconv.close
end

def finalize
def finalize : Nil
close
end
end
Expand All @@ -76,7 +76,7 @@ class IO
@closed = false
end

def read(io) : Nil
def read(io : IO) : Nil
loop do
return unless @out_slice.empty?

Expand Down Expand Up @@ -135,7 +135,7 @@ class IO
@in_buffer_left += LibC::SizeT.new(io.read(Slice.new(@in_buffer + @in_buffer_left, buffer_remaining)))
end

def read_byte(io) : UInt8?
def read_byte(io : IO) : UInt8?
read(io)
if out_slice.empty?
nil
Expand All @@ -146,7 +146,7 @@ class IO
end
end

def read_utf8(io, slice) : Int32
def read_utf8(io : IO, slice : Bytes) : Int32
count = 0
until slice.empty?
read(io)
Expand All @@ -161,7 +161,7 @@ class IO
count
end

def gets(io, delimiter : UInt8, limit : Int, chomp) : String?
def gets(io : IO, delimiter : UInt8, limit : Int, chomp : Bool) : String?
read(io)
return nil if @out_slice.empty?

Expand Down Expand Up @@ -229,17 +229,17 @@ class IO
string
end

def write(io) : Nil
def write(io : IO) : Nil
io.write @out_slice
@out_slice = Bytes.empty
end

def write(io, numbytes) : Nil
def write(io : IO, numbytes : Int) : Nil
io.write @out_slice[0, numbytes]
@out_slice += numbytes
end

def advance(numbytes) : Nil
def advance(numbytes : Int) : Nil
@out_slice += numbytes
end

Expand All @@ -250,7 +250,7 @@ class IO
@iconv.close
end

def finalize
def finalize : Nil
close
end
end
Expand Down
18 changes: 9 additions & 9 deletions src/io/file_descriptor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,29 @@ class IO::FileDescriptor < IO
# This might be different from the internal file descriptor. For example, when
# `STDIN` is a terminal on Windows, this returns `false` since the underlying
# blocking reads are done on a completely separate thread.
def blocking
def blocking : Bool
emulated = emulated_blocking?
return emulated unless emulated.nil?
system_blocking?
end

def blocking=(value)
def blocking=(value : Bool) : Nil
self.system_blocking = value
end

def close_on_exec? : Bool
system_close_on_exec?
end

def close_on_exec=(value : Bool)
def close_on_exec=(value : Bool) : Bool
self.system_close_on_exec = value
end

def self.fcntl(fd, cmd, arg = 0)
Crystal::System::FileDescriptor.fcntl(fd, cmd, arg)
end

def fcntl(cmd, arg = 0)
def fcntl(cmd : Int, arg : Int = 0) : Int
Crystal::System::FileDescriptor.fcntl(fd, cmd, arg)
end

Expand Down Expand Up @@ -199,7 +199,7 @@ class IO::FileDescriptor < IO
#
# NOTE: Metadata is flushed even when *flush_metadata* is false on Windows
# and DragonFly BSD.
def fsync(flush_metadata = true) : Nil
def fsync(flush_metadata : Bool = true) : Nil
flush
system_fsync(flush_metadata)
end
Expand All @@ -217,7 +217,7 @@ class IO::FileDescriptor < IO

# Places a shared advisory lock. More than one process may hold a shared lock for a given file descriptor at a given time.
# `IO::Error` is raised if *blocking* is set to `false` and an existing exclusive lock is set.
def flock_shared(blocking = true) : Nil
def flock_shared(blocking : Bool = true) : Nil
system_flock_shared(blocking)
end

Expand All @@ -232,7 +232,7 @@ class IO::FileDescriptor < IO

# Places an exclusive advisory lock. Only one process may hold an exclusive lock for a given file descriptor at a given time.
# `IO::Error` is raised if *blocking* is set to `false` and any existing lock is set.
def flock_exclusive(blocking = true) : Nil
def flock_exclusive(blocking : Bool = true) : Nil
system_flock_exclusive(blocking)
end

Expand All @@ -252,7 +252,7 @@ class IO::FileDescriptor < IO
# Resource release can be disabled with `close_on_finalize = false`.
#
# This method is a no-op if the file descriptor has already been closed.
def finalize
def finalize : Nil
return if closed? || !close_on_finalize?

Crystal::EventLoop.remove(self)
Expand All @@ -267,7 +267,7 @@ class IO::FileDescriptor < IO
system_tty?
end

def reopen(other : IO::FileDescriptor)
def reopen(other : IO::FileDescriptor) : IO::FileDescriptor
return other if self.fd == other.fd
system_reopen(other)

Expand Down
6 changes: 3 additions & 3 deletions src/io/memory.cr
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class IO::Memory < IO
# io.gets(2) # => "he"
# io.print "hi" # raises IO::Error
# ```
def self.new(string : String)
def self.new(string : String) : self
new string.to_slice, writeable: false
end

Expand Down Expand Up @@ -123,7 +123,7 @@ class IO::Memory < IO
end

# :nodoc:
def gets(delimiter : Char, limit : Int32, chomp = false) : String?
def gets(delimiter : Char, limit : Int32, chomp : Bool = false) : String?
return super if @encoding || delimiter.ord >= 128

check_open
Expand Down Expand Up @@ -182,7 +182,7 @@ class IO::Memory < IO
end

# :nodoc:
def skip(bytes_count) : Nil
def skip(bytes_count : Int) : Nil
check_open

available = @bytesize - @pos
Expand Down