This document describes rbs subtract.
You can use rbs subtract
from command line.
# Print RBS to the stdout, which is generated.rbs - hand-written.rbs
$ rbs subtract generated.rbs hand-written.rbs
# It takes multiple minuends. The last argument becomes subtrahend.
$ rbs subtract generated-a.rbs generated-b.rbs hand-written.rbs
# It updates `.rbs` files directly with `-w` option.
$ rbs subtract -w generated-a.rbs generated-b.rbs hand-written.rbs
# It takes multiple subtrahends with --subtrahend option
$ rbs subtract generated.rbs --subtrahend=hand-written-a.rbs --subtrahend=hand-written-b.rbs
rbs subtract
focuses on conbining auto-generated RBSs and hand-written RBSs (or two kinds auto-generated RBSs).
There are several RBS generators. For example, rbs prototype
, RBS Rails, and so on. They are useful, but the RBSs generated by them is not complete, for example, they include untyped
.
So we want to override the generated RBSs with hand-written RBSs. But we had no good way to override them.
# auto-generated RBS
class C
# This definition doesn't describe the argument and returned value types,
# so we want to describe them with hand-written RBS.
def m: (untyped) -> untyped
end
# hand-written RBS
class C
# The following definition does not work with the generated RBS
# because RBS doesn't allow duplicated method definitions.
def m: (String) -> Integer
# The following definition is valid, but it is not the expected behavior
# because the overload still has `(untyped) -> untyped`.
def m: (String) -> Integer | ...
end
To solve this problem, we need to remove C#m
definition from the generated RBS. But modifing a generated file by the hand introduces hard maintainability sooner or later.
rbs subtract
solves this problem. It removes duplicated definitions from the generated RBS automatically. Then we can maintain the generated RBSs.
The rbs subtract
's goal is modifying generated RBSs to make it valid with the other RBSs. So, after rbs subtract a.rbs b.rbs > c.rbs
, the environment including b.rbs
and c.rbs
has to be valid.
We can use this command with the following workflow on a Rails application.
# Generate RBSs for Active Record models under sig/rbs_rails directory
$ bin/rake rbs_rails:all
# Generate RBSs for all Ruby code under sig/prototype directory
$ rbs prototype rb --out-dir=sig/prototype --base-dir=. app lib
# Remove methods generated by RBS Rails from sig/prototype
$ rbs subtract --write sig/prototype sig/rbs_rails
# Remove hand-written methods from generated RBSs
$ rbs subtract --write sig/prototype sig/rbs_rails sig/hand-written
Then the sig directory contains a complete RBS files as an environment. It means rbs -Isig validate
passes (if there is no missing classes and so on).
See the test file.
The main implementation is RBS::Subtractor
. It subtracts an environment from declarations.
It uses RBS::Environment
as the subtrahend.
It needs to merge several class declarations for the same class, so RBS::Declarations
is not appropriate for this purpose.
The subtrahend RBSs is probably incomplete RBS, for example, it may depend on the minuend RBS. RBS::DefinitionBuilder
does not work in this case, so it is inappropriate also.
rbs subtract
is not aware of interfaces mixins. For example
# minuend - generated
class C
def x: () -> untyped
end
# subtrahend - hand-written
class C
include _I
end
interface _I
def x: () -> untyped
end
# subtracted by `rbs subtract`
class C
def x: () -> untyped
end
x
method remains in the subtracted. Because it is actually defined by _I
, but not C
.
It causes duplicated method definition error, so I'd like to improve this situation.
class C
from subtracted if the subtrahend incldues interface mixins.
The subtracted RBS doesn't work with the subtrahend if the subtrahend contains a class/module with type parameters.
# a.rbs
class C
def foo: () -> untyped
end
# b.rbs
class C[T]
def bar: () -> untyped
end
# rbs subtract a.rbs b.rbs is the following, the same as a.rbs
# The type parameter of `C` is different, so it causes an error.
class C
def foo: () -> untyped
end
Currently rbs subtract
command removes attr_accessor
if the subtrahend contains one of the methods that attr_accessor
defines. For example
# minuend.rbs
class C
# It defines a and a=
attr_accessor a: String
end
# subtrahend.rbs
class C
def a: () -> String
end
In this case, rbs subtract a.rbs b.rbs
prints nothing. It removes C#a=
unexpectedly.
We can fix this problem more easily than other problems. We can convert attr_accessor
to a attr_{reader,writer}
in this case.
This section describes alternative approaches that I considered.
rbs subtract
treat the last argument as a subtrahend by default. But you can also specify multiple subtrahends by --subtrahend
option. For example:
# Specify one subtrahend
$ rbs subtract minuend.rbs subtrahend.rbs
# Specify two or more subtrahends
$ rbs subtract minuend.rbs --subtrahend=subtrahend_1.rbs --subtrahend=subtrahend_2.rbs
Specifying multiple subtrahends is useful on the following situaion.
.
└── sig
├── app
│ └── models/user.rbs
├── lib
│ └── lib.rbs
├── prototype
│ └── app/models/user.rbs
└── rbs_rails
└── app/moels/user.rbs
6 directories, 4 files
In this case, rbs subtract
executes (sig/prototype + sig/rbs_rails) - (sig/app + sig/lib)
, which takes two directories as the subtrahends.
I considered the following solutions too.
# Separate minuends and subtrahends by `-`
#
# It looks cool, but it is not common as CLI.
# And I'm not sure I can implement it easily with optparse, because `-` is a meta character of optparse.
$ rbs subtract sig/prototype sig/rbs_rails - sig/app sig/lib
# Add --minuend option
#
# It is not bad, but I like --subtrahend.
$ rbs subtract --minuend=sig/prototype --minuend=sig/rbs_rails sig/app sig/lib
# Add --minuend and --subtrahend options
#
# It is too redundant.
$ rbs subtract --minuend=sig/prototype --minuend=sig/rbs_rails \
--subtrahend=sig/app --subtrahend=sig/lib
# Specify comma separated files as subtrahend
#
# I do not want to implement the comma separated files because of escaping comma.
# It will introduce complexity.
$ rbs subtract sig/prototype sig/rbs_rails sig/app,sig/lib