rigortype 0.4.0 → 0.4.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 +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
|
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
|
|
|
10
10
|
require_relative "../analysis/fact_store"
|
|
11
11
|
require_relative "../builtins/regex_refinement"
|
|
12
12
|
require_relative "guard_rebinding"
|
|
13
|
+
require_relative "key_presence_guard"
|
|
13
14
|
require_relative "last_line"
|
|
14
15
|
require_relative "operand_effects"
|
|
15
16
|
require_relative "optimistic_origin"
|
|
@@ -440,6 +441,20 @@ module Rigor
|
|
|
440
441
|
Source::ConstantPath.rooted?(node) ? "::#{name}" : name
|
|
441
442
|
end
|
|
442
443
|
|
|
444
|
+
# Issue #1446 — the classes a class guard narrowed the receiver `node` to where the guard left it `bot`
|
|
445
|
+
# ({Scope#bot_guard_classes_for}), or nil: a node that is not a local, instance-variable, global or constant
|
|
446
|
+
# read, or one no such guard narrowed.
|
|
447
|
+
def bot_guard_classes_of(node, scope)
|
|
448
|
+
case node
|
|
449
|
+
when Prism::LocalVariableReadNode then scope.bot_guard_classes_for(:local, node.name)
|
|
450
|
+
when Prism::InstanceVariableReadNode then scope.bot_guard_classes_for(:ivar, node.name)
|
|
451
|
+
when Prism::GlobalVariableReadNode then scope.bot_guard_classes_for(:global, node.name)
|
|
452
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
453
|
+
key = constant_key(node)
|
|
454
|
+
key && scope.bot_guard_classes_for(:constant, key)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
443
458
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
444
459
|
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
445
460
|
def predicate_scopes(node, scope)
|
|
@@ -504,15 +519,20 @@ module Rigor
|
|
|
504
519
|
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
505
520
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
506
521
|
|
|
507
|
-
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does
|
|
522
|
+
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
523
|
+
# does an instance variable (#1446).
|
|
508
524
|
slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
509
525
|
return [body_scope, scope] if slot.nil?
|
|
510
526
|
|
|
511
527
|
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
512
|
-
|
|
528
|
+
classes = conditions.map do |condition|
|
|
529
|
+
lexical_class_name(condition, scope) || case_equality_target_class(condition)
|
|
530
|
+
end
|
|
531
|
+
body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
|
|
532
|
+
[body_scope, narrow_guarded_slot(scope, slot, falsey_type)]
|
|
513
533
|
end
|
|
514
534
|
|
|
515
|
-
CASE_SUBJECT_KINDS = %i[local global constant].freeze
|
|
535
|
+
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
516
536
|
private_constant :CASE_SUBJECT_KINDS
|
|
517
537
|
|
|
518
538
|
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
@@ -979,8 +999,9 @@ module Rigor
|
|
|
979
999
|
end
|
|
980
1000
|
|
|
981
1001
|
GLOBAL_AND_CONSTANT = %i[global constant].freeze
|
|
1002
|
+
CLASS_GUARD_SLOT_KINDS = %i[ivar global constant].freeze
|
|
982
1003
|
ALL_SLOT_KINDS = %i[local ivar global constant].freeze
|
|
983
|
-
private_constant :GLOBAL_AND_CONSTANT, :ALL_SLOT_KINDS
|
|
1004
|
+
private_constant :GLOBAL_AND_CONSTANT, :CLASS_GUARD_SLOT_KINDS, :ALL_SLOT_KINDS
|
|
984
1005
|
|
|
985
1006
|
# The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
|
|
986
1007
|
# global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
|
|
@@ -1029,6 +1050,25 @@ module Rigor
|
|
|
1029
1050
|
end
|
|
1030
1051
|
end
|
|
1031
1052
|
|
|
1053
|
+
# Issue #1446 — {#narrow_slot} for a class guard: an instance variable narrows with its pre-guard type
|
|
1054
|
+
# recorded, as a global does, since code that may reach `self` may rebind it. The other guards on an instance
|
|
1055
|
+
# variable keep {#narrow_slot}'s unrecorded binding.
|
|
1056
|
+
def narrow_guarded_slot(scope, slot, type)
|
|
1057
|
+
return narrow_slot(scope, slot, type) unless slot.kind == :ivar
|
|
1058
|
+
return scope if type == slot.current
|
|
1059
|
+
|
|
1060
|
+
scope.with_guarded_ivar(slot.name, type, slot.current)
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Issue #1446 — `scope`, the guard's truthy edge, with `class_names` recorded for `slot` when the guard left
|
|
1064
|
+
# it `bot` ({Scope#with_bot_guard_classes}): the value that passes it is one of those classes. Nothing is
|
|
1065
|
+
# recorded when a condition names no class.
|
|
1066
|
+
def record_bot_guard(scope, slot, type, class_names)
|
|
1067
|
+
return scope unless type.is_a?(Type::Bot) && !class_names.empty? && class_names.none?(&:nil?)
|
|
1068
|
+
|
|
1069
|
+
scope.with_bot_guard_classes(slot.kind, slot.name, class_names)
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1032
1072
|
def narrow_global_slot(scope, slot, type)
|
|
1033
1073
|
return scope if type == slot.current
|
|
1034
1074
|
|
|
@@ -1234,7 +1274,7 @@ module Rigor
|
|
|
1234
1274
|
end
|
|
1235
1275
|
|
|
1236
1276
|
def simple_dispatch_name?(name)
|
|
1237
|
-
%i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
|
|
1277
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? include? member? empty? any?
|
|
1238
1278
|
none? respond_to? nan? finite?].include?(name)
|
|
1239
1279
|
end
|
|
1240
1280
|
|
|
@@ -1245,15 +1285,19 @@ module Rigor
|
|
|
1245
1285
|
when :instance_of? then analyse_class_predicate(node, scope, exact: true)
|
|
1246
1286
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
1247
1287
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
1248
|
-
when
|
|
1288
|
+
when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
|
|
1249
1289
|
when :match? then analyse_whole_regex_match_predicate(node, scope)
|
|
1250
|
-
when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
|
|
1290
|
+
when :key?, :has_key?, :include?, :member? then analyse_key_presence_predicate(node, scope)
|
|
1251
1291
|
when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
|
|
1252
1292
|
when :respond_to? then analyse_respond_to_predicate(node, scope)
|
|
1253
1293
|
when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
|
|
1254
1294
|
end
|
|
1255
1295
|
end
|
|
1256
1296
|
|
|
1297
|
+
def dispatch_regex_match_predicate(node, scope, name)
|
|
1298
|
+
name == :=~ ? analyse_regex_match_predicate(node, scope) : analyse_regex_not_match_predicate(node, scope)
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1257
1301
|
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
|
|
1258
1302
|
# non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
|
|
1259
1303
|
# define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
|
|
@@ -1419,14 +1463,16 @@ module Rigor
|
|
|
1419
1463
|
# `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
|
|
1420
1464
|
# conservative no-op. Only literal `Symbol`/`String` arguments and
|
|
1421
1465
|
# `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
|
|
1422
|
-
# (Dynamic, `Nominal[Hash]`, method-chain receivers
|
|
1423
|
-
#
|
|
1466
|
+
# (Dynamic, `Nominal[Hash]`, method-chain receivers) bails to no narrowing. A non-literal
|
|
1467
|
+
# key goes to {#analyse_computed_key_presence_predicate} (issue #1703).
|
|
1424
1468
|
def analyse_key_presence_predicate(node, scope)
|
|
1425
1469
|
return nil if node.arguments.nil?
|
|
1426
1470
|
return nil unless node.arguments.arguments.size == 1
|
|
1427
1471
|
|
|
1428
|
-
key
|
|
1429
|
-
|
|
1472
|
+
# A literal key promotes a shape's optional key only for `key?` / `has_key?`; `include?` / `member?` reach
|
|
1473
|
+
# the computed-key guard alone, so a String or Array receiver's `include?` keeps its own narrowings.
|
|
1474
|
+
key = static_hash_key(node.arguments.arguments.first) if %i[key? has_key?].include?(node.name)
|
|
1475
|
+
return analyse_computed_key_presence_predicate(node, scope) if key.nil?
|
|
1430
1476
|
|
|
1431
1477
|
case node.receiver
|
|
1432
1478
|
when Prism::LocalVariableReadNode
|
|
@@ -1436,6 +1482,17 @@ module Rigor
|
|
|
1436
1482
|
end
|
|
1437
1483
|
end
|
|
1438
1484
|
|
|
1485
|
+
# Issue #1703 — `H.key?(k)` / `has_key?` / `include?` / `member?` with a non-literal key (a local, an ivar,
|
|
1486
|
+
# or a reader chain such as `prop.column_type`) on a closed hash shape held in a local, an ivar or a
|
|
1487
|
+
# constant. The truthy edge records the guard, and a later `H[k]` of the same receiver and structurally the
|
|
1488
|
+
# same key drops the miss `nil` ({KeyPresenceGuard}). The falsey edge is the no-op: an
|
|
1489
|
+
# absent computed key says nothing a shape can record. Nil when the guard does not apply, so a collection's
|
|
1490
|
+
# `include?` still reaches the membership and String predicates.
|
|
1491
|
+
def analyse_computed_key_presence_predicate(node, scope)
|
|
1492
|
+
truthy = KeyPresenceGuard.record(node, scope)
|
|
1493
|
+
truthy && [truthy, scope]
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1439
1496
|
def key_presence_scopes(name, key, scope, reader:, writer:)
|
|
1440
1497
|
current = scope.public_send(reader, name)
|
|
1441
1498
|
return nil if current.nil?
|
|
@@ -1532,7 +1589,9 @@ module Rigor
|
|
|
1532
1589
|
#
|
|
1533
1590
|
# Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
|
|
1534
1591
|
# regex pattern we can count (see {#regex_match_pattern}).
|
|
1535
|
-
|
|
1592
|
+
#
|
|
1593
|
+
# `refine: false` leaves out the whole-receiver refinement, which names a String.
|
|
1594
|
+
def analyse_regex_match_predicate(node, scope, refine: true)
|
|
1536
1595
|
return nil if node.arguments.nil?
|
|
1537
1596
|
return nil unless node.arguments.arguments.size == 1
|
|
1538
1597
|
|
|
@@ -1552,10 +1611,96 @@ module Rigor
|
|
|
1552
1611
|
# match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
|
|
1553
1612
|
# matched, so the string operand narrows to the imported refinement the anchored pattern
|
|
1554
1613
|
# names. The falsey edge is untouched: a failed match proves nothing about the shape.
|
|
1555
|
-
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
|
|
1614
|
+
truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope) if refine
|
|
1556
1615
|
[truthy, falsey]
|
|
1557
1616
|
end
|
|
1558
1617
|
|
|
1618
|
+
# Issue #1379 — `subject !~ pattern` is `Kernel#!~`, which returns `!(subject =~ pattern)`. It runs the same
|
|
1619
|
+
# match in the caller's frame, so its edges are {#analyse_regex_match_predicate}'s swapped: the falsey edge is
|
|
1620
|
+
# the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
|
|
1621
|
+
# truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
|
|
1622
|
+
# declined (a non-regex operand, an ambiguous constant, `//x`).
|
|
1623
|
+
#
|
|
1624
|
+
# Unlike `=~`'s, an edge here is narrowed only when the receiver proves what the frame holds on it
|
|
1625
|
+
# ({#not_match_receiver_edges}). An edge it cannot vouch for keeps the match globals forgotten, since the call
|
|
1626
|
+
# may still have rebound them. The refinement is kept only when the local it would narrow is known to be a
|
|
1627
|
+
# String on the match edge ({#refinable_operand?}).
|
|
1628
|
+
def analyse_regex_not_match_predicate(node, scope)
|
|
1629
|
+
edges = analyse_regex_match_predicate(node, scope, refine: refinable_operand?(node, scope))
|
|
1630
|
+
return nil if edges.nil?
|
|
1631
|
+
|
|
1632
|
+
match, no_match = edges
|
|
1633
|
+
forgotten = scope.forget_match_globals
|
|
1634
|
+
case not_match_receiver_edges(node, scope)
|
|
1635
|
+
when :both then [no_match, match]
|
|
1636
|
+
when :match then [forgotten, match]
|
|
1637
|
+
else [forgotten, forgotten]
|
|
1638
|
+
end
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
# Which edges of `recv !~ pattern` the receiver lets {#analyse_regex_not_match_predicate} narrow. `Kernel#!~`
|
|
1642
|
+
# dispatches `=~` on the receiver:
|
|
1643
|
+
#
|
|
1644
|
+
# - A Regexp receiver (the pattern on the left) runs `Regexp#=~`, which sets `$~` on both outcomes, a `nil`
|
|
1645
|
+
# argument included: `:both`.
|
|
1646
|
+
# - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String or a
|
|
1647
|
+
# Symbol.
|
|
1648
|
+
# - `NilClass#=~` answers nil and leaves `$~` alone, so a nil receiver reaches the no-match edge with the
|
|
1649
|
+
# previous match still bound; it never reaches the match edge. A `Dynamic` receiver may be nil at run time
|
|
1650
|
+
# whatever its static facet says: `:match` when every member is a String, a Symbol, nil, or a `Dynamic`
|
|
1651
|
+
# that is untyped or whose facet holds only those.
|
|
1652
|
+
# - Any other receiver may define `=~` in Ruby, which binds `$~` in its own frame, not the caller's: nil.
|
|
1653
|
+
#
|
|
1654
|
+
# A safe-navigation `recv&.!~(pattern)` answers nil without matching on a nil receiver, so it declines too.
|
|
1655
|
+
def not_match_receiver_edges(node, scope)
|
|
1656
|
+
return nil if node.receiver.nil? || node.safe_navigation?
|
|
1657
|
+
return :both if regex_operand_pattern(node.receiver, scope).is_a?(RegexMatchPattern)
|
|
1658
|
+
|
|
1659
|
+
kinds = not_match_member_kinds(scope.type_of(node.receiver), scope)
|
|
1660
|
+
return :both if kinds.all? { |kind| %i[string symbol].include?(kind) }
|
|
1661
|
+
|
|
1662
|
+
:match if kinds.all? { |kind| %i[string symbol nil dynamic].include?(kind) }
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
# Whether the local operand {#apply_whole_receiver_refinement} would narrow holds only Strings and nil, which
|
|
1666
|
+
# the match edge excludes. The refinement names a String, so a member that may be a Symbol — a Symbol, or an
|
|
1667
|
+
# untyped `Dynamic` that `Kernel#!~` accepts as the receiver or `Regexp#=~` as the argument — keeps it out:
|
|
1668
|
+
# `return if raw !~ /\A\d+\z/` on an untyped `raw` must not make `raw.to_proc` an undefined method.
|
|
1669
|
+
def refinable_operand?(node, scope)
|
|
1670
|
+
operand = [node.receiver, *node.arguments&.arguments].find { |o| o.is_a?(Prism::LocalVariableReadNode) }
|
|
1671
|
+
type = operand && scope.local(operand.name)
|
|
1672
|
+
return true if type.nil?
|
|
1673
|
+
|
|
1674
|
+
not_match_member_kinds(type, scope).all? { |kind| %i[string nil].include?(kind) }
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
def not_match_member_kinds(type, scope)
|
|
1678
|
+
(type.is_a?(Type::Union) ? type.members : [type]).map { |member| not_match_member_kind(member, scope) }
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
# `:string` or `:symbol` for a member {.narrow_not_class} proves a String or a Symbol (a value or refinement of
|
|
1682
|
+
# either, or a class the environment orders below it; a project subclass it cannot order reads as neither),
|
|
1683
|
+
# `:nil`, `:dynamic` for a `Dynamic` whose facet is untyped or holds only Strings, Symbols and nil, or nil for
|
|
1684
|
+
# anything else.
|
|
1685
|
+
def not_match_member_kind(member, scope)
|
|
1686
|
+
return not_match_dynamic_kind(member, scope) if member.is_a?(Type::Dynamic)
|
|
1687
|
+
return :nil if narrow_nil(member) == member && !member.is_a?(Type::Bot)
|
|
1688
|
+
|
|
1689
|
+
environment = scope.environment
|
|
1690
|
+
{ "String" => :string, "Symbol" => :symbol }.each do |class_name, kind|
|
|
1691
|
+
return kind if narrow_not_class(member, class_name, environment: environment, scope: scope).is_a?(Type::Bot)
|
|
1692
|
+
end
|
|
1693
|
+
nil
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
def not_match_dynamic_kind(member, scope)
|
|
1697
|
+
facet = member.static_facet
|
|
1698
|
+
return :dynamic if facet.is_a?(Type::Top)
|
|
1699
|
+
|
|
1700
|
+
facet_kinds = not_match_member_kinds(facet, scope)
|
|
1701
|
+
:dynamic if facet_kinds.all? { |kind| %i[string symbol nil].include?(kind) }
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1559
1704
|
# `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
|
|
1560
1705
|
# proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
|
|
1561
1706
|
# single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
|
|
@@ -2239,9 +2384,10 @@ module Rigor
|
|
|
2239
2384
|
end
|
|
2240
2385
|
|
|
2241
2386
|
# Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
|
|
2242
|
-
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}).
|
|
2387
|
+
# narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}). So
|
|
2388
|
+
# does an instance variable (#1446, {#narrow_guarded_slot}).
|
|
2243
2389
|
def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
|
|
2244
|
-
slot = receiver_slot(receiver, scope, kinds:
|
|
2390
|
+
slot = receiver_slot(receiver, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2245
2391
|
return nil if slot.nil?
|
|
2246
2392
|
|
|
2247
2393
|
class_slot_scopes(scope, slot, class_name, exact: exact)
|
|
@@ -2250,11 +2396,9 @@ module Rigor
|
|
|
2250
2396
|
def class_slot_scopes(scope, slot, class_name, exact:)
|
|
2251
2397
|
environment = scope.environment
|
|
2252
2398
|
truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
environment: environment, scope: scope))
|
|
2257
|
-
]
|
|
2399
|
+
falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
|
|
2400
|
+
truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
|
|
2401
|
+
[truthy_scope, narrow_guarded_slot(scope, slot, falsey)]
|
|
2258
2402
|
end
|
|
2259
2403
|
|
|
2260
2404
|
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
@@ -2429,12 +2573,12 @@ module Rigor
|
|
|
2429
2573
|
end
|
|
2430
2574
|
|
|
2431
2575
|
# Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
|
|
2432
|
-
# constant argument. The Range / Regexp receivers stay local-only.
|
|
2576
|
+
# constant argument, and on an instance variable (#1446). The Range / Regexp receivers stay local-only.
|
|
2433
2577
|
def analyse_case_equality_on_slot(receiver, arg, scope)
|
|
2434
2578
|
class_name = lexical_class_name(receiver, scope)
|
|
2435
2579
|
return nil if class_name.nil?
|
|
2436
2580
|
|
|
2437
|
-
slot = receiver_slot(arg, scope, kinds:
|
|
2581
|
+
slot = receiver_slot(arg, scope, kinds: CLASS_GUARD_SLOT_KINDS)
|
|
2438
2582
|
return nil if slot.nil?
|
|
2439
2583
|
|
|
2440
2584
|
class_slot_scopes(scope, slot, class_name, exact: false)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../reflection"
|
|
4
|
+
require_relative "../analysis/dependency_recorder"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Issue #1697 — the modules the project mixes into `Object`, and what they say about a method name.
|
|
9
|
+
#
|
|
10
|
+
# A top-level `include M` is `main.include`, which is `Object.include`, so M's instance methods exist on
|
|
11
|
+
# every object: bare top-level calls (`main` is an `Object`) and calls on any receiver whose class
|
|
12
|
+
# descends from `Object`. `ScopeIndexer` records that statement under its own owner key
|
|
13
|
+
# ({TOPLEVEL_INCLUDE_KEY}), and an `include M` inside `class Object ... end` on `Object`. The RBS
|
|
14
|
+
# environment knows nothing of either: `Object`'s RBS ancestry is core's, so a direct lookup on an
|
|
15
|
+
# RBS-known receiver misses every one of M's methods. This module is the one place that asks the
|
|
16
|
+
# project's `Object` edges instead, for the
|
|
17
|
+
# `call.unresolved-toplevel` and `call.undefined-method` silences in `Analysis::CheckRules`. Nothing
|
|
18
|
+
# types a call through these edges yet: what Ruby reaches first, even for a bare top-level call, depends
|
|
19
|
+
# on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def` on `main`),
|
|
20
|
+
# so typing is issue #1715's.
|
|
21
|
+
#
|
|
22
|
+
# The edges are read through `Scope::ResolutionChain`, the two owners' instance chains, whose search
|
|
23
|
+
# files the ADR-46 class edge of every project entry it passes, the declaring files among them (a file
|
|
24
|
+
# that wrote a top-level `include` is one), so a consumer re-checks when the include that answered for
|
|
25
|
+
# it is edited away. A project that never reopens `Object` and writes no top-level `include` returns
|
|
26
|
+
# before reading anything. The question is a union over the chain, not "which definer does Ruby call":
|
|
27
|
+
# any entry that may define the name answers, so the chain's `settle` verdict is not asked.
|
|
28
|
+
module ObjectMixins
|
|
29
|
+
OWNER = "Object"
|
|
30
|
+
# The owner key `ScopeIndexer` files a top-level `include` under, beside `Object`'s own key, so that
|
|
31
|
+
# no ancestry walk that types a call reaches it (see `ScopeIndexer#toplevel_include_owner`).
|
|
32
|
+
TOPLEVEL_INCLUDE_KEY = "<toplevel-include>"
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# How the project's `Object` mixins answer `method_name`, nearest edge first:
|
|
37
|
+
#
|
|
38
|
+
# - `[:rbs, module_name, definition]` — an RBS-declared module declares it; `definition` is its
|
|
39
|
+
# `RBS::Definition::Method`. `:private_rbs` is the same for a private declaration, which a bare call
|
|
40
|
+
# reaches and a call with a receiver does not.
|
|
41
|
+
# - `[:source, module_name]` — a project module (or `Object` itself) defines it.
|
|
42
|
+
# - `[:singleton_function, module_name]` — an RBS-declared module declares it only as a singleton method.
|
|
43
|
+
# Core declares `Math`'s functions that way (`def self.sqrt`) although they are `module_function`s,
|
|
44
|
+
# whose private instance copies a bare call reaches after `include Math`. RBS cannot tell such a
|
|
45
|
+
# function from a plain `def self.`, so the answer silences a bare call only.
|
|
46
|
+
# - `[:opaque, module_name]` — a module neither RBS nor the project declares. Its surface cannot be
|
|
47
|
+
# enumerated, so it may define the name.
|
|
48
|
+
# - `[:truncated, "Object"]` — the chain was cut at its budget with no answer before the cut: a definer
|
|
49
|
+
# may sit past it, which is uncertainty, not absence.
|
|
50
|
+
# - `nil` — no edge, or every edge names a module known not to declare it.
|
|
51
|
+
#
|
|
52
|
+
# `skip:` names the kinds a caller cannot use; an entry of one of them is passed over and the search
|
|
53
|
+
# goes on to the next, so the answer is a union over the chain for those callers rather than the
|
|
54
|
+
# nearest entry only.
|
|
55
|
+
def answer(scope, method_name, skip: NO_SKIP)
|
|
56
|
+
return nil if scope.nil?
|
|
57
|
+
|
|
58
|
+
# ADR-46 — the answer depends on whether a top-level `include` exists anywhere, so a consumer checked
|
|
59
|
+
# while none did must re-check when one appears. The key appears as a declared "class" of the file that
|
|
60
|
+
# writes it (`discovered_class_sources`), which is what this name-keyed existence edge listens for.
|
|
61
|
+
if Analysis::DependencyRecorder.active?
|
|
62
|
+
Analysis::DependencyRecorder.read_last_segment(:class, TOPLEVEL_INCLUDE_KEY)
|
|
63
|
+
end
|
|
64
|
+
EDGE_OWNERS.each do |owner|
|
|
65
|
+
next unless scope.known_user_class?(owner)
|
|
66
|
+
|
|
67
|
+
found = answer_on_chain(scope, owner, method_name, skip)
|
|
68
|
+
return found if found
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Where `Object`'s project edges are recorded: a top-level `include` under the indexer's own key, and
|
|
74
|
+
# `Object` itself for `include M` in `class Object` (and `Object`'s own definitions).
|
|
75
|
+
EDGE_OWNERS = [TOPLEVEL_INCLUDE_KEY, OWNER].freeze
|
|
76
|
+
private_constant :EDGE_OWNERS
|
|
77
|
+
|
|
78
|
+
def answer_on_chain(scope, owner, method_name, skip)
|
|
79
|
+
chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
|
|
80
|
+
found = chain.search(scope, side: :instance) do |entry|
|
|
81
|
+
entry_answer = answer_for_entry(scope, entry, method_name)
|
|
82
|
+
entry_answer unless entry_answer.nil? || skip.include?(entry_answer.first)
|
|
83
|
+
end
|
|
84
|
+
return found if found
|
|
85
|
+
|
|
86
|
+
chain.truncated? ? [:truncated, owner] : nil
|
|
87
|
+
end
|
|
88
|
+
private_class_method :answer_on_chain
|
|
89
|
+
|
|
90
|
+
NO_SKIP = [].freeze
|
|
91
|
+
# An explicit receiver reaches neither a private method (a `module_function`'s instance copy among
|
|
92
|
+
# them) nor, on #746's
|
|
93
|
+
# reading of an ancestor's unknown include (the receiver's own includes only), a module whose surface
|
|
94
|
+
# is unknown: one top-level `include` of an undeclared module would otherwise silence
|
|
95
|
+
# `call.undefined-method` on every receiver in the project.
|
|
96
|
+
EXPLICIT_RECEIVER_SKIP = %i[private_rbs singleton_function opaque].freeze
|
|
97
|
+
private_constant :NO_SKIP, :EXPLICIT_RECEIVER_SKIP
|
|
98
|
+
|
|
99
|
+
# Whether some `Object` mixin may define `method_name`: the question the silences ask. A bare
|
|
100
|
+
# top-level call (`receiver: :implicit`, `call.unresolved-toplevel`) counts every answer, an unknown
|
|
101
|
+
# module's included: such an include is the likeliest source of a bare name, and silencing a typo
|
|
102
|
+
# there is the cheaper error. A call with a receiver (`receiver: :explicit`, `call.undefined-method`)
|
|
103
|
+
# counts only a declared, public definition.
|
|
104
|
+
def may_define?(scope, method_name, receiver:)
|
|
105
|
+
skip = receiver == :explicit ? EXPLICIT_RECEIVER_SKIP : NO_SKIP
|
|
106
|
+
!answer(scope, method_name, skip: skip).nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Whether a receiver of `class_name` reaches `Object`'s mixins at all: `Object` itself, or an RBS
|
|
110
|
+
# class the environment orders below it. A `BasicObject` subclass does not; a class the environment
|
|
111
|
+
# cannot order is not claimed.
|
|
112
|
+
def reaches_object?(scope, class_name)
|
|
113
|
+
name = class_name.to_s.delete_prefix("::")
|
|
114
|
+
return true if name == OWNER
|
|
115
|
+
|
|
116
|
+
environment = scope&.environment
|
|
117
|
+
!environment.nil? && environment.class_ordering(name, OWNER) == :subclass
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A project entry (`Object` itself, or a project module mixed into it) answers when it defines the name;
|
|
121
|
+
# an external one when RBS declares the name on one of the names its spelling can denote (as an instance
|
|
122
|
+
# method, or as a module's singleton function), and is opaque when none of those names is declared at
|
|
123
|
+
# all.
|
|
124
|
+
def answer_for_entry(scope, entry, method_name)
|
|
125
|
+
unless entry.external?
|
|
126
|
+
return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
entry.candidates.each do |candidate|
|
|
130
|
+
definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
131
|
+
return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
|
|
132
|
+
return [:singleton_function, candidate] if singleton_function?(scope, candidate, method_name)
|
|
133
|
+
end
|
|
134
|
+
return nil if entry.candidates.any? { |candidate| declared?(scope, candidate) }
|
|
135
|
+
|
|
136
|
+
[:opaque, entry.raw.to_s]
|
|
137
|
+
end
|
|
138
|
+
private_class_method :answer_for_entry
|
|
139
|
+
|
|
140
|
+
def private_definition?(definition)
|
|
141
|
+
definition.respond_to?(:accessibility) && definition.accessibility == :private
|
|
142
|
+
end
|
|
143
|
+
private_class_method :private_definition?
|
|
144
|
+
|
|
145
|
+
def singleton_function?(scope, name, method_name)
|
|
146
|
+
environment = scope.environment
|
|
147
|
+
return false if environment.nil? || !environment.rbs_module?(name)
|
|
148
|
+
|
|
149
|
+
definition = Rigor::Reflection.singleton_method_definition(name, method_name, scope: scope)
|
|
150
|
+
# Declared on the module itself: its singleton class also answers every `Module` method
|
|
151
|
+
# (`include`, `name`), which no `include` of it brings to the top level.
|
|
152
|
+
!definition.nil? && definition.defined_in.to_s.delete_prefix("::") == name
|
|
153
|
+
end
|
|
154
|
+
private_class_method :singleton_function?
|
|
155
|
+
|
|
156
|
+
# Asked of both tables a project method lands in, as `RbsDispatch#source_declares_through_ancestors?`
|
|
157
|
+
# does: `discovered_methods` withholds a plain `def` written in another file, which is exactly where a
|
|
158
|
+
# top-level `include` usually finds its module. The chain already expands each module's own ancestry, so
|
|
159
|
+
# one entry's own tables are asked.
|
|
160
|
+
def source_defines?(scope, name, method_name)
|
|
161
|
+
scope.discovered_method?(name, method_name, :instance) || !scope.user_def_for(name, method_name).nil?
|
|
162
|
+
end
|
|
163
|
+
private_class_method :source_defines?
|
|
164
|
+
|
|
165
|
+
# The same "is this mixin's surface enumerable" reading `Analysis::CheckRules#known_mixin?` gives a
|
|
166
|
+
# class's own includes (#746): a project class or module, or an RBS declaration Rigor did not
|
|
167
|
+
# synthesize to keep a broken signature buildable.
|
|
168
|
+
def declared?(scope, name)
|
|
169
|
+
return true if scope.discovered_classes.key?(name)
|
|
170
|
+
return false unless Rigor::Reflection.rbs_class_known?(name, scope: scope)
|
|
171
|
+
|
|
172
|
+
loader = scope.environment&.rbs_loader
|
|
173
|
+
return true if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
174
|
+
|
|
175
|
+
!loader.synthesized_type_names.include?(name)
|
|
176
|
+
end
|
|
177
|
+
private_class_method :declared?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -245,13 +245,16 @@ module Rigor
|
|
|
245
245
|
tables[:discovered_classes] = classes unless classes.empty?
|
|
246
246
|
DISCOVERY_FIELD.each do |index_key, field|
|
|
247
247
|
table = def_index.fetch(index_key)
|
|
248
|
-
tables[field] = table unless table.empty?
|
|
248
|
+
tables[field] = table unless table.empty? && !Scope::DiscoveryIndex.paired?(field)
|
|
249
249
|
end
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
# ADR-119 WD1 — the pairs travel together: a pair is dropped only when both halves are empty. A sibling whose
|
|
251
|
+
# member this collector does not seed completes to that member's empty value, which the base scope holds.
|
|
252
|
+
Scope::DiscoveryIndex.compact_pairs(tables.merge(def_index.fetch(:siblings)))
|
|
253
|
+
rescue StandardError => e
|
|
252
254
|
# Discovery is best-effort; a malformed corner of the project must not crash the
|
|
253
|
-
# protection scan. Without discovery the collector simply resolves fewer call sites.
|
|
254
|
-
|
|
255
|
+
# protection scan. Without discovery the collector simply resolves fewer call sites. A broken
|
|
256
|
+
# walk contract is not such a corner, and an empty seed would hide it.
|
|
257
|
+
e.is_a?(DeclarationWalk::ContractError) ? raise : {}
|
|
255
258
|
end
|
|
256
259
|
|
|
257
260
|
DISCOVERY_FIELD = {
|
|
@@ -260,7 +263,7 @@ module Rigor
|
|
|
260
263
|
def_sources: :discovered_def_sources,
|
|
261
264
|
superclasses: :discovered_superclasses,
|
|
262
265
|
header_nestings: :discovered_header_nestings,
|
|
263
|
-
includes: :discovered_includes,
|
|
266
|
+
includes: :discovered_includes, unpositioned_mixins: :unpositioned_mixins,
|
|
264
267
|
class_sources: :discovered_class_sources,
|
|
265
268
|
method_visibilities: :discovered_method_visibilities,
|
|
266
269
|
methods: :discovered_methods,
|
|
@@ -359,9 +362,7 @@ module Rigor
|
|
|
359
362
|
end
|
|
360
363
|
|
|
361
364
|
def lookup_def(scope, class_name, method, kind)
|
|
362
|
-
|
|
363
|
-
per_class = table[class_name]
|
|
364
|
-
per_class && per_class[method]
|
|
365
|
+
scope.def_node_slot(class_name, method, kind)
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
# The leading required-positional parameters — the ones a call's leading positional arguments
|
|
@@ -7,9 +7,9 @@ module Rigor
|
|
|
7
7
|
# ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
|
|
8
8
|
# pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
|
|
9
9
|
#
|
|
10
|
-
# Each entry records one `def` declaration the pre-pass observed inside a class /
|
|
11
|
-
# dispatcher's `try_project_patched_method` tier consults this registry between the plugin
|
|
12
|
-
# the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
|
|
10
|
+
# Each entry records one `def`, `alias` or `alias_method` declaration the pre-pass observed inside a class /
|
|
11
|
+
# module body. The dispatcher's `try_project_patched_method` tier consults this registry between the plugin
|
|
12
|
+
# tier and the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
|
|
13
13
|
# to cross-file dispatch.
|
|
14
14
|
#
|
|
15
15
|
# The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
|
|
@@ -22,8 +22,13 @@ module Rigor
|
|
|
22
22
|
# the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
|
|
23
23
|
# `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
|
|
24
24
|
# `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
#
|
|
26
|
+
# Issue #1702 — an `alias new old` / `alias_method :new, :old` in a patch is recorded too. When a `def` or
|
|
27
|
+
# alias earlier in the `pre_eval:` files binds `old`, the alias's entry copies that entry's `return_type` (and
|
|
28
|
+
# its `alias_of`); otherwise `alias_of` names `old`, a method the class had before the patch, and the
|
|
29
|
+
# dispatcher answers the alias with what that method answers. `alias_of` is nil for a `def`.
|
|
30
|
+
Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of) do
|
|
31
|
+
def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil)
|
|
27
32
|
super
|
|
28
33
|
end
|
|
29
34
|
end
|