rigortype 0.4.4 → 0.4.5
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 +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -300,17 +300,24 @@ module Rigor
|
|
|
300
300
|
# `Kernel` or `Enumerable` on any reopening. So each candidate name files the negative method edge of every
|
|
301
301
|
# {Relevance::HOOKS} name on its singleton side (`method:Comparable.included`, the key a new `def
|
|
302
302
|
# self.included` satisfies); a file adding any other method to the module re-checks nothing, and once the
|
|
303
|
-
# module is declared its sites carry later edits and the removal.
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
303
|
+
# module is declared its sites carry later edits and the removal. #1661 — each name also files `mixin:<name>`,
|
|
304
|
+
# which `IncrementalSession#mark_affected` fires when the module's unpositioned mixin edges move: a new file
|
|
305
|
+
# that taints it (`module Comparable; include(*MODS); end`) without a hook. The key is the qualified candidate,
|
|
306
|
+
# the spelling the chain resolves a project entry by. The unpositioned table may key the owner by a relative
|
|
307
|
+
# spelling (`Constants` inside `class File`), so the producer also fires every recorded `mixin:` key ending in
|
|
308
|
+
# `::<owner>` (`IncrementalSession#mixin_keys`); a taint on a project `Foo::Base` still re-checks no reader of
|
|
309
|
+
# an external `Lib::Base`. An ambiguous spelling has no candidate and files none: no project module can resolve
|
|
310
|
+
# it, so no taint can mark it. The edges are a function of the chain alone, so a consumer files them once per
|
|
311
|
+
# chain ({Analysis::DependencyRecorder.file_chain_once}, #1590). A hook written another way — an instance `def
|
|
312
|
+
# included` in a module extended onto the external, a `define_singleton_method(:included)` — satisfies no such
|
|
313
|
+
# key and is not seen.
|
|
307
314
|
def record_hooks(scope)
|
|
308
315
|
externals = (@memo[:externals] ||= @entries.select(&:external?).freeze)
|
|
309
316
|
return if externals.empty?
|
|
310
317
|
|
|
311
318
|
Analysis::DependencyRecorder.file_chain_once(self, :hooks, scope.discovery) do
|
|
312
319
|
externals.each do |entry|
|
|
313
|
-
entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.
|
|
320
|
+
entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.external_keys(name)) }
|
|
314
321
|
end
|
|
315
322
|
end
|
|
316
323
|
end
|
|
@@ -393,12 +400,18 @@ module Rigor
|
|
|
393
400
|
end
|
|
394
401
|
|
|
395
402
|
@hook_keys = {}
|
|
403
|
+
@external_keys = {}
|
|
396
404
|
|
|
397
405
|
# The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
|
|
398
406
|
def self.hook_keys(name)
|
|
399
407
|
@hook_keys[name] ||= Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
|
|
400
408
|
end
|
|
401
409
|
|
|
410
|
+
# {hook_keys} plus the `mixin:<name>` key an external entry files for its unpositioned mixins (#1661).
|
|
411
|
+
def self.external_keys(name)
|
|
412
|
+
@external_keys[name] ||= (hook_keys(name) + ["mixin:#{name}".freeze]).freeze
|
|
413
|
+
end
|
|
414
|
+
|
|
402
415
|
def record_head(scope, start, side)
|
|
403
416
|
ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
|
|
404
417
|
start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
|
|
@@ -440,6 +453,28 @@ module Rigor
|
|
|
440
453
|
@entries[@level_starts[index]...finish]
|
|
441
454
|
end
|
|
442
455
|
|
|
456
|
+
# Issue #1624 — whether the chain's last class has a superclass the tables cannot name, so ancestors the chain
|
|
457
|
+
# does not list follow it where a chain otherwise ends in the implicit `Object`: a superclass written as an
|
|
458
|
+
# expression (`class K < Struct.new(:a)`, `< Data.define(:a)`, `< Class.new(Base)`, `< pick_base`), which the
|
|
459
|
+
# superclass table records as `nil`, or a class a constant write creates (`K = Class.new(Base)`,
|
|
460
|
+
# `K = Struct.new(:a)`, `K = Data.define(:a)`), which has no superclass entry. The constructor's methods
|
|
461
|
+
# (`members`, the member readers, a block's `def`s) and everything it inherits live in that unlisted part.
|
|
462
|
+
# Every fact read here is filed under the last class's class edge, which a read reaching the tail has passed.
|
|
463
|
+
def opaque_tail?(scope)
|
|
464
|
+
@memo.fetch(:opaque_tail) do
|
|
465
|
+
last = level_count.positive? ? @level_classes[level_count - 1] : nil
|
|
466
|
+
@memo[:opaque_tail] = !last.nil? && ResolutionChain.opaque_superclass?(scope, last)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def self.opaque_superclass?(scope, name)
|
|
471
|
+
superclasses = scope.discovery.discovered_superclasses
|
|
472
|
+
return superclasses[name].nil? if superclasses.key?(name)
|
|
473
|
+
|
|
474
|
+
scope.discovery.discovered_parameter_envelopes[name]&.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK) ||
|
|
475
|
+
!(scope.struct_member_layout(name) || scope.data_member_layout(name)).nil?
|
|
476
|
+
end
|
|
477
|
+
|
|
443
478
|
# Each complete level up to the first superclass the project does not declare, as `[class_name,
|
|
444
479
|
# modules, externals]`: the level's class, the project modules around it (each once), and the candidate
|
|
445
480
|
# lists of the ancestors in it the project does not declare — the shape `call.wrong-arity` reads.
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -1213,13 +1213,28 @@ module Rigor
|
|
|
1213
1213
|
# wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
|
|
1214
1214
|
# even when no RBS sig describes them.
|
|
1215
1215
|
# A name defined on both sides of one class records {DiscoveryIndex::METHOD_KIND_BOTH} and matches either kind.
|
|
1216
|
+
#
|
|
1217
|
+
# Issue #2121 — a hit is an ADR-46 read. The table carries what no `def` symbol edge covers — an `attr_*`, an
|
|
1218
|
+
# `alias` or a `define_method` (a cross-file `def` is withheld from it) — so an answer that read a hit records
|
|
1219
|
+
# the class edge to the files declaring `class_name` and the name edge `defines:<method_name>`: deleting the
|
|
1220
|
+
# `attr_accessor` re-checks the caller it silenced, whether or not the deleting file declares the class. A miss
|
|
1221
|
+
# records nothing here; the readers that act on one record their own negative edges.
|
|
1216
1222
|
def discovered_method?(class_name, method_name, kind)
|
|
1217
1223
|
table = @discovery.discovered_methods[class_name.to_s]
|
|
1218
1224
|
return false unless table
|
|
1219
1225
|
|
|
1220
1226
|
recorded = table[method_name.to_sym]
|
|
1221
|
-
recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
|
|
1227
|
+
return false unless recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH
|
|
1228
|
+
|
|
1229
|
+
record_discovered_method(class_name, method_name) if Analysis::DependencyRecorder.active?
|
|
1230
|
+
true
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
def record_discovered_method(class_name, method_name)
|
|
1234
|
+
record_class_dependency(class_name)
|
|
1235
|
+
Analysis::DependencyRecorder.read_name(:defines, method_name)
|
|
1222
1236
|
end
|
|
1237
|
+
private :record_discovered_method
|
|
1223
1238
|
|
|
1224
1239
|
# ADR-34 § "Decision" — predicate identifying a toplevel-shaped scope (no enclosing `class` / `module` body).
|
|
1225
1240
|
# True at the top of a file AND inside a top-level `def` body (since toplevel defs leave `self_type` nil per
|
|
@@ -1410,6 +1425,27 @@ module Rigor
|
|
|
1410
1425
|
call_node)
|
|
1411
1426
|
end
|
|
1412
1427
|
|
|
1428
|
+
# Issue #1569 — whether `def_node`, written in this file, is a module function of `class_name`: a `:both`
|
|
1429
|
+
# deferred-range row over the def (a bare `module_function` before it, or `module_function def x`;
|
|
1430
|
+
# `ModuleFunctionState.prescan_deferred`), or a `:singleton` row for the name after the def
|
|
1431
|
+
# (`module_function :x`) whose singleton copy is this def (`ModuleFunctionState.each_singleton_copy`).
|
|
1432
|
+
def module_function_def?(class_name, def_node)
|
|
1433
|
+
path = source_path
|
|
1434
|
+
return false if path.nil?
|
|
1435
|
+
|
|
1436
|
+
ranges = discovered_deferred_ranges[path]
|
|
1437
|
+
return false if ranges.nil?
|
|
1438
|
+
|
|
1439
|
+
start = def_node.location.start_offset
|
|
1440
|
+
ranges.any? do |(row_start, _finish, name, kind, owner)|
|
|
1441
|
+
next false unless name == def_node.name && owner == class_name.to_s
|
|
1442
|
+
|
|
1443
|
+
(kind == :both && row_start == start) ||
|
|
1444
|
+
(kind == :singleton && row_start > start &&
|
|
1445
|
+
singleton_def_for(class_name, name)&.location&.start_offset == start)
|
|
1446
|
+
end
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1413
1449
|
# Shared ordering half of the two `*_def_shadows_call?` predicates, over this file's
|
|
1414
1450
|
# `discovered_deferred_ranges` (issue #1097). `exists` — a recorded `"path:line"` site or the
|
|
1415
1451
|
# method-existence table — says a def of `method_name`/`kind` on `class_name` is known at all; a
|
|
@@ -1917,8 +1953,15 @@ module Rigor
|
|
|
1917
1953
|
# Issue #992 — one class's `discovered_parameter_envelopes` bucket (`{[kind, method_name] => envelope}` plus
|
|
1918
1954
|
# the class-wide marks), or an empty Hash. Records the ADR-46 class edge first: a verdict read off the
|
|
1919
1955
|
# bucket depends on every file that declares the class, and a `memoize :f` added to one of them moves it.
|
|
1956
|
+
#
|
|
1957
|
+
# Issue #1833 — and the name edge `mark:<last segment>`: a file that does not declare the class may still mark it
|
|
1958
|
+
# (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a method body, a `refine` block), and
|
|
1959
|
+
# `IncrementalSession#mark_affected` fires the key when such a mark appears or vanishes.
|
|
1920
1960
|
def parameter_envelopes_of(class_name)
|
|
1921
|
-
|
|
1961
|
+
if Analysis::DependencyRecorder.active?
|
|
1962
|
+
record_class_dependency(class_name)
|
|
1963
|
+
Analysis::DependencyRecorder.read_last_segment(:mark, class_name.to_s)
|
|
1964
|
+
end
|
|
1922
1965
|
@discovery.discovered_parameter_envelopes[class_name.to_s] || EMPTY_PARAMETER_ENVELOPES
|
|
1923
1966
|
end
|
|
1924
1967
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../inference/rbs_type_translator"
|
|
4
4
|
require_relative "type_elaborator"
|
|
5
|
+
require_relative "../rooted_glob"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module SigGen
|
|
@@ -101,7 +102,7 @@ module Rigor
|
|
|
101
102
|
dir = path.is_a?(Pathname) ? path : Pathname(path.to_s)
|
|
102
103
|
next [] unless dir.directory?
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
RootedGlob.glob(dir, "**/*.rbs").map { |p| File.expand_path(p) }
|
|
105
106
|
end.to_set
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -60,7 +60,10 @@ module Rigor
|
|
|
60
60
|
inline_differs: "sig.skipped.inline-differs",
|
|
61
61
|
# #1676 — a plain top-level `def` defines a private `Object` method and has no class to write a
|
|
62
62
|
# declaration under, so it is reported rather than silently dropped. Whether to emit it stays open.
|
|
63
|
-
top_level_def: "sig.skipped.top-level-def"
|
|
63
|
+
top_level_def: "sig.skipped.top-level-def",
|
|
64
|
+
# #1968 — a bare `public` / `private` / `protected` that may not run (in control flow or a block) sits
|
|
65
|
+
# between a bare `module_function` and the def, so whether the def is a module function is unknown.
|
|
66
|
+
uncertain_module_function: "sig.skipped.uncertain-module-function"
|
|
64
67
|
}.freeze
|
|
65
68
|
end
|
|
66
69
|
end
|
|
@@ -8,6 +8,7 @@ require_relative "../project_environment"
|
|
|
8
8
|
require_relative "../scope"
|
|
9
9
|
require_relative "../reflection"
|
|
10
10
|
require_relative "../type"
|
|
11
|
+
require_relative "../inference/reflective_ivar_write"
|
|
11
12
|
require_relative "../source/constant_path"
|
|
12
13
|
require_relative "../source/literals"
|
|
13
14
|
require_relative "../source/node_children"
|
|
@@ -25,6 +26,7 @@ require_relative "inline_declarations"
|
|
|
25
26
|
require_relative "layout_index"
|
|
26
27
|
require_relative "method_candidate"
|
|
27
28
|
require_relative "writer"
|
|
29
|
+
require_relative "../rooted_glob"
|
|
28
30
|
|
|
29
31
|
module Rigor
|
|
30
32
|
module SigGen
|
|
@@ -98,6 +100,7 @@ module Rigor
|
|
|
98
100
|
# (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
|
|
99
101
|
@namespace_kinds = {}
|
|
100
102
|
@module_function_methods = Set.new
|
|
103
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
101
104
|
@class_shells = Set.new
|
|
102
105
|
@class_superclasses = {}
|
|
103
106
|
@meta_layouts = {}
|
|
@@ -446,7 +449,7 @@ module Rigor
|
|
|
446
449
|
def resolve_paths(args)
|
|
447
450
|
args.flat_map do |arg|
|
|
448
451
|
if File.directory?(arg)
|
|
449
|
-
|
|
452
|
+
RootedGlob.glob(arg, "**/*.rb")
|
|
450
453
|
elsif File.file?(arg) && arg.end_with?(".rb")
|
|
451
454
|
[arg]
|
|
452
455
|
else
|
|
@@ -463,10 +466,7 @@ module Rigor
|
|
|
463
466
|
base_scope = Scope.empty(environment: environment)
|
|
464
467
|
scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
|
|
465
468
|
|
|
466
|
-
|
|
467
|
-
@module_function_methods = Set.new
|
|
468
|
-
@class_shells = Set.new
|
|
469
|
-
@class_superclasses = {}
|
|
469
|
+
reset_file_maps
|
|
470
470
|
@meta_layouts = collect_meta_layouts(scope_index)
|
|
471
471
|
register_meta_classes
|
|
472
472
|
defs = collect_method_definitions(parse_result.value, scope_index)
|
|
@@ -488,6 +488,15 @@ module Rigor
|
|
|
488
488
|
collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
489
489
|
end
|
|
490
490
|
|
|
491
|
+
# The per-file scratch state {#analyse_file} starts each file with.
|
|
492
|
+
def reset_file_maps
|
|
493
|
+
@namespace_kinds = {}
|
|
494
|
+
@module_function_methods = Set.new
|
|
495
|
+
@uncertain_module_function_defs = Set.new.compare_by_identity
|
|
496
|
+
@class_shells = Set.new
|
|
497
|
+
@class_superclasses = {}
|
|
498
|
+
end
|
|
499
|
+
|
|
491
500
|
# Walks the AST collecting `(def_node, class_name, kind)` tuples for every `def` Rigor can re-type. Slice 1
|
|
492
501
|
# covered instance `def foo` methods inside a nameable `class` / `module` body. Slice 4 extends this to
|
|
493
502
|
# singleton-side methods via `def self.foo` and `class << self; def foo; end`. A plain top-level `def` is
|
|
@@ -732,6 +741,8 @@ module Rigor
|
|
|
732
741
|
class_name = prefix.join("::")
|
|
733
742
|
if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
|
|
734
743
|
@module_function_methods << [class_name, node.name]
|
|
744
|
+
elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
|
|
745
|
+
@uncertain_module_function_defs << node
|
|
735
746
|
end
|
|
736
747
|
out << [node, class_name, kind]
|
|
737
748
|
end
|
|
@@ -783,9 +794,13 @@ module Rigor
|
|
|
783
794
|
# private methods are implementation details, not part of the type contract downstream consumers (Steep,
|
|
784
795
|
# IDE, gem users) read. The default now skips private and protected methods; the `:include_private` flag
|
|
785
796
|
# restores the slice-4 behaviour for callers that want every method.
|
|
797
|
+
#
|
|
798
|
+
# A module function renders as `def self?.name`, which RBS reads as a public singleton method and a private
|
|
799
|
+
# instance method, so a `private` written before the `module_function` toggle does not exclude it (#1968).
|
|
786
800
|
def visibility_excludes?(def_node, class_name, kind, scope_index)
|
|
787
801
|
return false if kind == :singleton
|
|
788
802
|
return false if @include_private
|
|
803
|
+
return false if @module_function_methods.include?([class_name, def_node.name])
|
|
789
804
|
|
|
790
805
|
scope = scope_index[def_node] || scope_index.each_value.first
|
|
791
806
|
return false if scope.nil?
|
|
@@ -960,6 +975,9 @@ module Rigor
|
|
|
960
975
|
# #1676 — a top-level `def` is collected with no class; it is reported, never typed.
|
|
961
976
|
def classify_collected_def(path, def_node, class_name, kind, scope_index)
|
|
962
977
|
return skipped(path, def_node, class_name, kind, :top_level_def) if class_name.nil?
|
|
978
|
+
if @uncertain_module_function_defs.include?(def_node)
|
|
979
|
+
return skipped(path, def_node, class_name, kind, :uncertain_module_function)
|
|
980
|
+
end
|
|
963
981
|
|
|
964
982
|
classify_def(path, def_node, class_name, kind, scope_index)
|
|
965
983
|
end
|
|
@@ -1421,12 +1439,17 @@ module Rigor
|
|
|
1421
1439
|
# same position a method with no declaration at all is in. Over a declaration the author wrote, though, a
|
|
1422
1440
|
# VALUE literal the body proves (`-> 1`, `-> "x"`, `-> :k`) is an implementation detail the `untyped` was
|
|
1423
1441
|
# abstracting over, so it is widened to its class (`1 | 2` to `Integer`, `"S" | nil` to `(String | nil)`; #1489).
|
|
1424
|
-
# `
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1442
|
+
# `true` and `false` widen to `bool` (#1891): a predicate's base `-> true` made every subclass override
|
|
1443
|
+
# returning `false` a `def.override-return-widened`. A return that is only `nil` has no such widening — it is
|
|
1444
|
+
# the shape of a template method a subclass fills in — so it is declined and the `untyped` stays. `nil` in a
|
|
1445
|
+
# union (`T?`) and a computed `Float` are honest types and stay. An `untyped` that is only the inline reader's
|
|
1446
|
+
# synthesis ({#synthesized_declaration?}) is no word of the author's and keeps the strictest carrier.
|
|
1447
|
+
# `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag says a declaration
|
|
1448
|
+
# existed.
|
|
1428
1449
|
def declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
|
|
1429
1450
|
unless synthesized_declaration?(method_def)
|
|
1451
|
+
return equivalent(path, def_node, class_name, kind, inferred, "untyped") if bare_nil?(inferred)
|
|
1452
|
+
|
|
1430
1453
|
widened = widen_value_literals(inferred)
|
|
1431
1454
|
unless widened.equal?(inferred)
|
|
1432
1455
|
# A `rendered` line was spelled from the unwidened type; there is nothing to rebuild it from.
|
|
@@ -1446,6 +1469,7 @@ module Rigor
|
|
|
1446
1469
|
def widen_value_literals(type)
|
|
1447
1470
|
case type
|
|
1448
1471
|
when Type::Constant
|
|
1472
|
+
return widen_boolean_literal if [true, false].include?(type.value)
|
|
1449
1473
|
return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
|
|
1450
1474
|
|
|
1451
1475
|
Type::Combinator.nominal_of(type.value.class.name)
|
|
@@ -1457,6 +1481,14 @@ module Rigor
|
|
|
1457
1481
|
end
|
|
1458
1482
|
end
|
|
1459
1483
|
|
|
1484
|
+
def widen_boolean_literal
|
|
1485
|
+
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
def bare_nil?(type)
|
|
1489
|
+
type.is_a?(Type::Constant) && type.value.nil?
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1460
1492
|
# A declaration only the inline reader's synthesis (a `virtual:` buffer) supplies — ADR-93 gives every `def` an
|
|
1461
1493
|
# `untyped` member, annotated or not — is no word of the author's about the return, so it keeps the proposal
|
|
1462
1494
|
# a method with no declaration at all gets: the strictest carrier the body proves.
|
|
@@ -1590,11 +1622,13 @@ module Rigor
|
|
|
1590
1622
|
end
|
|
1591
1623
|
|
|
1592
1624
|
# `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
|
|
1593
|
-
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as
|
|
1594
|
-
#
|
|
1595
|
-
#
|
|
1625
|
+
# `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
|
|
1626
|
+
# which of the two branches it took keeps the list of literal-spellable classes from forking a second
|
|
1627
|
+
# copy into this file. A Range's nominal carries its element type (`Range[Integer]`, #1891), so it is
|
|
1628
|
+
# excluded by class rather than by comparing the erasure with the class name: like a Float it is a
|
|
1629
|
+
# nominal, not a pinned literal.
|
|
1596
1630
|
def rbs_literal?(type)
|
|
1597
|
-
type.is_a?(Type::Constant) && type.erase_to_rbs != type.value.class.name
|
|
1631
|
+
type.is_a?(Type::Constant) && !type.value.is_a?(Range) && type.erase_to_rbs != type.value.class.name
|
|
1598
1632
|
end
|
|
1599
1633
|
|
|
1600
1634
|
# Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
|
|
@@ -1837,7 +1871,8 @@ module Rigor
|
|
|
1837
1871
|
# pre-pass resolved the type to `nil` or `Dynamic[top]` — typically because `@ivar = param` inside
|
|
1838
1872
|
# `initialize` typed the param as `untyped` — the observation-derived type is substituted. This lets
|
|
1839
1873
|
# `attr_reader :name` emit a concrete type when `ClassName.new("alice")` call sites are visible to the
|
|
1840
|
-
# observation scan.
|
|
1874
|
+
# observation scan. {#build_observed_ivar_map} leaves out an ivar anything else writes, so a writer-backed
|
|
1875
|
+
# field is skipped (issue #1410).
|
|
1841
1876
|
def ivar_type_lookup(scope_index, class_name, obs_ivar_map = {})
|
|
1842
1877
|
any_scope = scope_index.each_value.first
|
|
1843
1878
|
return ->(_) {} if any_scope.nil?
|
|
@@ -1854,12 +1889,107 @@ module Rigor
|
|
|
1854
1889
|
# ivars assigned directly from `def initialize` parameters. Only populated when `@observations` is
|
|
1855
1890
|
# non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
|
|
1856
1891
|
# `param_name` is a required / optional positional or keyword parameter of `initialize`.
|
|
1892
|
+
#
|
|
1893
|
+
# Issue #1410: the observed arguments describe the field only while those `@ivar = param_name` writes are
|
|
1894
|
+
# all that writes it. An `attr_writer` / `attr_accessor`, a write in another instance method or block, or
|
|
1895
|
+
# any other write in `initialize` lets the field hold values the observation never saw, so such an ivar is
|
|
1896
|
+
# left out and its accessor is skipped as `untyped_return`, as a hand-written setter is.
|
|
1857
1897
|
def build_observed_ivar_map(root)
|
|
1858
1898
|
return {} if @observations.empty?
|
|
1859
1899
|
|
|
1860
1900
|
result = {}
|
|
1861
1901
|
collect_init_ivar_obs(root, [], result)
|
|
1862
|
-
|
|
1902
|
+
other_writes = Hash.new { |h, k| h[k] = Set.new }
|
|
1903
|
+
collect_other_ivar_writes(root, [], false, other_writes)
|
|
1904
|
+
result.to_h do |class_name, map|
|
|
1905
|
+
others = other_writes[class_name]
|
|
1906
|
+
[class_name, others.include?(ANY_IVAR) ? {} : map.reject { |attr_name, _| others.include?(:"@#{attr_name}") }]
|
|
1907
|
+
end
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
IVAR_WRITE_NODES = [
|
|
1911
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
1912
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
1913
|
+
].freeze
|
|
1914
|
+
ANY_IVAR = :*
|
|
1915
|
+
private_constant :IVAR_WRITE_NODES, :ANY_IVAR
|
|
1916
|
+
|
|
1917
|
+
# Records into `other_writes[class_name]` every instance ivar a class writes other than through an
|
|
1918
|
+
# `initialize`'s `@ivar = param_name`: the ivars an implicit-self `attr_writer` / `attr_accessor` names,
|
|
1919
|
+
# and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
|
|
1920
|
+
# ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
|
|
1921
|
+
# object's ivars and do not count.
|
|
1922
|
+
def collect_other_ivar_writes(node, prefix, in_block, other_writes)
|
|
1923
|
+
return unless node.is_a?(Prism::Node)
|
|
1924
|
+
|
|
1925
|
+
case node
|
|
1926
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
1927
|
+
child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
|
|
1928
|
+
return collect_other_ivar_writes(node.body, child_prefix, false, other_writes) if child_prefix
|
|
1929
|
+
when Prism::SingletonClassNode then return
|
|
1930
|
+
when Prism::DefNode then return record_def_ivar_writes(node, prefix, other_writes)
|
|
1931
|
+
when Prism::BlockNode, Prism::LambdaNode then in_block = true
|
|
1932
|
+
end
|
|
1933
|
+
record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
|
|
1934
|
+
|
|
1935
|
+
node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes) }
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
# The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
|
|
1939
|
+
def record_class_level_write(node, prefix, in_block, other_writes)
|
|
1940
|
+
writes = other_writes[prefix.join("::")]
|
|
1941
|
+
attr_writer_names(node).each { |name| writes << :"@#{name}" }
|
|
1942
|
+
name = written_ivar(node) if in_block
|
|
1943
|
+
writes << name if name
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
ATTR_WRITER_DECLARATIONS = %i[attr_writer attr_accessor].freeze
|
|
1947
|
+
SEND_NAMES = %i[send public_send __send__].freeze
|
|
1948
|
+
private_constant :ATTR_WRITER_DECLARATIONS, :SEND_NAMES
|
|
1949
|
+
|
|
1950
|
+
# The literal names `attr_writer :x` / `attr_accessor :x` declares when called on `self`, implicitly or as
|
|
1951
|
+
# `self.`, or through `send` / `public_send` / `__send__` with the declaration's name as a literal first
|
|
1952
|
+
# argument (`send(:attr_writer, :x)`). Any other call declares none this check sees.
|
|
1953
|
+
def attr_writer_names(node)
|
|
1954
|
+
return [] unless node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
1955
|
+
|
|
1956
|
+
names = Source::Literals.symbol_arguments(node)
|
|
1957
|
+
return names if ATTR_WRITER_DECLARATIONS.include?(node.name)
|
|
1958
|
+
return [] unless SEND_NAMES.include?(node.name)
|
|
1959
|
+
|
|
1960
|
+
ATTR_WRITER_DECLARATIONS.include?(Source::Literals.symbol_arg(node, 0)) ? names.drop(1) : []
|
|
1961
|
+
end
|
|
1962
|
+
|
|
1963
|
+
# Every ivar write in an instance `def` counts, except, in `initialize`, the `@ivar = param_name` write
|
|
1964
|
+
# {#scan_ivar_param_assignments} keys the observation on.
|
|
1965
|
+
def record_def_ivar_writes(def_node, prefix, other_writes)
|
|
1966
|
+
return if def_node.body.nil? || def_node.receiver || prefix.empty?
|
|
1967
|
+
|
|
1968
|
+
credited = Set.new.compare_by_identity
|
|
1969
|
+
if def_node.name == :initialize && def_node.parameters
|
|
1970
|
+
params = build_init_param_index(def_node.parameters).keys.to_set
|
|
1971
|
+
scan_ivar_param_assignments(def_node.body, params, {}, credited)
|
|
1972
|
+
end
|
|
1973
|
+
each_node(def_node.body) do |node|
|
|
1974
|
+
name = written_ivar(node)
|
|
1975
|
+
other_writes[prefix.join("::")] << name if name && !credited.include?(node)
|
|
1976
|
+
end
|
|
1977
|
+
end
|
|
1978
|
+
|
|
1979
|
+
# The ivar `node` writes: a write node's name, an `instance_variable_set` on `self`'s literal name, or
|
|
1980
|
+
# `ANY_IVAR` for one whose name the analysis cannot read ({Inference::ReflectiveIvarWrite}).
|
|
1981
|
+
def written_ivar(node)
|
|
1982
|
+
return node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
1983
|
+
return nil unless Inference::ReflectiveIvarWrite.self_write?(node)
|
|
1984
|
+
|
|
1985
|
+
Inference::ReflectiveIvarWrite.literal_name(node) || ANY_IVAR
|
|
1986
|
+
end
|
|
1987
|
+
|
|
1988
|
+
def each_node(node, &)
|
|
1989
|
+
return unless node.is_a?(Prism::Node)
|
|
1990
|
+
|
|
1991
|
+
yield node
|
|
1992
|
+
node.rigor_each_child { |child| each_node(child, &) }
|
|
1863
1993
|
end
|
|
1864
1994
|
|
|
1865
1995
|
def collect_init_ivar_obs(node, prefix, result)
|
|
@@ -1945,21 +2075,36 @@ module Rigor
|
|
|
1945
2075
|
|
|
1946
2076
|
# Walk a def body for direct `@ivar = local_var` assignments where `local_var` is one of the listed
|
|
1947
2077
|
# parameter names. Records ivar_name (Symbol with `@` prefix) → param_name. Does not recurse into nested
|
|
1948
|
-
# defs / classes / modules.
|
|
1949
|
-
|
|
2078
|
+
# defs / classes / modules. A read counts only when it reaches the method's own parameter: its
|
|
2079
|
+
# `LocalVariableReadNode#depth` equals the number of blocks / lambdas between it and the method, so a
|
|
2080
|
+
# block parameter of the same name (`each { |count| @count = count }`) is not the constructor argument
|
|
2081
|
+
# (#1410). `credited`, when given, collects every write node the result is keyed on.
|
|
2082
|
+
def scan_ivar_param_assignments(node, param_names, result, credited = nil, scopes = 0)
|
|
1950
2083
|
return unless node.is_a?(Prism::Node)
|
|
1951
2084
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
result[
|
|
2085
|
+
ivar, param = param_assignment(node, param_names, scopes)
|
|
2086
|
+
if ivar
|
|
2087
|
+
result[ivar] ||= param
|
|
2088
|
+
credited << node if credited && result[ivar] == param
|
|
1956
2089
|
end
|
|
1957
2090
|
|
|
1958
2091
|
return if node.is_a?(Prism::DefNode) ||
|
|
1959
2092
|
node.is_a?(Prism::ClassNode) ||
|
|
1960
2093
|
node.is_a?(Prism::ModuleNode)
|
|
1961
2094
|
|
|
1962
|
-
|
|
2095
|
+
scopes += 1 if node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
|
|
2096
|
+
node.rigor_each_child { |c| scan_ivar_param_assignments(c, param_names, result, credited, scopes) }
|
|
2097
|
+
end
|
|
2098
|
+
|
|
2099
|
+
# `[ivar, param]` for `@ivar = param` whose read reaches the method's own parameter `param`, else nil.
|
|
2100
|
+
def param_assignment(node, param_names, scopes)
|
|
2101
|
+
return nil unless node.is_a?(Prism::InstanceVariableWriteNode)
|
|
2102
|
+
|
|
2103
|
+
read = node.value
|
|
2104
|
+
return nil unless read.is_a?(Prism::LocalVariableReadNode) && read.depth == scopes
|
|
2105
|
+
return nil unless param_names.include?(read.name)
|
|
2106
|
+
|
|
2107
|
+
[node.name, read.name]
|
|
1963
2108
|
end
|
|
1964
2109
|
|
|
1965
2110
|
def build_attr_candidates(call_name, class_name, attr_name, ivar_type, ctx)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module SigGen
|
|
@@ -27,12 +28,14 @@ module Rigor
|
|
|
27
28
|
class LayoutIndex
|
|
28
29
|
# The `[method_name, kind]` keys a member declaration answers for, `kind` being `:instance` or
|
|
29
30
|
# `:singleton`. A `def self?.f` answers for both kinds; an `attr_accessor` answers for its reader and its
|
|
30
|
-
# writer
|
|
31
|
+
# writer; an `alias new old` (`alias self.new self.old`) answers for its new name only, under its own kind
|
|
32
|
+
# (#1975). Shared with {Writer}, which asks the same question of the target file it is merging into.
|
|
31
33
|
def self.member_keys(member)
|
|
32
34
|
case member
|
|
33
35
|
when RBS::AST::Members::MethodDefinition
|
|
34
36
|
kinds = member.kind == :singleton_instance ? %i[instance singleton] : [member.kind]
|
|
35
37
|
kinds.map { |kind| [member.name, kind] }
|
|
38
|
+
when RBS::AST::Members::Alias then [[member.new_name, member.kind]]
|
|
36
39
|
when RBS::AST::Members::AttrReader then [[member.name, member.kind]]
|
|
37
40
|
when RBS::AST::Members::AttrWriter then [[:"#{member.name}=", member.kind]]
|
|
38
41
|
when RBS::AST::Members::AttrAccessor then [[member.name, member.kind], [:"#{member.name}=", member.kind]]
|
|
@@ -92,7 +95,7 @@ module Rigor
|
|
|
92
95
|
base = Pathname(dir)
|
|
93
96
|
next unless base.directory?
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
RootedGlob.glob(base, "**/*.rbs").each do |rbs_file|
|
|
96
99
|
index_file(Pathname(rbs_file), accumulator)
|
|
97
100
|
end
|
|
98
101
|
end
|