rigortype 0.2.8 → 0.3.0
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 +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../reflection"
|
|
6
|
+
require_relative "../source/constant_path"
|
|
6
7
|
require_relative "../type"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../rbs_extended"
|
|
@@ -41,7 +42,13 @@ module Rigor
|
|
|
41
42
|
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
42
43
|
SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
|
|
43
44
|
ClassNarrowingContext = Data.define(:exact, :polarity, :environment)
|
|
44
|
-
|
|
45
|
+
# A recognised `=~` regex pattern operand: the pattern `source` string and whether it was
|
|
46
|
+
# compiled in extended (`//x`) mode. Extraction from a literal `RegularExpressionNode` and from
|
|
47
|
+
# a value-pinned `Constant[Regexp]` share this carrier so the participation walk and the
|
|
48
|
+
# extended-mode bail read one shape.
|
|
49
|
+
RegexMatchPattern = Data.define(:source, :extended)
|
|
50
|
+
private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext,
|
|
51
|
+
:RegexMatchPattern
|
|
45
52
|
|
|
46
53
|
module_function
|
|
47
54
|
|
|
@@ -873,6 +880,15 @@ module Rigor
|
|
|
873
880
|
string_predicate_result = analyse_string_predicate(node, scope)
|
|
874
881
|
return apply_safe_nav_non_nil(node, scope, string_predicate_result) if string_predicate_result
|
|
875
882
|
|
|
883
|
+
# A predicate whose RBS says it always returns `false` for one union arm cannot have
|
|
884
|
+
# been called on that arm when it answered truthily. `NilClass#present?: () -> false`
|
|
885
|
+
# (ActiveSupport) is the motivating case: `if login.present?` must narrow `String | nil`
|
|
886
|
+
# to `String`, and nothing else in the catalogue does it — `present?` is a gem method the
|
|
887
|
+
# engine must not hardcode, and `rigor:v1:predicate-if-true` facts never reach a union
|
|
888
|
+
# receiver.
|
|
889
|
+
polarity_result = analyse_union_predicate_polarity(node, scope)
|
|
890
|
+
return polarity_result if polarity_result
|
|
891
|
+
|
|
876
892
|
# A safe-navigation call (`v&.foo`) whose result is truthy proves the receiver was
|
|
877
893
|
# non-nil — `&.` returns `nil` when the receiver is nil, so a truthy outcome can
|
|
878
894
|
# only come from a non-nil receiver. Narrow the receiver on the truthy edge even
|
|
@@ -953,8 +969,8 @@ module Rigor
|
|
|
953
969
|
end
|
|
954
970
|
|
|
955
971
|
def simple_dispatch_name?(name)
|
|
956
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ key? has_key? empty? any?
|
|
957
|
-
respond_to?].include?(name)
|
|
972
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
973
|
+
none? respond_to?].include?(name)
|
|
958
974
|
end
|
|
959
975
|
|
|
960
976
|
def dispatch_call_simple(node, scope, name)
|
|
@@ -965,6 +981,7 @@ module Rigor
|
|
|
965
981
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
966
982
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
967
983
|
when :=~ then analyse_regex_match_predicate(node, scope)
|
|
984
|
+
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
968
985
|
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
969
986
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
970
987
|
when :respond_to? then analyse_respond_to_predicate(node, scope)
|
|
@@ -1190,30 +1207,123 @@ module Rigor
|
|
|
1190
1207
|
# - Falsey edge (`=~` returned nil — no match): `$~` and every numbered / back-reference
|
|
1191
1208
|
# global bound to `Constant<nil>`.
|
|
1192
1209
|
#
|
|
1193
|
-
# Returns nil (no narrowing) when the receiver / argument pair does not
|
|
1194
|
-
#
|
|
1210
|
+
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1211
|
+
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1195
1212
|
def analyse_regex_match_predicate(node, scope)
|
|
1196
1213
|
return nil if node.arguments.nil?
|
|
1197
1214
|
return nil unless node.arguments.arguments.size == 1
|
|
1198
1215
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1216
|
+
arg = node.arguments.arguments.first
|
|
1217
|
+
pattern = regex_match_pattern(node.receiver, arg, scope)
|
|
1218
|
+
return nil if pattern.nil?
|
|
1219
|
+
# Extended mode (`//x`) lets the pattern carry free whitespace and `#` comments, and a
|
|
1220
|
+
# comment may contain a literal `(` — which the light char-scan walker would miscount as a
|
|
1221
|
+
# capturing group, shifting `$N` indices and narrowing the wrong global. That is a
|
|
1222
|
+
# false-negative-class misnarrowing, so bail rather than risk it. The literal path shares
|
|
1223
|
+
# this bail: `RegularExpressionNode` carries the same latent miscount.
|
|
1224
|
+
return nil if pattern.extended
|
|
1225
|
+
|
|
1226
|
+
unconditional = unconditional_capture_groups(pattern.source)
|
|
1227
|
+
truthy, falsey = regex_match_predicate_scopes(scope, unconditional)
|
|
1228
|
+
# #164 — layer whole-receiver refinement onto the truthy edge without disturbing the
|
|
1229
|
+
# match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
|
|
1230
|
+
# matched, so the string operand narrows to the imported refinement the anchored pattern
|
|
1231
|
+
# names. The falsey edge is untouched: a failed match proves nothing about the shape.
|
|
1232
|
+
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
|
|
1233
|
+
[truthy, falsey]
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
1237
|
+
# proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
|
|
1238
|
+
# single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
|
|
1239
|
+
# receiver local to the matching imported refinement on the truthy edge; the falsey edge
|
|
1240
|
+
# passes through unchanged (failing one pattern proves nothing about the string's shape).
|
|
1241
|
+
# A non-literal / unanchored / `\Z` / line-anchored / `//x` pattern falls through to no
|
|
1242
|
+
# narrowing.
|
|
1243
|
+
def analyse_whole_regex_match_predicate(node, scope)
|
|
1244
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1245
|
+
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
1246
|
+
|
|
1247
|
+
pattern = regex_operand_pattern(node.arguments.arguments.first, scope)
|
|
1248
|
+
return nil unless pattern.is_a?(RegexMatchPattern)
|
|
1249
|
+
return nil if pattern.extended
|
|
1201
1250
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1251
|
+
refinement = ::Rigor::Builtins::RegexRefinement.for_whole_pattern(pattern.source)
|
|
1252
|
+
return nil if refinement.nil?
|
|
1253
|
+
|
|
1254
|
+
local_name = node.receiver.name
|
|
1255
|
+
return nil if scope.local(local_name).nil?
|
|
1256
|
+
|
|
1257
|
+
[scope.with_local(local_name, refinement), scope]
|
|
1204
1258
|
end
|
|
1205
1259
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1260
|
+
# Narrows the string operand of a `=~` predicate to a whole-receiver refinement when the
|
|
1261
|
+
# regex source is a recognised `\A…\z`-anchored pattern. Returns the (possibly unchanged)
|
|
1262
|
+
# truthy scope; the falsey edge is never touched by this layer.
|
|
1263
|
+
def apply_whole_receiver_refinement(truthy, receiver, arg, source, scope)
|
|
1264
|
+
refinement = ::Rigor::Builtins::RegexRefinement.for_whole_pattern(source)
|
|
1265
|
+
return truthy if refinement.nil?
|
|
1209
1266
|
|
|
1210
|
-
|
|
1267
|
+
# `regex_match_pattern` already resolved the regex operand from a literal or a
|
|
1268
|
+
# `Constant[Regexp]` constant read — never a bare local — so the string subject is
|
|
1269
|
+
# unambiguously the local-read operand, if either operand is one.
|
|
1270
|
+
subject = [receiver, arg].find { |operand| operand.is_a?(Prism::LocalVariableReadNode) }
|
|
1271
|
+
return truthy if subject.nil? || scope.local(subject.name).nil?
|
|
1272
|
+
|
|
1273
|
+
truthy.with_local(subject.name, refinement)
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
# Recognises the regex pattern operand of a `=~` predicate. An operand resolves to a regex when
|
|
1277
|
+
# it is either a syntactic `RegularExpressionNode` literal OR a constant read whose type is a
|
|
1278
|
+
# value-pinned `Constant[Regexp]` (`RE = /.../` and `RE = Regexp.new(...)` both fold to the
|
|
1279
|
+
# carrier in the whole-program constant pre-pass). Returns a single {RegexMatchPattern}, or nil
|
|
1280
|
+
# (no narrowing) when NEITHER operand resolves to a regex, when BOTH do (`/a/ =~ /b/` — no
|
|
1281
|
+
# string subject to bind), or when a constant operand is ambiguous — typed as a `Union` from a
|
|
1282
|
+
# twice-assigned constant, where no single source can be pinned.
|
|
1283
|
+
def regex_match_pattern(left, right, scope)
|
|
1284
|
+
left_pattern = regex_operand_pattern(left, scope)
|
|
1285
|
+
right_pattern = regex_operand_pattern(right, scope)
|
|
1286
|
+
return nil if left_pattern == :ambiguous || right_pattern == :ambiguous
|
|
1287
|
+
|
|
1288
|
+
patterns = [left_pattern, right_pattern].grep(RegexMatchPattern)
|
|
1289
|
+
patterns.size == 1 ? patterns.first : nil
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
# Classifies one `=~` operand: a {RegexMatchPattern} when it resolves to a regex (literal or
|
|
1293
|
+
# `Constant[Regexp]`), `:ambiguous` when a constant operand types as a `Union` (a twice-assigned
|
|
1294
|
+
# constant), or nil when the operand is not a regex at all.
|
|
1295
|
+
def regex_operand_pattern(node, scope)
|
|
1296
|
+
case node
|
|
1297
|
+
when Prism::RegularExpressionNode
|
|
1298
|
+
RegexMatchPattern.new(source: node.unescaped, extended: node.extended?)
|
|
1299
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1300
|
+
regex_operand_from_constant(node, scope)
|
|
1301
|
+
end
|
|
1211
1302
|
end
|
|
1212
1303
|
|
|
1213
|
-
#
|
|
1214
|
-
#
|
|
1215
|
-
#
|
|
1216
|
-
|
|
1304
|
+
# Resolves a constant-reference operand to a regex pattern via the shared lexical-constant
|
|
1305
|
+
# resolution (`Reflection.resolve_constant_type`). `Regexp#options` gives the real
|
|
1306
|
+
# extended-mode flag; `Regexp#source` the pattern. Returns `:ambiguous` for a `Union`-typed
|
|
1307
|
+
# constant so the caller bails, and nil when the constant does not resolve to a `Constant[Regexp]`.
|
|
1308
|
+
def regex_operand_from_constant(node, scope)
|
|
1309
|
+
name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
1310
|
+
return nil if name.nil?
|
|
1311
|
+
|
|
1312
|
+
type = Reflection.resolve_constant_type(name, scope: scope)
|
|
1313
|
+
return :ambiguous if type.is_a?(Type::Union)
|
|
1314
|
+
return nil unless type.is_a?(Type::Constant)
|
|
1315
|
+
|
|
1316
|
+
value = type.value
|
|
1317
|
+
return nil unless value.is_a?(Regexp)
|
|
1318
|
+
|
|
1319
|
+
RegexMatchPattern.new(source: value.source, extended: value.options.anybits?(Regexp::EXTENDED))
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
# Curated set of match globals bound non-nil on every successful `=~` regardless of grouping:
|
|
1323
|
+
# `$~` (the MatchData), `$&` (whole match), `` $` `` (pre-match), `$'` (post-match). Numbered
|
|
1324
|
+
# references (`$1..$N`) and `$+` (the last matched group) are handled separately because they
|
|
1325
|
+
# depend on the regex source — `$+` is gated on the same participation set as `$N` (#177).
|
|
1326
|
+
REGEX_MATCH_GLOBALS = %i[$~ $& $` $'].freeze
|
|
1217
1327
|
private_constant :REGEX_MATCH_GLOBALS
|
|
1218
1328
|
|
|
1219
1329
|
# `unconditional` is the Set of 1-based numbered-capture indices whose group is
|
|
@@ -1242,6 +1352,15 @@ module Rigor
|
|
|
1242
1352
|
truthy = truthy.with_global(name, string_t)
|
|
1243
1353
|
falsey = falsey.with_global(name, nil_t)
|
|
1244
1354
|
end
|
|
1355
|
+
# `$+` is the LAST matched group, so it is nil on a successful match whose groups are all
|
|
1356
|
+
# optional (`"b" =~ /(a)?b/` matches yet leaves `$+` nil) or absent (no capture group). It
|
|
1357
|
+
# narrows to `String` only when some group is guaranteed to participate — the same
|
|
1358
|
+
# participation gate `$N` uses (#177). With no unconditional group it is left unbound on both
|
|
1359
|
+
# edges (fall through to the RBS `String?` default), exactly as an optional `$N` is.
|
|
1360
|
+
unless unconditional.empty?
|
|
1361
|
+
truthy = truthy.with_global(:$+, string_t)
|
|
1362
|
+
falsey = falsey.with_global(:$+, nil_t)
|
|
1363
|
+
end
|
|
1245
1364
|
[truthy, falsey]
|
|
1246
1365
|
end
|
|
1247
1366
|
|
|
@@ -2352,6 +2471,115 @@ module Rigor
|
|
|
2352
2471
|
end
|
|
2353
2472
|
end
|
|
2354
2473
|
|
|
2474
|
+
# The three value-pinned classes a union arm can name whose instances are the single
|
|
2475
|
+
# value itself. Their finality is what makes the polarity rule below sound: no subclass
|
|
2476
|
+
# can override the predicate, because there is no subclass.
|
|
2477
|
+
POLARITY_ARM_CLASSES = { nil => "NilClass", true => "TrueClass", false => "FalseClass" }.freeze
|
|
2478
|
+
private_constant :POLARITY_ARM_CLASSES
|
|
2479
|
+
|
|
2480
|
+
# Drops union arms a zero-argument predicate's own signature rules out on each edge.
|
|
2481
|
+
#
|
|
2482
|
+
# `if x.present?` with `x: String | nil` must see `String` in the body. Rigor could not:
|
|
2483
|
+
# `present?` is an ActiveSupport method (so the engine must not join the hardcoded
|
|
2484
|
+
# `nil?` / `empty?` catalogue with it), and `resolve_rbs_extended_method` hands a union
|
|
2485
|
+
# receiver no `rigor:v1:predicate-if-true` facts, so annotating the RBS would not help
|
|
2486
|
+
# either. But the answer is already written down: the bundled ActiveSupport signature
|
|
2487
|
+
# declares `NilClass#present?: () -> false`, and a method that always returns `false` for
|
|
2488
|
+
# `nil` cannot have answered truthily on a `nil` receiver.
|
|
2489
|
+
#
|
|
2490
|
+
# Soundness rests on the arm being a value-pinned `nil` / `true` / `false`. For an
|
|
2491
|
+
# ordinary `Nominal[Foo]` arm the static type admits Foo's subclasses, any of which may
|
|
2492
|
+
# override the predicate and return the other polarity; the three classes here have no
|
|
2493
|
+
# subclasses, so the declared return is the runtime return.
|
|
2494
|
+
#
|
|
2495
|
+
# Safe navigation is excluded: `x&.blank?` yields `nil` (falsey) for a nil receiver
|
|
2496
|
+
# rather than `NilClass#blank?`'s declared `true`, so the falsey edge would wrongly drop
|
|
2497
|
+
# the nil arm. `analyse_safe_nav_receiver` below handles that shape's truthy edge.
|
|
2498
|
+
#
|
|
2499
|
+
# Returns `[truthy_scope, falsey_scope]`, or nil when no arm is ruled out, when an edge
|
|
2500
|
+
# would be emptied, or when the receiver has no scope binding to narrow.
|
|
2501
|
+
def analyse_union_predicate_polarity(node, scope)
|
|
2502
|
+
return nil if node.safe_navigation? || !argument_free?(node)
|
|
2503
|
+
return nil unless node.name.end_with?("?")
|
|
2504
|
+
|
|
2505
|
+
receiver_type = receiver_binding_type(node.receiver, scope)
|
|
2506
|
+
return nil unless receiver_type.is_a?(Type::Union)
|
|
2507
|
+
|
|
2508
|
+
truthy, falsey = partition_arms_by_polarity(receiver_type.members, node.name, scope)
|
|
2509
|
+
return nil if truthy.size == receiver_type.members.size && falsey.size == receiver_type.members.size
|
|
2510
|
+
return nil if truthy.empty? || falsey.empty?
|
|
2511
|
+
|
|
2512
|
+
[
|
|
2513
|
+
narrow_receiver_binding(node.receiver, scope, Type::Combinator.union(*truthy)),
|
|
2514
|
+
narrow_receiver_binding(node.receiver, scope, Type::Combinator.union(*falsey))
|
|
2515
|
+
]
|
|
2516
|
+
end
|
|
2517
|
+
|
|
2518
|
+
# Splits `members` into the arms that survive the truthy edge and those that survive the
|
|
2519
|
+
# falsey edge. An arm whose polarity is unknown survives both.
|
|
2520
|
+
def partition_arms_by_polarity(members, method_name, scope)
|
|
2521
|
+
truthy = []
|
|
2522
|
+
falsey = []
|
|
2523
|
+
members.each do |arm|
|
|
2524
|
+
case arm_predicate_polarity(arm, method_name, scope)
|
|
2525
|
+
when :always_false then falsey << arm
|
|
2526
|
+
when :always_true then truthy << arm
|
|
2527
|
+
else
|
|
2528
|
+
truthy << arm
|
|
2529
|
+
falsey << arm
|
|
2530
|
+
end
|
|
2531
|
+
end
|
|
2532
|
+
[truthy, falsey]
|
|
2533
|
+
end
|
|
2534
|
+
|
|
2535
|
+
# `:always_false` / `:always_true` when every RBS overload of `method_name` on the arm's
|
|
2536
|
+
# class returns that literal; nil for any other arm, signature, or lookup failure.
|
|
2537
|
+
def arm_predicate_polarity(arm, method_name, scope)
|
|
2538
|
+
returns = arm_predicate_return_types(arm, method_name, scope)
|
|
2539
|
+
return nil if returns.nil? || returns.empty?
|
|
2540
|
+
return :always_false if returns.all? { |type| literal_boolean?(type, false) }
|
|
2541
|
+
return :always_true if returns.all? { |type| literal_boolean?(type, true) }
|
|
2542
|
+
|
|
2543
|
+
nil
|
|
2544
|
+
end
|
|
2545
|
+
|
|
2546
|
+
# Declared return type of every RBS overload of `method_name` on the arm's class, or nil
|
|
2547
|
+
# when the arm is not one of the three value-pinned classes or the lookup fails.
|
|
2548
|
+
def arm_predicate_return_types(arm, method_name, scope)
|
|
2549
|
+
return nil unless arm.is_a?(Type::Constant)
|
|
2550
|
+
|
|
2551
|
+
class_name = POLARITY_ARM_CLASSES[arm.value]
|
|
2552
|
+
return nil if class_name.nil?
|
|
2553
|
+
|
|
2554
|
+
definition = Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
2555
|
+
definition&.method_types&.map { |method_type| method_type.type.return_type }
|
|
2556
|
+
rescue StandardError
|
|
2557
|
+
nil
|
|
2558
|
+
end
|
|
2559
|
+
|
|
2560
|
+
def literal_boolean?(rbs_type, value)
|
|
2561
|
+
rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
|
|
2562
|
+
end
|
|
2563
|
+
|
|
2564
|
+
# The four receiver shapes with a scope binding the edges can rebind, mirroring
|
|
2565
|
+
# `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
|
|
2566
|
+
# the caller declines.
|
|
2567
|
+
def receiver_binding_type(receiver, scope)
|
|
2568
|
+
case receiver
|
|
2569
|
+
when Prism::LocalVariableReadNode then scope.local(receiver.name)
|
|
2570
|
+
when Prism::InstanceVariableReadNode then scope.ivar(receiver.name)
|
|
2571
|
+
when Prism::SelfNode then scope.self_type
|
|
2572
|
+
end
|
|
2573
|
+
end
|
|
2574
|
+
|
|
2575
|
+
def narrow_receiver_binding(receiver, scope, narrowed)
|
|
2576
|
+
case receiver
|
|
2577
|
+
when Prism::LocalVariableReadNode then scope.with_local(receiver.name, narrowed)
|
|
2578
|
+
when Prism::InstanceVariableReadNode then scope.with_ivar(receiver.name, narrowed)
|
|
2579
|
+
when Prism::SelfNode then scope.with_self_type(narrowed)
|
|
2580
|
+
end
|
|
2581
|
+
end
|
|
2582
|
+
|
|
2355
2583
|
# Narrows a safe-navigation call's receiver (`v&.foo`) to its non-nil fragment on the
|
|
2356
2584
|
# truthy edge, returning `[truthy, falsey]` or nil when nothing applies (not safe-nav,
|
|
2357
2585
|
# opaque receiver, or already non-nil). Used standalone for a bare `v&.foo` truthy edge
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "scope_indexer"
|
|
6
|
+
require_relative "fork_map"
|
|
6
7
|
require_relative "../source/node_walker"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -120,15 +121,20 @@ module Rigor
|
|
|
120
121
|
# @param target_ruby [String, nil] Prism parse target.
|
|
121
122
|
# @param max_rounds [Integer] the WD5 fixpoint cap (1 = single-level).
|
|
122
123
|
# @return [Hash{[String,Symbol,Symbol] => Hash{Symbol => Rigor::Type}}] frozen.
|
|
123
|
-
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
124
|
-
new(files: files, environment: environment, target_ruby: target_ruby,
|
|
124
|
+
def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
|
|
125
|
+
new(files: files, environment: environment, target_ruby: target_ruby,
|
|
126
|
+
max_rounds: max_rounds, workers: workers).collect
|
|
125
127
|
end
|
|
126
128
|
|
|
127
|
-
def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS)
|
|
129
|
+
def initialize(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
|
|
128
130
|
@files = files
|
|
129
131
|
@environment = environment
|
|
130
132
|
@target_ruby = target_ruby
|
|
131
133
|
@max_rounds = max_rounds
|
|
134
|
+
# P3-10 — fork-parallelism for the per-round re-typing (the dominant coverage-protection cost). A
|
|
135
|
+
# round's per-file typing is independent; contributions merge associatively (see {#merge_round}), so
|
|
136
|
+
# forking over file slices is byte-identical to the sequential pass. 0/1 → sequential.
|
|
137
|
+
@workers = workers
|
|
132
138
|
# Reset per round (see {#run_round}). `[[class, method, kind], param_sym]` => [Type] of
|
|
133
139
|
# observed concrete arguments (a default-block Hash, not a `{}` literal, so the
|
|
134
140
|
# analyzer types its reads generically — {#finalize}), plus the ids widened to
|
|
@@ -139,7 +145,16 @@ module Rigor
|
|
|
139
145
|
|
|
140
146
|
def collect
|
|
141
147
|
parsed = parse_all
|
|
148
|
+
# Force the full RBS load on the parent so forked round-workers copy-on-write inherit a warm
|
|
149
|
+
# environment instead of each rebuilding it (mirrors the check / scan fork pools). A no-op on the
|
|
150
|
+
# sequential path.
|
|
151
|
+
@environment.rbs_loader&.prewarm if ForkMap.parallel?([@workers, parsed.size].min)
|
|
142
152
|
discovery = discovery_seed_tables
|
|
153
|
+
# The union of every method name any discovered `def` declares. A call whose name is absent cannot
|
|
154
|
+
# bind in `resolve_callee`, so {#record_call} short-circuits it before typing its receiver (an
|
|
155
|
+
# on-demand dispatch for an otherwise-unresolved call). ~73% of call sites in a Rails app name a
|
|
156
|
+
# method no user def declares; a pure negative filter, so the resolved table is byte-identical.
|
|
157
|
+
@user_method_names = user_method_names(discovery)
|
|
143
158
|
table = EMPTY
|
|
144
159
|
@max_rounds.times do
|
|
145
160
|
rounded = run_round(parsed, discovery, table)
|
|
@@ -164,18 +179,51 @@ module Rigor
|
|
|
164
179
|
end
|
|
165
180
|
|
|
166
181
|
# One fixpoint round: re-type every file with `seed_table` (the previous round's inferred
|
|
167
|
-
# parameters) seeded, collecting the next round's table.
|
|
182
|
+
# parameters) seeded, collecting the next round's table. The per-file typing is fork-mapped over
|
|
183
|
+
# `@workers` (byte-identical to sequential — see {#merge_round}); each slice returns a marshalable
|
|
184
|
+
# `[observations, poisoned]` contribution the parent merges in slice order.
|
|
168
185
|
def run_round(parsed, discovery_tables, seed_table)
|
|
186
|
+
seed_scope = build_seed_scope(discovery_tables, seed_table)
|
|
187
|
+
contributions = ForkMap.call(items: parsed, workers: @workers) do |slice|
|
|
188
|
+
accumulate_slice(slice, seed_scope)
|
|
189
|
+
end
|
|
190
|
+
merge_round(contributions)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Types every call in one contiguous file slice, returning a marshalable contribution: the per-`id`
|
|
194
|
+
# observed argument types (default proc stripped for `Marshal`) and the poisoned-`id` list. Uses the
|
|
195
|
+
# per-process observation ivars, so a forked worker and the sequential single-slice call share this
|
|
196
|
+
# exact code.
|
|
197
|
+
def accumulate_slice(parsed_slice, seed_scope)
|
|
169
198
|
@type_observations = Hash.new { |hash, id| hash[id] = [] }
|
|
170
199
|
@poisoned_params = Set.new
|
|
171
|
-
|
|
172
|
-
parsed.each do |path, ast|
|
|
200
|
+
parsed_slice.each do |path, ast|
|
|
173
201
|
index = ScopeIndexer.index(ast, default_scope: seed_scope.with_source_path(path))
|
|
174
202
|
Source::NodeWalker.each(ast) do |node|
|
|
175
203
|
record_call(node, index) if node.is_a?(Prism::CallNode)
|
|
176
204
|
end
|
|
177
205
|
end
|
|
178
|
-
|
|
206
|
+
# Copy into a plain Hash, dropping the default proc — a Marshal-unfriendly proc the fork worker
|
|
207
|
+
# would otherwise fail to dump. (`.to_h` returns self here, keeping the proc, so copy explicitly.)
|
|
208
|
+
observations = @type_observations.each_with_object({}) { |(id, types), plain| plain[id] = types }
|
|
209
|
+
[observations, @poisoned_params.to_a]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Merges the per-slice contributions into the round's table. Associative and order-preserving, so the
|
|
213
|
+
# result is identical to a sequential single-slice run: a parameter is poisoned if ANY slice poisoned
|
|
214
|
+
# it, its observations are the file-order concatenation across slices, and the {MAX_CALL_SITE_TYPES}
|
|
215
|
+
# cap re-applies over the merged total (a per-slice sub-cap can undercount, so the parent enforces the
|
|
216
|
+
# real cap — the same "poisoned" outcome the sequential mid-stream cap reaches).
|
|
217
|
+
def merge_round(contributions)
|
|
218
|
+
poisoned = contributions.each_with_object(Set.new) { |(_obs, pois), set| set.merge(pois) }
|
|
219
|
+
observations = {}
|
|
220
|
+
contributions.each do |(obs, _pois)|
|
|
221
|
+
obs.each do |id, types|
|
|
222
|
+
(observations[id] ||= []).concat(types) unless poisoned.include?(id)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
observations.each { |id, types| poisoned << id if types.length > MAX_CALL_SITE_TYPES }
|
|
226
|
+
finalize(observations, poisoned)
|
|
179
227
|
end
|
|
180
228
|
|
|
181
229
|
# A scope carrying the cross-file discovery index (so `Foo.new` receivers and
|
|
@@ -221,7 +269,17 @@ module Rigor
|
|
|
221
269
|
}.freeze
|
|
222
270
|
private_constant :DISCOVERY_FIELD
|
|
223
271
|
|
|
272
|
+
# Built once over the whole project and inherited by the fork workers via copy-on-write; a frozen Set
|
|
273
|
+
# for O(1) membership in the {#record_call} hot loop.
|
|
274
|
+
def user_method_names(discovery)
|
|
275
|
+
tables = [discovery[:discovered_def_nodes], discovery[:discovered_singleton_def_nodes]].compact
|
|
276
|
+
tables.flat_map { |table| table.values.flat_map(&:keys) }.to_set.freeze
|
|
277
|
+
end
|
|
278
|
+
|
|
224
279
|
def record_call(call_node, index)
|
|
280
|
+
# See {#collect}: a name no discovered def declares cannot resolve, so skip without typing anything.
|
|
281
|
+
return unless @user_method_names.include?(call_node.name)
|
|
282
|
+
|
|
225
283
|
args = positional_args(call_node)
|
|
226
284
|
return if args.nil?
|
|
227
285
|
|
|
@@ -343,13 +401,15 @@ module Rigor
|
|
|
343
401
|
@type_observations.delete(id)
|
|
344
402
|
end
|
|
345
403
|
|
|
346
|
-
|
|
404
|
+
# Builds the round's frozen `[class, method, kind] => {param => Type}` table from the merged
|
|
405
|
+
# observations and poisoned set.
|
|
406
|
+
def finalize(merged_observations, poisoned)
|
|
347
407
|
# `result` is a default-block Hash (not a `{}` literal) so the analyzer types its reads
|
|
348
408
|
# generically rather than folding the empty shape — the nesting writes stay plain
|
|
349
409
|
# assignments, no literal-fold conditions.
|
|
350
410
|
result = Hash.new { |hash, key| hash[key] = {} }
|
|
351
|
-
|
|
352
|
-
next if
|
|
411
|
+
merged_observations.each do |id, observations|
|
|
412
|
+
next if poisoned.include?(id)
|
|
353
413
|
next if observations.empty?
|
|
354
414
|
|
|
355
415
|
union = Type::Combinator.union(*observations)
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "project_patched_methods"
|
|
6
6
|
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
7
|
require_relative "../source/constant_path"
|
|
8
|
+
require_relative "../source/node_children"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -141,7 +142,7 @@ module Rigor
|
|
|
141
142
|
private_class_method :walk_node
|
|
142
143
|
|
|
143
144
|
def walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
144
|
-
node.
|
|
145
|
+
node.rigor_each_child do |child|
|
|
145
146
|
walk_node(child, qualified_prefix, in_singleton_class, source_path, entries)
|
|
146
147
|
end
|
|
147
148
|
end
|
|
@@ -48,7 +48,14 @@ module Rigor
|
|
|
48
48
|
RBS::Types::Bases::Self => :translate_self,
|
|
49
49
|
RBS::Types::Bases::Instance => :translate_instance,
|
|
50
50
|
RBS::Types::Bases::Class => :translate_untyped,
|
|
51
|
-
RBS
|
|
51
|
+
# `void` is the top type, not `untyped`: RBS defines the two as the same type ("They are all
|
|
52
|
+
# equivalent for the type system; they are all *top type*" — rbs `docs/syntax.md`), with `void`
|
|
53
|
+
# carrying only a hint that the value should not be used. Mapping it to `untyped` made Rigor looser
|
|
54
|
+
# than the toolchain it reads — `Dynamic[top]` is consistent with everything at a gradual boundary,
|
|
55
|
+
# where `top` demands proof — so a caller could silently depend on a return whose author declared
|
|
56
|
+
# "don't rely on this". See ADR-92 WD2; `special-types.md` § `void` (a distinct carrier + a value-use
|
|
57
|
+
# diagnostic) remains unimplemented and marked.
|
|
58
|
+
RBS::Types::Bases::Void => :translate_top,
|
|
52
59
|
RBS::Types::Optional => :translate_optional,
|
|
53
60
|
RBS::Types::Union => :translate_union,
|
|
54
61
|
RBS::Types::Literal => :translate_literal,
|