-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add type checking with Sorbet #273
base: master
Are you sure you want to change the base?
Conversation
to unignore debugger.rb
Sorbet cannot resolve constants off child classes
Explain to Sorbet that this module will be included in a regular Object, not a BasicObject https://sorbet.org/docs/error-reference#7003
then ran srb rbi suggest-typed srb rbi hidden-definitions
srb init is magic and that needs to be fixed its steps should be documented in `srb rbi` help anyway it deletes sorbet/rbi/sorbet-typed/lib/yast2/all/report.rbi and we will have to restore it
autocorrect https://srb.help/5046
They are Ruby Interface definitions for the Sorbet type checker https://sorbet.org/ They enable checking that users of yast2-ruby-bindings are calling us with the correct types.
@@ -0,0 +1,61 @@ | |||
# typed: strong | |||
|
|||
module Yast::Builtins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi interesting, code few questions: Is there check that verify that rbi and real rb code matches?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
- If I make a mistake in builtins.rbi, it will go undetected because builtins.rb is still
typed: false
- convert.rb is
typed: true
but it defines its methods with metaprogramming so again, undetected :( - ops.rb is
typed: false
but if I change it totrue
there are few enough errors and one of them is in fact a mismatch that you ask about:
src/ruby/yast/ops.rb:113: Method Yast::Ops.get redefined without matching argument count. Expected: 3, got: 4 https://srb.help/4010
113 | def self.get(object, indexes, default = nil, skip_frames = 0)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
rbi/yast/ops.rbi:15: Previous definition
15 | def self.get(object, indexes, default=T.unsafe(nil), &block); end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
module Yast | ||
class Client | ||
include I18n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this needed? Cannot it load just from rb file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorbet takes great care to be fast so I guess the major reason is to be able to read just the RBI files of your dependencies and not parse and analyze all their sources.
|
||
# Namespacing note: | ||
# if we say `module Yast::I18n` then String means ::String | ||
# if we said `module Yast; module I18n` then String would mean Yast::String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe it would be better to be explicit and just use ::String?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yast::String should be destroyed, it is a design bug in YCP Killer.
Do you really want to write ::String
instead of String
everywhere?
Fortunately the two have such different APIs that an accidental confusion is resolved pretty quickly.
arg1: T.untyped | ||
).returns(T.untyped) | ||
end | ||
def self.Read(scr_path, arg1 = T.unsafe(nil)); end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write is missing due to WIP or is there other reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, just the minimal viable prototype. SCR.Write is pretty high in the TODO list.
@@ -0,0 +1,152 @@ | |||
# typed: strong | |||
module Yast | |||
module UIShortcuts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks quite fragile, maybe generate it by script? Also we basically kill type checking here as each type has own limitation for params
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way forward is to add more type information here, so generating this is a dead end.
So why did I include what appears to be a useless list?
It's because UIShortcuts is a mixin, and when I remove this RBI file, any typed: true
class that includes it will fail on unknown methods. (seen in yast2.rpm)
rbi/yast/wfm.rbi
Outdated
@@ -0,0 +1,24 @@ | |||
# typed: strong | |||
module Yast::WFM | |||
class NoParameter; end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks quite magic for me. For sure it will need some explanation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you are right, I did perpetrate magic here. Fortunately it did not have time to mutate so I can still remember the reason and will document it.
# See Args below
class NoParameter
...
# The following sig is a way to describe an optional parameter which has NOT a default value.
# It is a union of
# sig { params().returns(T::Array[YCPAny]) }
# sig { params(i: Integer).returns(YCPAny) }
Float, | ||
Symbol, | ||
String, | ||
T::Array[T.untyped], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cannot we have recursion here like T::Array[YCPAny]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's try... nope.
describe I18n do | ||
|
||
describe Yast do | ||
describe Yast::I18n do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks string, I prefer just `describe Yast::I18n" without that Yast one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/string/strange/ - yes, I just wanted to avoid reindenting. will fix.
tests/i18n_spec.rb
Outdated
|
||
let(:translated) { "translated".freeze } | ||
let(:singular) { "untranslated".freeze } | ||
let(:plural) { "plural".freeze } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why constants does not work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorbet is stricter than Ruby and thinks that constants defined in one describe
are not visible in a sibling describe
. I will commit a shorter fix which just moves the constants up one block level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeap, that will look better
This reverts commit 7e13dc0.
Sorbet is stricter than Ruby and thinks that constants defined in one `describe` are not visible in a *sibling* `describe` This fix is simpler than the one reverted in the parent commit.
Side note about typechecking RSpec tests: It is supposed to work but my initial attempts errored at not recognizing the "context" method. I see typechecking RSpec as low priority because the tests themselves have 100% code coverage.
typed: true uncovers type error that will be fixed in following commits Splat arguments (*args) are poorly supported by sorbet and in cases where they cannot be factored away (like our pervasive logging API) the way around is to turn off the argument checks by making the receiver(!) untyped with T.unsafe
a ** b might overflow an Integer so use a literal number instead
This is an actual bug, even if it is hard to trigger - the spaceship operator <=> would have to go mad and return something else than -1 0 1. [SorbetFoundABug]
if FastGettext.text_domain raises then the ensure call to textdomain would use an uninitialized value (nil)
Found corner cases (which were not type correct YCP) that produced weird exceptions, made them explanatory TypeErrors. Removed deep_copy on an Integer. The sig lies a bit in that it allows FalseClass as the object argument. If I omit it, the static type checker will complain that the "else" clause after "when String, when Array" can never be reached. That would be correct if the sig were in the RB file, but it is in the RBI file, not used at run time. I need to figure out the proper way to have sigs in RB and RBI and check that they are in sync. Currently it does not work :(
(Part of SUSE Hackweek: https://hackweek.suse.com/20/projects/type-check-yast-with-sorbet)
https://sorbet.org/ is a static (ahead of time) type checker.
How it works in general
Code is annotated with type declarations. Sorbet declarations are valid Ruby syntax and so can be inlined within the code or placed in separate files (RBI, for Ruby Interface).
The type checking is gradual, meaning that each file declares with a
# typed: level
header how strictly typed its code is:ignore
is unchecked,false
only checks constants (class names),true
is the general level of checking (method names, argument counts, variable types) and there arestrict
andstrong
levels for even more checks.The contents of the
sorbet/
directory is autogenerated.How it works in YaST
Manually written type declarations go to the
rbi/
directory and are installed in/usr/share/YaST2/rbi
to be used by dependent packages.In yast2-ruby-bindings
This being a basic package used everywhere else, I suppose we will want very high level of type checking eventually.
As the first step, my goal was to add just enough types to enable detecting an (artificial) bug in yast2.rpm: Annotating
Stage.rb
(a very short file) just enough to detect that another file has a typo,Stage.inital
instead ofStage.initial