rigortype 0.3.5 → 0.3.7
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/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -37,7 +37,7 @@ module Rigor
|
|
|
37
37
|
#
|
|
38
38
|
# See docs/internal-spec/inference-engine.md (Narrowing) and
|
|
39
39
|
# docs/type-specification/control-flow-analysis.md for the binding contract.
|
|
40
|
-
# rubocop:disable Metrics/ModuleLength
|
|
40
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
41
41
|
module Narrowing
|
|
42
42
|
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
43
43
|
SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
|
|
@@ -439,7 +439,7 @@ module Rigor
|
|
|
439
439
|
end
|
|
440
440
|
end
|
|
441
441
|
|
|
442
|
-
# rubocop:disable Metrics/ClassLength
|
|
442
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
443
443
|
class << self
|
|
444
444
|
private
|
|
445
445
|
|
|
@@ -897,6 +897,30 @@ module Rigor
|
|
|
897
897
|
# the conservative no-op (falsey could mean nil receiver OR a falsey method result).
|
|
898
898
|
safe_nav_result = analyse_safe_nav_receiver(node, scope)
|
|
899
899
|
return safe_nav_result if safe_nav_result
|
|
900
|
+
|
|
901
|
+
# Issue #606 slice 1 — the same proof one call further out, where the `&.` result is
|
|
902
|
+
# fed to a suffix (`custom&.value.present?`, `backup&.user.nil?`) or compared against
|
|
903
|
+
# a non-nil operand. Sits after every pre-existing NARROWING path so those keep first claim
|
|
904
|
+
# on the node (the RBS-extended contribution hook still runs later and can be pre-empted —
|
|
905
|
+
# no live collision today since arg-free chains carry no argument facts):
|
|
906
|
+
# each of them returns nil for these shapes today (the predicate analysers require a
|
|
907
|
+
# local-read receiver, which a chain is not), so this only ever runs on nodes that
|
|
908
|
+
# were otherwise about to narrow nothing.
|
|
909
|
+
chain_result =
|
|
910
|
+
if %i[== !=].include?(node.name)
|
|
911
|
+
analyse_safe_nav_chain_equality(node, scope)
|
|
912
|
+
else
|
|
913
|
+
analyse_safe_nav_chain(node, scope)
|
|
914
|
+
end
|
|
915
|
+
return chain_result if chain_result
|
|
916
|
+
|
|
917
|
+
# Issue #606 slice 2 — membership. Runs after `analyse_string_predicate`, which owns
|
|
918
|
+
# the `str.include?(literal)` shape; this one wants the mirror image (a collection
|
|
919
|
+
# receiver, a bound argument), so the two never contend for the same node.
|
|
920
|
+
if node.name == :include?
|
|
921
|
+
membership_result = analyse_membership_predicate(node, scope)
|
|
922
|
+
return membership_result if membership_result
|
|
923
|
+
end
|
|
900
924
|
end
|
|
901
925
|
|
|
902
926
|
# Slice 7 phase 15 — RBS::Extended predicate effects. When the method's RBS signature
|
|
@@ -1311,7 +1335,8 @@ module Rigor
|
|
|
1311
1335
|
name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
1312
1336
|
return nil if name.nil?
|
|
1313
1337
|
|
|
1314
|
-
type = Reflection.resolve_constant_type(name, scope: scope
|
|
1338
|
+
type = Reflection.resolve_constant_type(name, scope: scope,
|
|
1339
|
+
rooted: Source::ConstantPath.rooted?(node))
|
|
1315
1340
|
return :ambiguous if type.is_a?(Type::Union)
|
|
1316
1341
|
return nil unless type.is_a?(Type::Constant)
|
|
1317
1342
|
|
|
@@ -1764,7 +1789,8 @@ module Rigor
|
|
|
1764
1789
|
return nil if node.arguments.nil?
|
|
1765
1790
|
return nil unless node.arguments.arguments.size == 1
|
|
1766
1791
|
|
|
1767
|
-
|
|
1792
|
+
argument = node.arguments.arguments.first
|
|
1793
|
+
bare_name = static_class_name(argument)
|
|
1768
1794
|
return nil if bare_name.nil?
|
|
1769
1795
|
|
|
1770
1796
|
# Resolve `bare_name` through the lexical-scope chain so a name shadowed by the
|
|
@@ -1773,7 +1799,15 @@ module Rigor
|
|
|
1773
1799
|
# `Rigor::Type::Singleton#==`, `is_a?(Singleton)` should resolve to
|
|
1774
1800
|
# `Rigor::Type::Singleton`, not the top-level stdlib `Singleton` mixin (which would
|
|
1775
1801
|
# surface as a spurious `undefined-method` on subsequent `other.class_name` calls).
|
|
1776
|
-
|
|
1802
|
+
#
|
|
1803
|
+
# A leading `::` opts out of that walk (#614): `static_class_name` de-roots the
|
|
1804
|
+
# argument, so without the marker the guard answered the shadow while the READ
|
|
1805
|
+
# `::Foo.new` answered the top-level class — the guard and the value it guards
|
|
1806
|
+
# disagreeing about the same spelling. When no top-level class owns the name we
|
|
1807
|
+
# DECLINE (nil) rather than pass the unknown name down: keeping the pre-state is the
|
|
1808
|
+
# conservative answer, and narrowing to the shadow is the one answer that is wrong.
|
|
1809
|
+
class_name = rooted_class_predicate_name(argument, bare_name, scope)
|
|
1810
|
+
return nil if class_name.nil?
|
|
1777
1811
|
|
|
1778
1812
|
case node.receiver
|
|
1779
1813
|
when Prism::LocalVariableReadNode
|
|
@@ -1783,6 +1817,15 @@ module Rigor
|
|
|
1783
1817
|
end
|
|
1784
1818
|
end
|
|
1785
1819
|
|
|
1820
|
+
# The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
|
|
1821
|
+
# `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
|
|
1822
|
+
# the lexical resolution otherwise.
|
|
1823
|
+
def rooted_class_predicate_name(argument, bare_name, scope)
|
|
1824
|
+
return resolve_class_name_lexically(bare_name, scope) unless Source::ConstantPath.rooted?(argument)
|
|
1825
|
+
|
|
1826
|
+
class_known_to_scope?(scope, bare_name) ? bare_name : nil
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1786
1829
|
def analyse_class_predicate_on_local(node, scope, class_name, exact)
|
|
1787
1830
|
current = scope.local(node.receiver.name)
|
|
1788
1831
|
return nil if current.nil?
|
|
@@ -1844,9 +1887,27 @@ module Rigor
|
|
|
1844
1887
|
# `<prefix>::<bare_name>` (or bare `<bare_name>` at the top level) that the environment
|
|
1845
1888
|
# recognises. Falls back to `bare_name` itself when nothing in the chain resolves; the
|
|
1846
1889
|
# downstream `narrow_class` then yields the conservative answer for unknown receivers.
|
|
1890
|
+
#
|
|
1891
|
+
# A MULTI-SEGMENT spelling is relative too and gets the same walk (#635). `Type::Nominal`
|
|
1892
|
+
# written inside `module Rigor` is not "already qualified": Ruby anchors the path's FIRST
|
|
1893
|
+
# segment through `Module.nesting`, so the constant it names is `Rigor::Type::Nominal`.
|
|
1894
|
+
# Treating any `::` as absolute left every such guard narrowing to a name no environment
|
|
1895
|
+
# knows, and each subsequent reader call on the narrowed receiver degraded to
|
|
1896
|
+
# `Dynamic[top]` — ~300 sites in Rigor's own `lib/` alone. Trying the whole path under
|
|
1897
|
+
# each prefix is the same rule the single-segment case already applies, and it adopts a
|
|
1898
|
+
# candidate only when the scope actually knows it, so a genuinely absolute spelling still
|
|
1899
|
+
# falls through to `bare_name` unchanged.
|
|
1900
|
+
#
|
|
1901
|
+
# The chain IS `Module.nesting` wherever a declaration walk recorded one (#652): a COMPACT
|
|
1902
|
+
# `class Admin::UsersController` contributes the single entry `["Admin::UsersController"]`, so a
|
|
1903
|
+
# bare `User` guard there narrows to the top-level `::User` exactly as Ruby resolves it, while
|
|
1904
|
+
# the nested `module Admin; class UsersController` spelling still reaches `Admin::User`. Peeling
|
|
1905
|
+
# `scope.self_type.class_name` could not tell the two apart — the string is identical — and
|
|
1906
|
+
# answered `Admin::User` for both, reporting `undefined method` on correct code across all four
|
|
1907
|
+
# shapes at once. Because the repair lives in the shared derivation, `is_a?`, `case`/`when`, `===`
|
|
1908
|
+
# and the constant typer were fixed together rather than one guard shape at a time. A scope no
|
|
1909
|
+
# declaration walk built still falls back to the peel; see {Reflection.lexical_nesting_chain}.
|
|
1847
1910
|
def resolve_class_name_lexically(bare_name, scope)
|
|
1848
|
-
return bare_name if bare_name.include?("::") # Already qualified.
|
|
1849
|
-
|
|
1850
1911
|
chain = lexical_nesting_for(scope)
|
|
1851
1912
|
chain.each do |prefix|
|
|
1852
1913
|
candidate = "#{prefix}::#{bare_name}"
|
|
@@ -1864,20 +1925,12 @@ module Rigor
|
|
|
1864
1925
|
scope.discovered_classes.key?(candidate)
|
|
1865
1926
|
end
|
|
1866
1927
|
|
|
1867
|
-
#
|
|
1868
|
-
#
|
|
1869
|
-
#
|
|
1870
|
-
#
|
|
1871
|
-
# "Foo"]`. Returns an empty array when `self_type` is unknown.
|
|
1928
|
+
# `Module.nesting` for the guard's body, innermost first. Delegates to
|
|
1929
|
+
# {Reflection.lexical_nesting_chain} — the SINGLE owner of the question, shared with the constant
|
|
1930
|
+
# typer's step-1 ladder, so `User` read as a value and `x.is_a?(User)` written beside it cannot
|
|
1931
|
+
# resolve to two different classes ([#652](https://github.com/rigortype/rigor/issues/652)).
|
|
1872
1932
|
def lexical_nesting_for(scope)
|
|
1873
|
-
|
|
1874
|
-
base = case self_type
|
|
1875
|
-
when Type::Nominal, Type::Singleton then self_type.class_name
|
|
1876
|
-
end
|
|
1877
|
-
return [] if base.nil? || base.empty?
|
|
1878
|
-
|
|
1879
|
-
parts = base.split("::")
|
|
1880
|
-
parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
|
|
1933
|
+
Reflection.lexical_nesting_chain(scope)
|
|
1881
1934
|
end
|
|
1882
1935
|
|
|
1883
1936
|
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
@@ -1934,7 +1987,7 @@ module Rigor
|
|
|
1934
1987
|
# Only static class/module receivers narrow here — the Range / Regexp literal receivers
|
|
1935
1988
|
# (`(1..10) === x.foo`) are not a common method-chain shape and stay deferred.
|
|
1936
1989
|
def analyse_case_equality_on_chain(receiver, chain_arg, scope)
|
|
1937
|
-
class_name =
|
|
1990
|
+
class_name = lexical_class_name(receiver, scope)
|
|
1938
1991
|
return nil if class_name.nil?
|
|
1939
1992
|
|
|
1940
1993
|
address = stable_chain_address(chain_arg)
|
|
@@ -1952,7 +2005,7 @@ module Rigor
|
|
|
1952
2005
|
end
|
|
1953
2006
|
|
|
1954
2007
|
def analyse_case_equality_receiver(receiver, scope, local_name, current)
|
|
1955
|
-
if (class_name =
|
|
2008
|
+
if (class_name = lexical_class_name(receiver, scope))
|
|
1956
2009
|
return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
|
|
1957
2010
|
end
|
|
1958
2011
|
|
|
@@ -2192,7 +2245,7 @@ module Rigor
|
|
|
2192
2245
|
return integer_literal_when_result(current, int_literal, falsey_acc)
|
|
2193
2246
|
end
|
|
2194
2247
|
|
|
2195
|
-
target =
|
|
2248
|
+
target = lexical_class_name(condition, scope) || case_equality_target_class(condition)
|
|
2196
2249
|
return class_when_result(scope, current, target, falsey_acc) if target
|
|
2197
2250
|
|
|
2198
2251
|
nil
|
|
@@ -2304,6 +2357,25 @@ module Rigor
|
|
|
2304
2357
|
end
|
|
2305
2358
|
end
|
|
2306
2359
|
|
|
2360
|
+
# The class a constant reference denotes at THIS point in the source (#635). `case t when
|
|
2361
|
+
# Type::Union` inside `module Rigor` names `Rigor::Type::Union`, and narrowing to the
|
|
2362
|
+
# source spelling instead left the guarded receiver an unknown nominal whose every reader
|
|
2363
|
+
# dispatched to `Dynamic[top]`. The resolution APPROXIMATES Ruby's — see
|
|
2364
|
+
# {#resolve_class_name_lexically} for where the derived nesting chain diverges (#652).
|
|
2365
|
+
#
|
|
2366
|
+
# Shared by the case-equality shapes (`Class === x`, `case/when`) so one spelling narrows
|
|
2367
|
+
# to one name whichever guard it is written in; `is_a?` / `kind_of?` / `instance_of?` go
|
|
2368
|
+
# through {#rooted_class_predicate_name}, which layers #614's extra decline on top of the
|
|
2369
|
+
# same walk. A rooted `::Foo` names the top level and never a lexically nearer shadow, so
|
|
2370
|
+
# it keeps the un-walked spelling. nil for any non-constant shape, as before.
|
|
2371
|
+
def lexical_class_name(node, scope)
|
|
2372
|
+
bare_name = static_class_name(node)
|
|
2373
|
+
return nil if bare_name.nil?
|
|
2374
|
+
return bare_name if Source::ConstantPath.rooted?(node)
|
|
2375
|
+
|
|
2376
|
+
resolve_class_name_lexically(bare_name, scope)
|
|
2377
|
+
end
|
|
2378
|
+
|
|
2307
2379
|
# ----- narrow_class / narrow_not_class helpers -----
|
|
2308
2380
|
|
|
2309
2381
|
# Polarity-aware dispatch table for {.narrow_class} / {.narrow_not_class}. Avoids
|
|
@@ -2364,9 +2436,34 @@ module Rigor
|
|
|
2364
2436
|
context.polarity == :positive ? narrow_class_other(type, class_name) : type
|
|
2365
2437
|
end
|
|
2366
2438
|
|
|
2439
|
+
# Issue #657 — `Bot` is the assertion "this can never match", and only a KNOWN-disjoint ordering
|
|
2440
|
+
# supports it. `subclass_of?` folds `:disjoint` and `:unknown` into one `false`, which is safe on
|
|
2441
|
+
# the negative edge (where the pre-state survives) and an over-claim on the positive one:
|
|
2442
|
+
#
|
|
2443
|
+
# module Taggable; def tag = "t"; end
|
|
2444
|
+
# class Integer; include Taggable; end
|
|
2445
|
+
# x = 1
|
|
2446
|
+
# case x
|
|
2447
|
+
# when Taggable then x.tag # reported unreachable; `1.is_a?(Taggable)` is true
|
|
2448
|
+
# end
|
|
2449
|
+
#
|
|
2450
|
+
# The ordering is `:unknown` because an in-source `include` into a CORE class never reaches the
|
|
2451
|
+
# environment's class ordering — the include is discovered in project source, `Integer`'s ancestry
|
|
2452
|
+
# comes from RBS, and the two are not joined (the precision half, left in #657).
|
|
2453
|
+
#
|
|
2454
|
+
# `:unknown` joins to Dynamic, the same answer {#narrow_nominal_to_class} gives and for the same
|
|
2455
|
+
# reason: keeping the old bound licenses `call.undefined-method` on the guard-proven branch. Keeping
|
|
2456
|
+
# the literal was tried first and did exactly that — `s = "str"` under `when Unrelated` stayed
|
|
2457
|
+
# `Constant["str"]` and reported `undefined method 'other'`, trading one false positive for
|
|
2458
|
+
# another. The value is lost only on the guarded arm, which is where the guard just told us the
|
|
2459
|
+
# environment's picture was incomplete.
|
|
2367
2460
|
def narrow_constant_to_class(constant, class_name, context)
|
|
2368
2461
|
rigor_class = constant.value.class.name
|
|
2369
|
-
subclass_of?(rigor_class, class_name, context)
|
|
2462
|
+
return constant if subclass_of?(rigor_class, class_name, context)
|
|
2463
|
+
return Type::Combinator.untyped if !context.exact &&
|
|
2464
|
+
class_ordering(rigor_class, class_name, context) == :unknown
|
|
2465
|
+
|
|
2466
|
+
Type::Combinator.bot
|
|
2370
2467
|
end
|
|
2371
2468
|
|
|
2372
2469
|
def narrow_constant_not_class(constant, class_name, context)
|
|
@@ -2387,7 +2484,13 @@ module Rigor
|
|
|
2387
2484
|
case class_ordering(nominal.class_name, class_name, context)
|
|
2388
2485
|
when :superclass then Type::Combinator.nominal_of(class_name)
|
|
2389
2486
|
when :disjoint then Type::Combinator.bot
|
|
2390
|
-
|
|
2487
|
+
when :subclass then nominal
|
|
2488
|
+
else
|
|
2489
|
+
# :unknown — the guard proved membership in a class the environment cannot order against the
|
|
2490
|
+
# bound (an RBS-less gem class, typically). Keeping the old bound licenses `undefined-method`
|
|
2491
|
+
# on the guard-proven branch (concurrent-ruby's `done.is_a?(Concurrent::Maybe)` kept `Array`
|
|
2492
|
+
# and fired on `.value`); the honest join is Dynamic — the guard destroyed the old knowledge.
|
|
2493
|
+
Type::Combinator.untyped
|
|
2391
2494
|
end
|
|
2392
2495
|
end
|
|
2393
2496
|
|
|
@@ -2606,6 +2709,209 @@ module Rigor
|
|
|
2606
2709
|
[scope.public_send(writer, receiver.name, non_nil), scope]
|
|
2607
2710
|
end
|
|
2608
2711
|
|
|
2712
|
+
# Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
|
|
2713
|
+
# the receiver non-nil when the `&.` call is itself the predicate (`if v&.foo`). It cannot
|
|
2714
|
+
# see the far more common shape where the `&.` result is fed to one more call:
|
|
2715
|
+
#
|
|
2716
|
+
# if custom&.value.present? # mastodon extended_description.rb / privacy_policy.rb
|
|
2717
|
+
# return true if backup&.user.nil? # mastodon backup_worker.rb (the FALL-THROUGH proves it)
|
|
2718
|
+
#
|
|
2719
|
+
# Both were measured firing `call.possible-nil-receiver` on correct code (#574's matrix).
|
|
2720
|
+
#
|
|
2721
|
+
# ## Why a truthy chain proves the root non-nil, and when it does not
|
|
2722
|
+
#
|
|
2723
|
+
# If the root IS nil, `root&.y` is nil and every suffix runs ON nil. Reaching the guarded
|
|
2724
|
+
# branch then requires each of them to have returned — and the outermost to have returned
|
|
2725
|
+
# TRUTHY. Two things can stop that, and only these two are sound to rely on:
|
|
2726
|
+
#
|
|
2727
|
+
# 1. `NilClass` does not define the method. The call raises, so control never reaches the
|
|
2728
|
+
# branch at all, and narrowing the branch body is vacuously correct.
|
|
2729
|
+
# 2. The method is defined on `NilClass` and documented falsey there — {NIL_FALSEY_SUFFIXES}.
|
|
2730
|
+
# ActiveSupport's `nil.present?` is `false`, so a truthy answer excludes the nil root.
|
|
2731
|
+
#
|
|
2732
|
+
# Anything else must decline, and the temptation to wave this through is exactly where the
|
|
2733
|
+
# bug would be: most of what `NilClass` DOES define returns something truthy. `nil.to_s` is
|
|
2734
|
+
# `""`, `nil.to_a` is `[]`, `nil.to_i` is `0`, `nil.inspect` is `"nil"` — all truthy — and
|
|
2735
|
+
# `nil.nil?` and `nil.blank?` are literally `true`. Narrowing on `x&.y.nil?`'s TRUTHY edge
|
|
2736
|
+
# would invert the guard's meaning. `nil?` therefore gets the opposite edge instead: a
|
|
2737
|
+
# FALSEY `x&.y.nil?` says `x&.y` is not nil, which no nil root can produce.
|
|
2738
|
+
#
|
|
2739
|
+
# Scoped to chains that actually contain `&.`. The same raise-based argument would license
|
|
2740
|
+
# narrowing a plain `x.y.present?` chain, but there `x.y` is itself the latent nil bug this
|
|
2741
|
+
# rule exists to report, and suppressing it is the opposite of the intent.
|
|
2742
|
+
NIL_FALSEY_SUFFIXES = %i[present?].freeze
|
|
2743
|
+
private_constant :NIL_FALSEY_SUFFIXES
|
|
2744
|
+
|
|
2745
|
+
def analyse_safe_nav_chain(node, scope)
|
|
2746
|
+
chain = safe_nav_chain(node)
|
|
2747
|
+
return nil if chain.nil?
|
|
2748
|
+
|
|
2749
|
+
receiver, suffixes = chain
|
|
2750
|
+
edge = safe_nav_chain_edge(suffixes, scope)
|
|
2751
|
+
return nil if edge.nil?
|
|
2752
|
+
|
|
2753
|
+
narrowed = safe_nav_chain_narrowed_scope(receiver, scope)
|
|
2754
|
+
return nil if narrowed.nil?
|
|
2755
|
+
|
|
2756
|
+
edge == :truthy ? [narrowed, scope] : [scope, narrowed]
|
|
2757
|
+
end
|
|
2758
|
+
|
|
2759
|
+
# Walks a call chain down to its `&.` link, collecting the suffix method names applied
|
|
2760
|
+
# above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
|
|
2761
|
+
# direct local / ivar read — those are the two bindings a scope can rewrite — and unless
|
|
2762
|
+
# every suffix is a bare reader or predicate, since an argument or a block could make the
|
|
2763
|
+
# answer depend on something other than the receiver.
|
|
2764
|
+
#
|
|
2765
|
+
# `allow_bare:` false keeps the zero-suffix case (`if v&.foo`) with
|
|
2766
|
+
# {.analyse_safe_nav_receiver}, which already owns it; the equality form passes true
|
|
2767
|
+
# because there the `&.` call IS the comparison's receiver.
|
|
2768
|
+
def safe_nav_chain(node, allow_bare: false)
|
|
2769
|
+
suffixes = []
|
|
2770
|
+
current = node
|
|
2771
|
+
while current.is_a?(Prism::CallNode) && !current.safe_navigation?
|
|
2772
|
+
return nil unless argument_free?(current) && current.block.nil?
|
|
2773
|
+
|
|
2774
|
+
suffixes << current.name
|
|
2775
|
+
current = current.receiver
|
|
2776
|
+
end
|
|
2777
|
+
return nil unless current.is_a?(Prism::CallNode) && current.safe_navigation?
|
|
2778
|
+
return nil if suffixes.empty? && !allow_bare
|
|
2779
|
+
|
|
2780
|
+
receiver = current.receiver
|
|
2781
|
+
return nil unless receiver.is_a?(Prism::LocalVariableReadNode) ||
|
|
2782
|
+
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
2783
|
+
|
|
2784
|
+
[receiver, suffixes]
|
|
2785
|
+
end
|
|
2786
|
+
|
|
2787
|
+
# Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
|
|
2788
|
+
# below the outermost) only ever have to NOT return, so they are held to the raise test
|
|
2789
|
+
# alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
|
|
2790
|
+
def safe_nav_chain_edge(suffixes, scope)
|
|
2791
|
+
outermost, *inner = suffixes
|
|
2792
|
+
return nil unless inner.all? { |name| !nilclass_method?(name, scope) }
|
|
2793
|
+
return :falsey if outermost == :nil?
|
|
2794
|
+
return :truthy if NIL_FALSEY_SUFFIXES.include?(outermost)
|
|
2795
|
+
return :truthy unless nilclass_method?(outermost, scope)
|
|
2796
|
+
|
|
2797
|
+
nil
|
|
2798
|
+
end
|
|
2799
|
+
|
|
2800
|
+
def safe_nav_chain_narrowed_scope(receiver, scope)
|
|
2801
|
+
reader, writer =
|
|
2802
|
+
case receiver
|
|
2803
|
+
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
2804
|
+
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
2805
|
+
else return nil
|
|
2806
|
+
end
|
|
2807
|
+
|
|
2808
|
+
current = scope.public_send(reader, receiver.name)
|
|
2809
|
+
return nil if current.nil?
|
|
2810
|
+
|
|
2811
|
+
non_nil = narrow_non_nil(current)
|
|
2812
|
+
return nil if non_nil.equal?(current)
|
|
2813
|
+
|
|
2814
|
+
scope.public_send(writer, receiver.name, non_nil)
|
|
2815
|
+
end
|
|
2816
|
+
|
|
2817
|
+
# Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
|
|
2818
|
+
# nil: `next unless status&.account_id == @account.id`. A nil root makes the left side nil,
|
|
2819
|
+
# and `nil == <non-nil>` is false, so a true comparison excludes it.
|
|
2820
|
+
#
|
|
2821
|
+
# The whole form turns on proving the OTHER operand non-nil, and `x&.y == nil` is the
|
|
2822
|
+
# counter-example that must never narrow — there a nil root makes the comparison TRUE.
|
|
2823
|
+
# {.provably_non_nil_type?} is deliberately a whitelist rather than "not obviously nil":
|
|
2824
|
+
# an operand typed `Dynamic` may well be nil at runtime, so it declines. That costs the
|
|
2825
|
+
# third corpus site (`@account.id` types opaque there), which is the honest price of not
|
|
2826
|
+
# guessing.
|
|
2827
|
+
#
|
|
2828
|
+
# `!=` takes the opposite edge, for the same reason `nil?` did above: a nil root makes
|
|
2829
|
+
# `nil != <non-nil>` true, so it is the FALSEY edge that proves the root non-nil.
|
|
2830
|
+
def analyse_safe_nav_chain_equality(node, scope)
|
|
2831
|
+
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
2832
|
+
|
|
2833
|
+
chain = safe_nav_chain(node.receiver, allow_bare: true)
|
|
2834
|
+
return nil if chain.nil?
|
|
2835
|
+
|
|
2836
|
+
receiver, suffixes = chain
|
|
2837
|
+
return nil unless suffixes.all? { |name| !nilclass_method?(name, scope) }
|
|
2838
|
+
return nil unless provably_non_nil_type?(scope.type_of(node.arguments.arguments.first))
|
|
2839
|
+
|
|
2840
|
+
narrowed = safe_nav_chain_narrowed_scope(receiver, scope)
|
|
2841
|
+
return nil if narrowed.nil?
|
|
2842
|
+
|
|
2843
|
+
node.name == :== ? [narrowed, scope] : [scope, narrowed]
|
|
2844
|
+
end
|
|
2845
|
+
|
|
2846
|
+
# Whitelist: true only for types every inhabitant of which is non-nil. `Dynamic` / `Top`
|
|
2847
|
+
# answer FALSE — an untyped operand is exactly the case where nil cannot be excluded —
|
|
2848
|
+
# and so does `Bot`, which has no inhabitants to reason about.
|
|
2849
|
+
def provably_non_nil_type?(type)
|
|
2850
|
+
case type
|
|
2851
|
+
when Type::Constant then !type.value.nil?
|
|
2852
|
+
when Type::Nominal then type.class_name != "NilClass"
|
|
2853
|
+
when Type::Singleton, Type::Tuple, Type::HashShape then true
|
|
2854
|
+
when Type::Union then type.members.all? { |member| provably_non_nil_type?(member) }
|
|
2855
|
+
end
|
|
2856
|
+
end
|
|
2857
|
+
|
|
2858
|
+
# Issue #606 slice 2 — MEMBERSHIP. `collection.include?(x)` answering true proves `x` is one
|
|
2859
|
+
# of the collection's elements, so if none of those can be nil, neither can `x`:
|
|
2860
|
+
#
|
|
2861
|
+
# unless Redmine::Reaction::REACTABLE_TYPES.include?(object_type) # redmine reactions_controller.rb
|
|
2862
|
+
# render_403
|
|
2863
|
+
# return
|
|
2864
|
+
# end
|
|
2865
|
+
# @object = object_type.constantize.find(...) # ← fired before this
|
|
2866
|
+
#
|
|
2867
|
+
# The proof lives entirely in the collection, which is why {.nil_free_collection?} answers
|
|
2868
|
+
# only for shapes whose element types are actually known — a literal / `Tuple`, or an
|
|
2869
|
+
# `Array` / `Set` carrying an element type. An untyped collection declines: `Dynamic` may
|
|
2870
|
+
# hold nil, and "we do not know" must not read as "no nil here".
|
|
2871
|
+
#
|
|
2872
|
+
# Only the truthy edge narrows. A false `include?` says nothing about `x` — a nil `x` is
|
|
2873
|
+
# simply not in the list, which is exactly what false means.
|
|
2874
|
+
MEMBERSHIP_COLLECTION_CLASSES = %w[Array Set].freeze
|
|
2875
|
+
private_constant :MEMBERSHIP_COLLECTION_CLASSES
|
|
2876
|
+
|
|
2877
|
+
def analyse_membership_predicate(node, scope)
|
|
2878
|
+
return nil if node.receiver.nil? || node.arguments.nil?
|
|
2879
|
+
return nil unless node.arguments.arguments.size == 1
|
|
2880
|
+
|
|
2881
|
+
argument = node.arguments.arguments.first
|
|
2882
|
+
reader, writer =
|
|
2883
|
+
case argument
|
|
2884
|
+
when Prism::LocalVariableReadNode then %i[local with_local]
|
|
2885
|
+
when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
|
|
2886
|
+
else return nil
|
|
2887
|
+
end
|
|
2888
|
+
return nil unless nil_free_collection?(scope.type_of(node.receiver))
|
|
2889
|
+
|
|
2890
|
+
current = scope.public_send(reader, argument.name)
|
|
2891
|
+
return nil if current.nil?
|
|
2892
|
+
|
|
2893
|
+
non_nil = narrow_non_nil(current)
|
|
2894
|
+
return nil if non_nil.equal?(current)
|
|
2895
|
+
|
|
2896
|
+
[scope.public_send(writer, argument.name, non_nil), scope]
|
|
2897
|
+
end
|
|
2898
|
+
|
|
2899
|
+
# True only when every inhabitant the collection can yield is non-nil. Restricted to
|
|
2900
|
+
# sequence shapes on purpose: `Hash#include?` asks about KEYS and `Range#include?` about
|
|
2901
|
+
# bounds, so neither element story is the one this rule reasons about.
|
|
2902
|
+
def nil_free_collection?(type)
|
|
2903
|
+
case type
|
|
2904
|
+
when Type::Tuple
|
|
2905
|
+
!type.elements.empty? && type.elements.all? { |element| provably_non_nil_type?(element) }
|
|
2906
|
+
when Type::Constant
|
|
2907
|
+
type.value.is_a?(Array) && !type.value.empty? && type.value.none?(&:nil?)
|
|
2908
|
+
when Type::Nominal
|
|
2909
|
+
MEMBERSHIP_COLLECTION_CLASSES.include?(type.class_name) &&
|
|
2910
|
+
type.type_args.size == 1 &&
|
|
2911
|
+
provably_non_nil_type?(type.type_args.first)
|
|
2912
|
+
end
|
|
2913
|
+
end
|
|
2914
|
+
|
|
2609
2915
|
# Layers the safe-nav non-nil truthy narrowing over the edges an existing predicate path
|
|
2610
2916
|
# already produced, so a safe-nav string predicate (`v&.start_with?(x)`) keeps its
|
|
2611
2917
|
# relational fact AND proves `v` non-nil on the truthy edge. Re-runs the predicate's
|
|
@@ -2645,8 +2951,6 @@ module Rigor
|
|
|
2645
2951
|
[truthy_a.join(truthy_b), falsey_b]
|
|
2646
2952
|
end
|
|
2647
2953
|
end
|
|
2648
|
-
# rubocop:enable Metrics/ClassLength
|
|
2649
2954
|
end
|
|
2650
|
-
# rubocop:enable Metrics/ModuleLength
|
|
2651
2955
|
end
|
|
2652
2956
|
end
|
|
@@ -260,6 +260,7 @@ module Rigor
|
|
|
260
260
|
singleton_def_nodes: :discovered_singleton_def_nodes,
|
|
261
261
|
def_sources: :discovered_def_sources,
|
|
262
262
|
superclasses: :discovered_superclasses,
|
|
263
|
+
header_nestings: :discovered_header_nestings,
|
|
263
264
|
includes: :discovered_includes,
|
|
264
265
|
class_sources: :discovered_class_sources,
|
|
265
266
|
method_visibilities: :discovered_method_visibilities,
|
|
@@ -54,6 +54,21 @@ module Rigor
|
|
|
54
54
|
# Value-pinning a *provably* frozen single-write literal cross-file is a strictly later question; it needs
|
|
55
55
|
# its own FP measurement and is deliberately not attempted here.
|
|
56
56
|
#
|
|
57
|
+
# **Issue #644 asked that later question and answered it.** The discovery pre-pass now publishes a
|
|
58
|
+
# project-wide table of syntactic frozen-scalar literal assignments (Symbol / Integer / Float / boolean,
|
|
59
|
+
# written once, in one file), with the FP measurement this paragraph asked for. Where that table and this
|
|
60
|
+
# one both answer a name, THAT TABLE WINS and the value is not erased — a listed `TIMEOUT = 30` reads
|
|
61
|
+
# `30`, not `Integer`. Nothing above is wrong; its scope is narrower than it reads. The widening exists
|
|
62
|
+
# for the rvalues whose basis this collector cannot fully see, and every hazard it names — the closed
|
|
63
|
+
# `HashShape`, the `Tuple` displacing an RBS overload, the mutable String — is a form #644's table
|
|
64
|
+
# DECLINES. So the two producers do not overlap where the widening earns its cost, and where they do
|
|
65
|
+
# overlap the narrower, purely syntactic basis is the better-founded answer. Normative in
|
|
66
|
+
# `docs/internal-spec/inference-engine.md` § "Cross-file value constants"; pinned by
|
|
67
|
+
# `spec/rigor/analysis/pre_eval_constants_spec.rb`.
|
|
68
|
+
#
|
|
69
|
+
# What this collector still owns alone: every rvalue #644 declines — a String, an Array, a Hash, a class
|
|
70
|
+
# alias, and anything that needs the rvalue TYPED rather than read off the syntax.
|
|
71
|
+
#
|
|
57
72
|
# ## The multi-file write rule — widen on conflict, all the way to `Dynamic[top]`
|
|
58
73
|
#
|
|
59
74
|
# Within one file, `ScopeIndexer#record_constant_write` unions repeated writes; that union is widened as a
|
|
@@ -153,9 +153,12 @@ module Rigor
|
|
|
153
153
|
when Type::Bot then :bot
|
|
154
154
|
when Type::Top then :top
|
|
155
155
|
when Type::Constant then :constant
|
|
156
|
-
when Type::Nominal, Type::Singleton
|
|
156
|
+
when Type::Nominal, Type::Singleton,
|
|
157
|
+
Type::DataClass, Type::StructClass then :nominal
|
|
157
158
|
when Type::Tuple, Type::HashShape,
|
|
158
|
-
Type::IntegerRange, Type::App
|
|
159
|
+
Type::IntegerRange, Type::App,
|
|
160
|
+
Type::DataInstance, Type::StructInstance,
|
|
161
|
+
Type::BoundMethod then :shaped
|
|
159
162
|
when Type::Refined then :refined
|
|
160
163
|
when Type::Dynamic then classify_dynamic(type)
|
|
161
164
|
when Type::Union then worst_of(type.members)
|
|
@@ -151,7 +151,8 @@ module Rigor
|
|
|
151
151
|
def descend_class_or_module(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
152
152
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
153
153
|
if name && node.body
|
|
154
|
-
|
|
154
|
+
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
155
|
+
walk_node(node.body, child_prefix, in_singleton_class, source_path, entries)
|
|
155
156
|
else
|
|
156
157
|
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
157
158
|
end
|