Last active
May 16, 2025 17:13
-
-
Save nobodywasishere/6c1d42eda30d469346b21516b6294710 to your computer and use it in GitHub Desktop.
Type safe equality checker for Crystal lang (1.14.0)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# These are a series of monkey-patches designed to make type-unsafe | |
# equality comparisons (via `==` and `===`) more apparent via warnings, enabled | |
# by the `-Dtype_safe_equality_check` flag. Typically, you are allowed to compare | |
# anything to anything in Crystal, which can lead to cases like: | |
# | |
# ```crystal | |
# some_string[idx]? == "j" | |
# ``` | |
# | |
# Where everything compiles and runs fine, but this equality always evaluates to false | |
# as `String#[]?` returns `Char?`. This is a serious footgun with Crystal that I have | |
# run into a lot while developing, and has lead to several hidden bugs in my code. | |
# | |
# This implements a stricter equality, where deprecation warnings are given when | |
# comparing objects of different types, ignoring unions that only include a type and `Nil`. | |
# | |
# For example, this is valid: | |
# | |
# ```crystal | |
# def receive(msg : String?) | |
# msg == "hello" || msg == nil | |
# end | |
# ``` | |
# | |
# And this is not: | |
# | |
# ```crystal | |
# def receive(msg : String?) | |
# msg == :hello | |
# # ^^ warning: This equality is type-unsafe and may always evaluate to false. \ | |
# Use `#type_unsafe_equals?` if this is intentional. | |
# end | |
# ``` | |
# | |
# As a fallback, the method `#type_unsafe_equals?(other)` can be used to bypass this restriction. | |
# Unfortunately, there is not such a fallback for `case` statements at this time, so | |
# these may give warnings that are more difficult to work around. | |
# | |
# This only has an impact on code logic when the flag `-Dtype_safe_equality_check` is enabled, and it's | |
# recommend to only enable it for checks, not normal compilation. | |
# | |
# When using this, it is recommended to ignore all warnings coming from this file, stdlib, and the `lib/` folder. | |
# Codegen is also not required. On Linux / MacOS in a Makefile, this can be accomplished with the command: | |
# ```sh | |
# type_safe_equality_check: | |
# crystal build path/to/main/file.cr \ | |
# -Dtype_safe_equality_check \ | |
# --no-codegen --no-debug \ | |
# --error-on-warnings \ | |
# --exclude-warnings=lib \ | |
# --exclude-warnings=path/to/type_safe_equality.cr \ | |
# --exclude-warnings="$(shell crystal env CRYSTAL_PATH | cut -d':' -f2)" | |
# ``` | |
# | |
private macro type_safe_eq | |
{% if flag?(:type_safe_equality_check) %} | |
def ==(other : self?) | |
return false if other.nil? | |
self == other | |
end | |
def ==(other : Nil) | |
false | |
end | |
@[Deprecated("This equality is type-unsafe and may always evaluate to false. Use `#type_unsafe_equals?` if this is intentional.")] | |
def ==(other) | |
false | |
end | |
{% end %} | |
# NOTE(margret): Small helper method to swallow the deprecation warning | |
def type_unsafe_equals?(other) : Bool | |
self == other | |
end | |
end | |
struct Value | |
type_safe_eq | |
end | |
struct ReferenceStorage(T) | |
type_safe_eq | |
end | |
class Log::Metadata | |
type_safe_eq | |
end | |
abstract struct Enum | |
type_safe_eq | |
end | |
class Array(T) | |
type_safe_eq | |
end | |
struct StaticArray(T, N) | |
type_safe_eq | |
end | |
struct Complex | |
type_safe_eq | |
end | |
class Reference | |
type_safe_eq | |
end | |
struct Tuple | |
type_safe_eq | |
end | |
{% if flag?(:type_safe_equality_check) %} | |
struct Struct | |
def ==(other : self | Nil) : Bool | |
if other.is_a?(self) | |
{% for ivar in @type.instance_vars %} | |
# NOTE(margret): These could potentially be type-unsafe, but hard to debug | |
# them given the warning is added to here instead of the caller of `==`. | |
# These are swallowed by ignoring this file (for now) | |
return false unless @{{ivar.id}} == other.@{{ivar.id}} | |
{% end %} | |
end | |
!other.nil? | |
end | |
def ==(other : Nil) | |
false | |
end | |
@[Deprecated("This equality is type-unsafe and may always evaluate to false. Use `#type_unsafe_equals?` if this is intentional.")] | |
def ==(other) | |
false | |
end | |
end | |
class Object | |
def ===(other : self?) | |
return false if other.nil? | |
self == other | |
end | |
def ===(other : Nil) | |
false | |
end | |
@[Deprecated("This equality is type-unsafe and may always evaluate to false")] | |
def ===(other) | |
false | |
end | |
end | |
struct Nil | |
def ===(other) | |
false | |
end | |
def ==(other) | |
false | |
end | |
end | |
class Regex | |
# NOTE(margret): Monkey-patches to allow nillable comparisons, | |
# otherwise will fall back on `Object#==(other)` | |
def ===(other : String?) : Bool | |
return false if other.nil? | |
self === other | |
end | |
end | |
class String | |
# NOTE(margret): Monkey-patches to allow nillable comparisons, | |
# otherwise will fall back on `Object#==(other)` | |
def ===(other : String?) : Bool | |
return false if other.nil? | |
self === other | |
end | |
end | |
{% end %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment