rigortype 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -14
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +90 -1
|
@@ -88,6 +88,7 @@ module Rigor
|
|
|
88
88
|
to_h: :tuple_to_h,
|
|
89
89
|
zip: :tuple_zip,
|
|
90
90
|
:[] => :tuple_index,
|
|
91
|
+
slice: :tuple_index,
|
|
91
92
|
fetch: :tuple_index,
|
|
92
93
|
dig: :tuple_dig,
|
|
93
94
|
values_at: :tuple_values_at,
|
|
@@ -842,7 +843,10 @@ module Rigor
|
|
|
842
843
|
|
|
843
844
|
# `tuple.min` / `tuple.max` — fold when every element is
|
|
844
845
|
# a `Constant` whose values share a Ruby-comparable
|
|
845
|
-
# domain. Empty tuples fold to `Constant[nil]`.
|
|
846
|
+
# domain. Empty tuples fold to `Constant[nil]`. The 1-arg
|
|
847
|
+
# `min(n)` / `max(n)` form folds to a `Tuple` of the n
|
|
848
|
+
# edge-most values in Ruby's order (`min(n)` ascending,
|
|
849
|
+
# `max(n)` descending) — the n-arg sibling of `first(n)`.
|
|
846
850
|
def tuple_min(tuple, _method_name, args)
|
|
847
851
|
tuple_minmax(tuple, args, :min)
|
|
848
852
|
end
|
|
@@ -852,7 +856,7 @@ module Rigor
|
|
|
852
856
|
end
|
|
853
857
|
|
|
854
858
|
def tuple_minmax(tuple, args, edge)
|
|
855
|
-
return
|
|
859
|
+
return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
|
|
856
860
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
857
861
|
|
|
858
862
|
values = constant_values(tuple.elements)
|
|
@@ -864,6 +868,25 @@ module Rigor
|
|
|
864
868
|
nil
|
|
865
869
|
end
|
|
866
870
|
|
|
871
|
+
# `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
|
|
872
|
+
# edge-most element values, delegating to Ruby's
|
|
873
|
+
# `Array#min` / `#max` for the ordering. Declines on a
|
|
874
|
+
# non-static / negative count or non-Constant elements.
|
|
875
|
+
# The result is bounded by the tuple's known arity, so no
|
|
876
|
+
# extra size cap is needed.
|
|
877
|
+
def tuple_minmax_n(tuple, arg, edge)
|
|
878
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
879
|
+
return nil if arg.value.negative?
|
|
880
|
+
|
|
881
|
+
values = constant_values(tuple.elements)
|
|
882
|
+
return nil if values.nil?
|
|
883
|
+
|
|
884
|
+
picked = values.public_send(edge, arg.value)
|
|
885
|
+
Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
|
|
886
|
+
rescue StandardError
|
|
887
|
+
nil
|
|
888
|
+
end
|
|
889
|
+
|
|
867
890
|
# `tuple.minmax` — the `[min, max]` pair as a 2-slot
|
|
868
891
|
# `Tuple[Constant[min], Constant[max]]`, mirroring the
|
|
869
892
|
# `Range#minmax` fold. Every element must be a `Constant`
|
|
@@ -1201,6 +1224,13 @@ module Rigor
|
|
|
1201
1224
|
# indices still fall through because the same handler serves
|
|
1202
1225
|
# `fetch`, while statically nil slices can be represented
|
|
1203
1226
|
# precisely for `[]`.
|
|
1227
|
+
# `[]` and its exact alias `slice` share the index / Range /
|
|
1228
|
+
# start-length folding. `fetch` routes here too but stays
|
|
1229
|
+
# integer-index-only: the Range and start-length branches gate
|
|
1230
|
+
# on this selector set, which `fetch` is deliberately not in.
|
|
1231
|
+
SLICE_SELECTORS = Set[:[], :slice].freeze
|
|
1232
|
+
private_constant :SLICE_SELECTORS
|
|
1233
|
+
|
|
1204
1234
|
def tuple_index(tuple, method_name, args)
|
|
1205
1235
|
case args.size
|
|
1206
1236
|
when 1 then tuple_single_index(tuple, method_name, args.first)
|
|
@@ -1211,17 +1241,18 @@ module Rigor
|
|
|
1211
1241
|
def tuple_single_index(tuple, method_name, arg)
|
|
1212
1242
|
return nil unless arg.is_a?(Type::Constant)
|
|
1213
1243
|
|
|
1214
|
-
|
|
1215
|
-
return
|
|
1244
|
+
value = arg.value
|
|
1245
|
+
return tuple_range_slice(tuple, value) if SLICE_SELECTORS.include?(method_name) && value.is_a?(Range)
|
|
1246
|
+
return nil unless value.is_a?(Integer)
|
|
1216
1247
|
|
|
1217
|
-
idx = normalise_index(
|
|
1248
|
+
idx = normalise_index(value, tuple.elements.size)
|
|
1218
1249
|
return nil unless idx
|
|
1219
1250
|
|
|
1220
1251
|
tuple.elements[idx]
|
|
1221
1252
|
end
|
|
1222
1253
|
|
|
1223
1254
|
def tuple_start_length_slice(tuple, method_name, args)
|
|
1224
|
-
return nil unless method_name
|
|
1255
|
+
return nil unless SLICE_SELECTORS.include?(method_name)
|
|
1225
1256
|
|
|
1226
1257
|
start, length = args
|
|
1227
1258
|
return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
|
|
@@ -118,10 +118,12 @@ module Rigor
|
|
|
118
118
|
# table to suppress false positives for methods the
|
|
119
119
|
# user has defined but no RBS sig describes. Merged
|
|
120
120
|
# UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
# One combined descent yields both the discovered-methods existence
|
|
122
|
+
# table and the instance def-node table — see
|
|
123
|
+
# {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
|
|
124
|
+
# former onto the scope and returns the def-node table for
|
|
125
|
+
# `merge_project_method_indexes` below.
|
|
126
|
+
seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
|
|
125
127
|
|
|
126
128
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
127
129
|
# def nodes, the class -> superclass map, and the
|
|
@@ -134,7 +136,7 @@ module Rigor
|
|
|
134
136
|
# table. Seeded inside `merge_project_method_indexes` so the
|
|
135
137
|
# per-file visibilities merge OVER the cross-file project seed
|
|
136
138
|
# rather than overwriting it.
|
|
137
|
-
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
139
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
138
140
|
|
|
139
141
|
table = {}.compare_by_identity
|
|
140
142
|
table.default = seeded_scope
|
|
@@ -160,6 +162,19 @@ module Rigor
|
|
|
160
162
|
table
|
|
161
163
|
end
|
|
162
164
|
|
|
165
|
+
# Runs the combined methods/def-nodes descent (one walk of the file),
|
|
166
|
+
# seeds the discovered-methods existence table onto `seeded_scope`
|
|
167
|
+
# (merged UNDER the cross-file pre-pass seed `default_scope` carries),
|
|
168
|
+
# and returns `[scope, file_def_nodes]` so the caller can thread the
|
|
169
|
+
# def-node table into {#merge_project_method_indexes} without walking
|
|
170
|
+
# the file a second time.
|
|
171
|
+
def seed_discovered_methods(seeded_scope, default_scope, root)
|
|
172
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
173
|
+
discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
|
|
174
|
+
scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
|
|
175
|
+
[scope, file_def_nodes]
|
|
176
|
+
end
|
|
177
|
+
|
|
163
178
|
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
|
|
164
179
|
# ({Inference::StructFoldSafety}). Struct member layouts of constant
|
|
165
180
|
# receivers are resolved through the side-table the seeded scope carries.
|
|
@@ -179,9 +194,9 @@ module Rigor
|
|
|
179
194
|
# `discovered_def_index_for_paths` seed carried on
|
|
180
195
|
# `default_scope` — same-file declarations win per entry,
|
|
181
196
|
# the cross-file seed supplies sibling-file ancestors.
|
|
182
|
-
def merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
197
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
|
|
183
198
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
184
|
-
|
|
199
|
+
file_def_nodes
|
|
185
200
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
186
201
|
singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
|
|
187
202
|
build_discovered_singleton_def_nodes(root)
|
|
@@ -1406,16 +1421,29 @@ module Rigor
|
|
|
1406
1421
|
Type::Combinator.singleton_of(full)
|
|
1407
1422
|
end
|
|
1408
1423
|
|
|
1409
|
-
# Slice 7 phase 12 — in-source method discovery pre-pass
|
|
1410
|
-
#
|
|
1411
|
-
#
|
|
1412
|
-
#
|
|
1413
|
-
#
|
|
1414
|
-
# `
|
|
1415
|
-
def
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1424
|
+
# Slice 7 phase 12 — in-source method discovery pre-pass, fused with
|
|
1425
|
+
# the instance-method def-node pre-pass (v0.0.2 #5). One descent
|
|
1426
|
+
# produces BOTH tables the per-file `index` and the cross-file
|
|
1427
|
+
# pre-pass each need together:
|
|
1428
|
+
#
|
|
1429
|
+
# - `methods` : `{class_name => {method => :instance | :singleton}}`
|
|
1430
|
+
# for every `def` / `define_method(:name)` / `attr_*` / `alias` /
|
|
1431
|
+
# Data/Struct-member reader (the undefined-method existence table).
|
|
1432
|
+
# - `def_nodes` : `{class_name => {method => Prism::DefNode}}` for
|
|
1433
|
+
# every instance-side `def` (the inter-procedural return-inference
|
|
1434
|
+
# table; singleton defs and `define_method` are intentionally
|
|
1435
|
+
# skipped — `record_def_node` filters them).
|
|
1436
|
+
#
|
|
1437
|
+
# `walk_methods` and `walk_def_nodes` had byte-identical class /
|
|
1438
|
+
# module / singleton / meta-block descents (both stop at `DefNode`),
|
|
1439
|
+
# so a single combined walk records both accumulators at once instead
|
|
1440
|
+
# of traversing every file twice.
|
|
1441
|
+
def build_methods_and_def_nodes(root)
|
|
1442
|
+
methods = {}
|
|
1443
|
+
def_nodes = {}
|
|
1444
|
+
walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
|
|
1445
|
+
apply_alias_def_nodes(root, def_nodes)
|
|
1446
|
+
[methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
|
|
1419
1447
|
end
|
|
1420
1448
|
|
|
1421
1449
|
# Merges two `class_name => { method => kind }` tables, unioning
|
|
@@ -1431,7 +1459,14 @@ module Rigor
|
|
|
1431
1459
|
end
|
|
1432
1460
|
|
|
1433
1461
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
1434
|
-
|
|
1462
|
+
# Combined `walk_methods` + `walk_def_nodes` descent. The two walks
|
|
1463
|
+
# had identical class / module / singleton-class / meta-block
|
|
1464
|
+
# traversals and both stopped at `DefNode`; the only divergences are
|
|
1465
|
+
# leaf actions (recorded into the right accumulator) and the original
|
|
1466
|
+
# `walk_methods` returning at `AliasMethodNode` (its symbol-only
|
|
1467
|
+
# children carry no def / class node, so not descending them is
|
|
1468
|
+
# byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
|
|
1469
|
+
def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1435
1470
|
return unless node.is_a?(Prism::Node)
|
|
1436
1471
|
|
|
1437
1472
|
case node
|
|
@@ -1439,40 +1474,40 @@ module Rigor
|
|
|
1439
1474
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1440
1475
|
if name
|
|
1441
1476
|
child_prefix = qualified_prefix + [name]
|
|
1442
|
-
record_meta_superclass_members(node, child_prefix,
|
|
1443
|
-
|
|
1477
|
+
record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
|
|
1478
|
+
walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
|
|
1444
1479
|
return
|
|
1445
1480
|
end
|
|
1446
1481
|
when Prism::SingletonClassNode
|
|
1447
1482
|
if node.body
|
|
1448
1483
|
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1449
1484
|
if singleton_prefix
|
|
1450
|
-
|
|
1485
|
+
walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
|
|
1451
1486
|
return
|
|
1452
1487
|
end
|
|
1453
1488
|
end
|
|
1454
1489
|
when Prism::ConstantWriteNode
|
|
1455
1490
|
if meta_new_block_body(node)
|
|
1456
1491
|
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1457
|
-
|
|
1492
|
+
walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
|
|
1458
1493
|
return
|
|
1459
1494
|
end
|
|
1460
1495
|
when Prism::DefNode
|
|
1461
|
-
record_def_method(node, qualified_prefix, in_singleton_class,
|
|
1496
|
+
record_def_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1497
|
+
record_def_node(node, qualified_prefix, in_singleton_class, def_nodes_acc)
|
|
1462
1498
|
return
|
|
1463
1499
|
when Prism::AliasMethodNode
|
|
1464
|
-
record_alias_method(node, qualified_prefix, in_singleton_class,
|
|
1500
|
+
record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1465
1501
|
return
|
|
1466
1502
|
when Prism::CallNode
|
|
1467
|
-
record_define_method(node, qualified_prefix, in_singleton_class,
|
|
1503
|
+
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
1468
1504
|
if ATTR_MACROS.include?(node.name)
|
|
1469
|
-
record_attr_methods(node, qualified_prefix, in_singleton_class,
|
|
1470
|
-
accumulator)
|
|
1505
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1471
1506
|
end
|
|
1472
1507
|
end
|
|
1473
1508
|
|
|
1474
1509
|
node.compact_child_nodes.each do |child|
|
|
1475
|
-
|
|
1510
|
+
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
|
|
1476
1511
|
end
|
|
1477
1512
|
end
|
|
1478
1513
|
|
|
@@ -1606,57 +1641,6 @@ module Rigor
|
|
|
1606
1641
|
end
|
|
1607
1642
|
end
|
|
1608
1643
|
|
|
1609
|
-
# v0.0.2 #5 — instance-side def-node recording. Walks
|
|
1610
|
-
# class bodies the same way as `build_discovered_methods`
|
|
1611
|
-
# but records the actual `Prism::DefNode` for each
|
|
1612
|
-
# **instance** method so `ExpressionTyper` can re-type
|
|
1613
|
-
# the body at the call site for inter-procedural return
|
|
1614
|
-
# inference. Singleton methods and `define_method` calls
|
|
1615
|
-
# are intentionally skipped: the inference path needs a
|
|
1616
|
-
# statically introspectable body, and singleton dispatch
|
|
1617
|
-
# has its own complications (Class / Module ancestry)
|
|
1618
|
-
# the first-iteration rule does not yet model.
|
|
1619
|
-
def build_discovered_def_nodes(root)
|
|
1620
|
-
accumulator = {}
|
|
1621
|
-
walk_def_nodes(root, [], false, accumulator)
|
|
1622
|
-
apply_alias_def_nodes(root, accumulator)
|
|
1623
|
-
accumulator.transform_values(&:freeze).freeze
|
|
1624
|
-
end
|
|
1625
|
-
|
|
1626
|
-
def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1627
|
-
return unless node.is_a?(Prism::Node)
|
|
1628
|
-
|
|
1629
|
-
case node
|
|
1630
|
-
when Prism::ClassNode, Prism::ModuleNode
|
|
1631
|
-
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1632
|
-
if name
|
|
1633
|
-
child_prefix = qualified_prefix + [name]
|
|
1634
|
-
walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
|
|
1635
|
-
return
|
|
1636
|
-
end
|
|
1637
|
-
when Prism::SingletonClassNode
|
|
1638
|
-
if node.body
|
|
1639
|
-
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1640
|
-
if singleton_prefix
|
|
1641
|
-
walk_def_nodes(node.body, singleton_prefix, true, accumulator)
|
|
1642
|
-
return
|
|
1643
|
-
end
|
|
1644
|
-
end
|
|
1645
|
-
when Prism::ConstantWriteNode
|
|
1646
|
-
if meta_new_block_body(node)
|
|
1647
|
-
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1648
|
-
walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
|
|
1649
|
-
return
|
|
1650
|
-
end
|
|
1651
|
-
when Prism::DefNode
|
|
1652
|
-
record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
|
|
1653
|
-
return
|
|
1654
|
-
end
|
|
1655
|
-
|
|
1656
|
-
node.compact_child_nodes.each do |child|
|
|
1657
|
-
walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
1658
|
-
end
|
|
1659
|
-
end
|
|
1660
1644
|
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
1661
1645
|
# files DefNodes that live outside any class / module
|
|
1662
1646
|
# body (top-level helpers, `def`s nested inside DSL
|
|
@@ -2385,7 +2369,13 @@ module Rigor
|
|
|
2385
2369
|
# the override-visibility-reduced rule can read an ancestor's
|
|
2386
2370
|
# visibility declared in a sibling file.
|
|
2387
2371
|
def accumulate_project_index(acc, path, root)
|
|
2388
|
-
|
|
2372
|
+
# One combined descent yields both the methods existence table and
|
|
2373
|
+
# the def-node table; the latter is also consumed by
|
|
2374
|
+
# `record_class_sources`, so a def-dense file is walked once here
|
|
2375
|
+
# instead of three times (methods + def-nodes ×2). See
|
|
2376
|
+
# {#build_methods_and_def_nodes}.
|
|
2377
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2378
|
+
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2389
2379
|
build_discovered_singleton_def_nodes(root).each do |class_name, methods|
|
|
2390
2380
|
(acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
|
|
2391
2381
|
end
|
|
@@ -2395,20 +2385,28 @@ module Rigor
|
|
|
2395
2385
|
includes.each do |class_name, mods|
|
|
2396
2386
|
acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
|
|
2397
2387
|
end
|
|
2398
|
-
record_class_sources(acc[:class_sources], path, root, superclasses, includes)
|
|
2399
|
-
merge_class_keyed_index_tables(acc, root)
|
|
2388
|
+
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes)
|
|
2389
|
+
merge_class_keyed_index_tables(acc, root, file_methods)
|
|
2390
|
+
merge_member_layout_tables(acc, root)
|
|
2391
|
+
end
|
|
2392
|
+
|
|
2393
|
+
# Folds one file's Data + Struct member-layout tables into the
|
|
2394
|
+
# cross-file accumulator (kept out of {#accumulate_project_index} to
|
|
2395
|
+
# hold its ABC budget).
|
|
2396
|
+
def merge_member_layout_tables(acc, root)
|
|
2400
2397
|
acc[:data_member_layouts].merge!(build_data_member_layouts(root))
|
|
2401
2398
|
acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
|
|
2402
2399
|
end
|
|
2403
2400
|
|
|
2404
2401
|
# Folds the per-class method-visibility and method-existence tables of
|
|
2405
2402
|
# one file into the cross-file accumulator (kept out of
|
|
2406
|
-
# {#accumulate_project_index} to hold its ABC budget).
|
|
2407
|
-
def
|
|
2403
|
+
# {#accumulate_project_index} to hold its ABC budget). `file_methods`
|
|
2404
|
+
# is the existence table from the combined methods/def-nodes descent.
|
|
2405
|
+
def merge_class_keyed_index_tables(acc, root, file_methods)
|
|
2408
2406
|
build_discovered_method_visibilities(root).each do |class_name, table|
|
|
2409
2407
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
2410
2408
|
end
|
|
2411
|
-
|
|
2409
|
+
file_methods.each do |class_name, table|
|
|
2412
2410
|
(acc[:methods][class_name] ||= {}).merge!(table)
|
|
2413
2411
|
end
|
|
2414
2412
|
end
|
|
@@ -2423,13 +2421,13 @@ module Rigor
|
|
|
2423
2421
|
# dependency recording (ADR-46). The class-declaration walk
|
|
2424
2422
|
# (`collect_class_decls`) catches bodyless / def-less reopenings the
|
|
2425
2423
|
# other three builders miss.
|
|
2426
|
-
def record_class_sources(class_sources, path, root, superclasses, includes)
|
|
2424
|
+
def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
|
|
2427
2425
|
names = Set.new
|
|
2428
2426
|
collect_class_decls(root, [], decls = {})
|
|
2429
2427
|
names.merge(decls.keys)
|
|
2430
2428
|
names.merge(superclasses.keys)
|
|
2431
2429
|
names.merge(includes.keys)
|
|
2432
|
-
names.merge(
|
|
2430
|
+
names.merge(file_def_nodes.keys)
|
|
2433
2431
|
names.each { |name| (class_sources[name] ||= Set.new) << path }
|
|
2434
2432
|
end
|
|
2435
2433
|
|
|
@@ -2438,8 +2436,8 @@ module Rigor
|
|
|
2438
2436
|
# seen `"path:line"` definition site in `def_sources` (ADR-17 —
|
|
2439
2437
|
# the un-registered-project-patch signal `call.undefined-method`
|
|
2440
2438
|
# and `rigor triage` key on).
|
|
2441
|
-
def merge_discovered_defs(def_nodes, def_sources, path,
|
|
2442
|
-
|
|
2439
|
+
def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
|
|
2440
|
+
file_def_nodes.each do |class_name, methods|
|
|
2443
2441
|
(def_nodes[class_name] ||= {}).merge!(methods)
|
|
2444
2442
|
sources = (def_sources[class_name] ||= {})
|
|
2445
2443
|
methods.each do |method_name, def_node|
|
|
@@ -12,13 +12,13 @@ module Rigor
|
|
|
12
12
|
# `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
|
|
13
13
|
# before re-exec). Three backends behind one interface:
|
|
14
14
|
#
|
|
15
|
-
# - `none`
|
|
16
|
-
# Lowest cost; no isolation.
|
|
17
|
-
# invoked library is trusted + pure.
|
|
15
|
+
# - `none` — load into the main space and call directly.
|
|
16
|
+
# Lowest cost; no isolation. Used as the fallback where fork is
|
|
17
|
+
# unavailable; fine because the invoked library is trusted + pure.
|
|
18
18
|
# - `ruby_box` — call inside a {Box} (`Ruby::Box`, `RUBY_BOX=1`). Isolates
|
|
19
19
|
# core-class monkey-patches + lets gem versions coexist, but a native
|
|
20
20
|
# crash in the boxed work still takes the process down (in-process).
|
|
21
|
-
# - `process` — call in a forked worker ({Process}); returns data over a
|
|
21
|
+
# - `process` (**default**) — call in a forked worker ({Process}); returns data over a
|
|
22
22
|
# pipe. The strongest: a child crash (even `SIGSEGV`) is contained —
|
|
23
23
|
# the parent survives and declines. Higher cost (fork + IPC).
|
|
24
24
|
#
|
|
@@ -73,7 +73,7 @@ module Rigor
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# `none` — load the trusted library into the main space and call it
|
|
76
|
-
# directly. No isolation; lowest cost; the
|
|
76
|
+
# directly. No isolation; lowest cost; the fork-unavailable fallback.
|
|
77
77
|
module Direct
|
|
78
78
|
module_function
|
|
79
79
|
|
data/lib/rigor/plugin/loader.rb
CHANGED
|
@@ -183,8 +183,10 @@ module Rigor
|
|
|
183
183
|
Plugin.registered_for(newly_registered.first)
|
|
184
184
|
else
|
|
185
185
|
raise LoadError.new(
|
|
186
|
-
"plugin gem #{entry[:gem].inspect}
|
|
187
|
-
"(#{newly_registered.sort.inspect})
|
|
186
|
+
"plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
|
|
187
|
+
"(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
|
|
188
|
+
"it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
|
|
189
|
+
"(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
|
|
188
190
|
plugin_ref: entry[:gem]
|
|
189
191
|
)
|
|
190
192
|
end
|
data/lib/rigor/version.rb
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-ask
|
|
3
|
+
description: |
|
|
4
|
+
Rigor is a niche, fast-moving Ruby type checker; its rules, flags, and type behaviour are version-specific, so what you "remember" about it is likely wrong or stale — do NOT answer from memory or guess. For ANY question about Rigor, use this skill and investigate procedurally: run `rigor docs` (handbook + manual, bundled OFFLINE and version-matched), `rigor explain` for a diagnostic id, and for the user's own code `rigor check` / `annotate` / `type-of`, then answer only from what you read. Covers: why a line is flagged or if it's a false positive; the type model (narrowing, refinements, `Dynamic`, RBS); config keys, flags, baselines; comparisons to Sorbet, Steep, mypy, PHPStan; whether it handles Rails, RSpec, or a gem; writing an RBS signature; "what is Rigor / why use it / is it right for us?". Trigger on any Rigor mention seeking understanding — even casual, comparative, or grumbling. Skip only when Rigor isn't mentioned, or it's purely "set it up / fix / reduce it for me" (→ rigor-next-steps).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.2.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Ask Rigor anything
|
|
12
|
+
|
|
13
|
+
Someone has a question about Rigor. It might be about a diagnostic, the
|
|
14
|
+
type model, a flag, how Rigor stacks up against another type checker,
|
|
15
|
+
whether Rigor can handle their framework, how to type a method — or just
|
|
16
|
+
"what is this, and should I use it?" Whatever it is, **answer from the
|
|
17
|
+
source, not from memory.**
|
|
18
|
+
|
|
19
|
+
Two things make that easy, and you have both offline:
|
|
20
|
+
|
|
21
|
+
- **Rigor's own docs ship inside the gem.** `rigor docs` serves the full
|
|
22
|
+
handbook and manual, always matching the user's installed version, no
|
|
23
|
+
network. This is the authoritative copy: a rule's exact firing
|
|
24
|
+
condition, a flag's spelling, a config default — all drift release to
|
|
25
|
+
release, and `rigor docs` is the copy that shipped with *this* install,
|
|
26
|
+
so an answer drawn from it cannot disagree with the binary they run.
|
|
27
|
+
- **Rigor can read the user's actual code.** When the question is about
|
|
28
|
+
*their* program — "why is this flagged?", "what type does Rigor see
|
|
29
|
+
here?", "how well-typed is my project?" — `rigor check` / `annotate` /
|
|
30
|
+
`type-of` / `triage` / `coverage` answer from what Rigor inferred. A
|
|
31
|
+
concrete inferred type beats any abstract explanation.
|
|
32
|
+
|
|
33
|
+
This is the user's shortcut: they only ever need to remember two skills —
|
|
34
|
+
**`rigor-next-steps`** ("what should we do next?") and **`rigor-ask`**
|
|
35
|
+
("answer this about Rigor"). They ask in plain language; *you* turn it
|
|
36
|
+
into the right lookup or analysis so they never have to remember the
|
|
37
|
+
command.
|
|
38
|
+
|
|
39
|
+
## The toolbox
|
|
40
|
+
|
|
41
|
+
Everything here is read-only and needs no network.
|
|
42
|
+
|
|
43
|
+
### Reading the docs
|
|
44
|
+
|
|
45
|
+
| Command | Use |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `rigor docs` | The offline doc index (`llms.txt`) — the map. Start here when you don't know which page. |
|
|
48
|
+
| `rigor docs --list [manual\|handbook]` | List every bundled page with its path (optionally one category). |
|
|
49
|
+
| `rigor docs <name>` | Print a page. `<name>` is a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a unique short name (`narrowing`). Pages that exist in **both** trees (e.g. `plugins`) must be qualified — `manual/07-plugins` vs `handbook/09-plugins`. |
|
|
50
|
+
| `rigor explain <rule>` | The catalogue entry for a diagnostic id (`rigor explain call.undefined-method`) — what it means, why it fires, how to address it. |
|
|
51
|
+
|
|
52
|
+
### Grounding the answer in the user's code
|
|
53
|
+
|
|
54
|
+
| Command | Use |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `rigor check <path>` | Run the analysis. Scope it to a file or directory for a quick answer — don't analyse the whole project just to settle one question. `--format json` exposes structured fields (`receiver_type`, `method_name`, `evidence_tier`, …). |
|
|
57
|
+
| `rigor annotate <file>` | Reprint the file with the inferred type of each line in the margin — *what Rigor actually sees*. |
|
|
58
|
+
| `rigor type-of <file>:<line>:<col>` | The inferred type at one position. |
|
|
59
|
+
| `rigor triage` | Cluster the project's diagnostics by rule / receiver / method — for "what's the shape of my errors?". |
|
|
60
|
+
| `rigor coverage [--protection]` | Type / type-protection coverage — for "how well-typed is this?" and "where are the holes?". |
|
|
61
|
+
| `rigor plugins` | Which plugins are installed and enabled *here* — the honest answer to "does Rigor support <gem/framework>?". |
|
|
62
|
+
| `rigor sig-gen <path>` | Generate RBS for code — for "how do I type this?". Offer it and show the result; this project prefers sig-gen over hand-written RBS. |
|
|
63
|
+
|
|
64
|
+
## Where the answer lives
|
|
65
|
+
|
|
66
|
+
Classify the question, then go to the page(s) — and, for anything about
|
|
67
|
+
*their* code, the command(s) — that own it. When unsure where a page is,
|
|
68
|
+
`rigor docs` (the index) or `rigor docs --list handbook` routes you.
|
|
69
|
+
|
|
70
|
+
| The question is about… | Go to |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| **A specific diagnostic** — "why is this flagged?", "what does this error mean?", "is this a false positive?" | `rigor explain <rule>`, then `rigor docs diagnostics`. If it's *their* code, also `rigor annotate <file>` / `rigor type-of` to see the inferred types the rule fired on. |
|
|
73
|
+
| **The type model / a concept** — narrowing, refinements, tuple & hash shapes, `Dynamic`, RBS interop, lightweight HKT | The handbook: `rigor docs --list handbook`, then the chapter — `handbook/03-narrowing`, `04-tuples-and-shapes`, `07-rbs-and-extended`, `12-lightweight-hkt`, … |
|
|
74
|
+
| **Operating Rigor** — a config key, CLI flag, baseline, plugins, CI, caching | The manual: `rigor docs configuration`, `cli-reference`, `baseline`, `manual/07-plugins`, `ci`, `caching`, `troubleshooting`. |
|
|
75
|
+
| **How Rigor compares to another tool** — Sorbet, Steep, RBS, TypeScript, mypy, PHPStan, TypeProf, Go, Rust, Java/C# | The chapter/appendix written for exactly that: `handbook/10-sorbet`, `appendix-steep`, `appendix-typescript`, `appendix-mypy`, `appendix-phpstan`, `appendix-typeprof`, `appendix-rust`, `appendix-go`, `appendix-java-csharp` (`rigor docs --list handbook` shows them all). |
|
|
76
|
+
| **Whether Rigor can do X** — generics, Rails, RSpec, a specific gem, concurrency | The handbook for the language feature; for framework/gem support, **`rigor plugins`** (what's actually available in *this* install) plus the per-plugin page `rigor docs rigor-<gem>` (e.g. `rigor docs rigor-sidekiq`) and the catalogue `rigor docs --list manual`. |
|
|
77
|
+
| **Writing a type / RBS** — "how do I type this?", an annotation, a signature | Handbook `07-rbs-and-extended` + `11-sig-gen`; manual `rbs-extended-annotations`. Then offer `rigor sig-gen <path>` to generate it (preferred over hand-RBS) and show the output. |
|
|
78
|
+
| **What Rigor is / why use it / is it right for me** | Handbook `01-getting-started` for the pitch, `handbook/02-everyday-types` for a quick mental model of the type zoo. Ground "is it right for *my* project" in a scoped `rigor check` / `rigor coverage` so they see Rigor on their real code. |
|
|
79
|
+
|
|
80
|
+
## Answer from the page, name the page
|
|
81
|
+
|
|
82
|
+
Quote or paraphrase the relevant passage and **say which page you drew
|
|
83
|
+
from** (e.g. "per `rigor docs handbook/03-narrowing` …"), so the user can
|
|
84
|
+
re-read it with the same command. Prefer the doc's own wording over a
|
|
85
|
+
remembered approximation. When you ran a command against their code, show
|
|
86
|
+
the relevant line of output — a concrete inferred type is more convincing
|
|
87
|
+
than prose, and it proves the answer rather than asserting it.
|
|
88
|
+
|
|
89
|
+
## When the question is really "do X for me"
|
|
90
|
+
|
|
91
|
+
Some questions are a task in disguise: *"how do I get Rigor into CI?"*,
|
|
92
|
+
*"how do I shrink this baseline?"*, *"how do I set Rigor up here?"* The
|
|
93
|
+
useful reply is **short**: orient the user — what the thing is, the one
|
|
94
|
+
decision that actually matters, the rough shape of it — then **hand the
|
|
95
|
+
doing to the skill built for it.** Resist pasting the full procedure
|
|
96
|
+
inline (the entire CI workflow YAML, the whole baseline-reduction loop):
|
|
97
|
+
that skill owns the steps, keeps them correct, and updates as the tool
|
|
98
|
+
moves, so duplicating them here only bloats the answer and drifts out of
|
|
99
|
+
date. The line is *explaining* the thing (yours) versus *wiring it in*
|
|
100
|
+
(the setup skill's).
|
|
101
|
+
|
|
102
|
+
A good hand-off is two or three sentences of orientation plus the pointer:
|
|
103
|
+
|
|
104
|
+
- setup / "what next?" → **`rigor-next-steps`** (it probes the project and routes)
|
|
105
|
+
- CI → **`rigor-ci-setup`** · editor → **`rigor-editor-setup`** · MCP agent → **`rigor-mcp-setup`**
|
|
106
|
+
- baseline reduction → **`rigor-baseline-reduce`** · coverage holes → **`rigor-protection-uplift`**
|
|
107
|
+
- a missing gem/DSL → **`rigor-plugin-author`** · monkey-patch clusters → **`rigor-monkeypatch-resolve`**
|
|
108
|
+
|
|
109
|
+
When in doubt, give less and point — it respects the user's "two skills
|
|
110
|
+
to remember" promise and keeps each answer to the part only `rigor-ask`
|
|
111
|
+
can give.
|
|
112
|
+
|
|
113
|
+
## If the docs don't cover it
|
|
114
|
+
|
|
115
|
+
The bundled set is the **drive-Rigor** corpus (manual + handbook). The
|
|
116
|
+
normative **type specification**, the internal spec, and the ADRs are
|
|
117
|
+
contributor-facing and stay web-only — they are *not* in `rigor docs`; if
|
|
118
|
+
a question genuinely needs them, say so and point at
|
|
119
|
+
<https://rigor.typedduck.fail/llms.txt> rather than guessing.
|
|
120
|
+
|
|
121
|
+
But before you defer, remember Rigor installs from RubyGems **with its
|
|
122
|
+
full source** — the per-plugin pages under the gem's `docs/manual/plugins/`,
|
|
123
|
+
the analyzer and plugin code under `lib/`. For a detail no doc page spells
|
|
124
|
+
out (a plugin's exact rule, a default baked into the code), reading the
|
|
125
|
+
bundled file directly is a perfectly good way to ground the answer, and
|
|
126
|
+
beats a guess. The rule that never bends: **never invent a flag, rule id,
|
|
127
|
+
config key, behaviour, or command output** — read the page, read the
|
|
128
|
+
source, or run the command, and quote only output you actually saw. A
|
|
129
|
+
confident wrong answer about a type checker is worse than "let me check."
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
**A diagnostic on their code** — *"Why is Rigor flagging `s.lenght`?"*
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
rigor explain call.undefined-method # what the rule means and why it fires
|
|
137
|
+
rigor annotate demo.rb # the inferred type of `s` on that line
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Answer from both: Rigor inferred a concrete `String` receiver for `s`,
|
|
141
|
+
and `String` has no `lenght` (a typo for `length`) — grounded in what
|
|
142
|
+
`annotate` showed, not in a guess.
|
|
143
|
+
|
|
144
|
+
**A comparison** — *"How is Rigor different from Sorbet?"*
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
rigor docs handbook/10-sorbet # the chapter written for this
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Answer from the chapter's framing (RBS-superset, gradual `Dynamic`,
|
|
151
|
+
inference-first) rather than a remembered summary, and name it so they
|
|
152
|
+
can read on.
|
|
153
|
+
|
|
154
|
+
**A capability** — *"Does Rigor understand our Sidekiq workers?"*
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
rigor plugins # is rigor-sidekiq enabled in THIS project?
|
|
158
|
+
rigor docs rigor-sidekiq # the per-plugin page: what it teaches Rigor
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Answer from what's actually installed, plus — if useful — a scoped
|
|
162
|
+
`rigor check app/workers` so they see Rigor on their real workers.
|
|
163
|
+
|
|
164
|
+
**Authoring** — *"How do I type this method?"*
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
rigor sig-gen path/to/file.rb # generate the RBS, show it
|
|
168
|
+
rigor docs handbook/07-rbs-and-extended
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Generate it, show the signature, and explain it from the handbook —
|
|
172
|
+
preferring sig-gen's output over hand-written RBS.
|