Last active
January 2, 2026 05:53
-
-
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 | |
| @[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, | |
| # as warnings in this file is ignored by `make type_safe_equality_check` | |
| 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 | |
| @[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 | |
| # NOTE(margret): This doesn't have a `.type_unsafe_equals?` fallback, so disabling for now. | |
| # Enable to check type-safe equality in `case` statements | |
| # @[Deprecated("This equality is type-unsafe and may always evaluate to false")] | |
| # def ===(other) | |
| # self == other | |
| # 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 | |
| # https://github.com/crystal-lang/crystal/pull/8893#issuecomment-2646349090 | |
| class Hash(K, V) | |
| @[Deprecated("This index is type-unsafe and may always evaluate to false.")] | |
| def []?(key) : V? | |
| previous_def | |
| end | |
| def []?(key : K?) : V? | |
| self.[key]? | |
| end | |
| @[Deprecated("This index is type-unsafe and may always evaluate to false.")] | |
| def [](key) : V | |
| previous_def | |
| end | |
| def [](key : K?) : V | |
| self.[key] | |
| end | |
| @[Deprecated("This index is type-unsafe and may always evaluate to false.")] | |
| def has_key?(key) : Bool | |
| previous_def | |
| end | |
| def has_key?(key : K?) : Bool | |
| has_key?(key) | |
| end | |
| end | |
| module Enumerable(T) | |
| @[Deprecated("This method call is type-unsafe and may always evaluate to false.")] | |
| def count(item) : Int32 | |
| previous_def | |
| end | |
| def count(item : T?) : Int32 | |
| count(item) | |
| end | |
| @[Deprecated("This method call is type-unsafe and may always evaluate to false.")] | |
| def includes?(obj) : Bool | |
| previous_def | |
| end | |
| def includes?(obj : T?) : Bool | |
| includes?(obj) | |
| end | |
| @[Deprecated("This method call is type-unsafe and may always evaluate to false.")] | |
| def index(obj) : Int32? | |
| previous_def | |
| end | |
| def index(obj : T?) : Int32? | |
| index(obj) | |
| end | |
| @[Deprecated("This method call is type-unsafe and may always evaluate to false.")] | |
| def index!(obj) : Int32 | |
| previous_def | |
| end | |
| def index!(obj : T?) : Int32 | |
| index!(obj) | |
| end | |
| end | |
| {% end %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment