rigortype 0.3.6 → 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 +1 -1
- 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 +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- 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/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 +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- 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 +147 -10
- 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/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/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- 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.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -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/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/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
- metadata +69 -3
|
@@ -12,6 +12,8 @@ require_relative "dependency_recorder"
|
|
|
12
12
|
require_relative "check_rules/rule_ids"
|
|
13
13
|
require_relative "check_rules/inferred_param_guard"
|
|
14
14
|
require_relative "check_rules/declaration_sourced_guard"
|
|
15
|
+
require_relative "check_rules/published_constant_guard"
|
|
16
|
+
require_relative "check_rules/dead_version_guard_arms"
|
|
15
17
|
require_relative "check_rules/rule_walk"
|
|
16
18
|
require_relative "check_rules/always_truthy_condition_collector"
|
|
17
19
|
require_relative "check_rules/unreachable_clause_collector"
|
|
@@ -50,7 +52,7 @@ module Rigor
|
|
|
50
52
|
# resolves these to their runtime class for dispatch.
|
|
51
53
|
# - receivers whose class name is NOT registered in the
|
|
52
54
|
# loader (RBS-blind environments, unknown stdlib).
|
|
53
|
-
# rubocop:disable Metrics/ModuleLength
|
|
55
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
54
56
|
module CheckRules
|
|
55
57
|
# ADR-87 WD4 — the canonical rule-id table lives in the light `check_rules/rule_ids.rb` (required at the
|
|
56
58
|
# top of this file) so {Analysis::RuleCatalog} can read it without loading the engine.
|
|
@@ -102,6 +104,11 @@ module Rigor
|
|
|
102
104
|
COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
|
|
103
105
|
diagnostics.concat(send(builder, path, collectors[role].results))
|
|
104
106
|
end
|
|
107
|
+
# ADR-47 WD5 — the dead arm of a decidable version guard (#627) reports nothing: it cannot run on
|
|
108
|
+
# the Ruby being checked with, so a diagnostic there is a false positive. Applied before
|
|
109
|
+
# `suppression.*` joins the list, which stays reportable inside a dead arm — a malformed
|
|
110
|
+
# `# rigor:disable` marker is an authoring error regardless of whether its code runs.
|
|
111
|
+
diagnostics = DeadVersionGuardArms.filter(diagnostics, root)
|
|
105
112
|
# Suppression-marker validation (`suppression.*`) runs BEFORE the filter so its own diagnostics are
|
|
106
113
|
# suppressible like any other rule — `# rigor:disable suppression.unknown-rule` on the offending
|
|
107
114
|
# comment's line works, with no regress (the token itself is known, so it never re-fires).
|
|
@@ -649,7 +656,7 @@ module Rigor
|
|
|
649
656
|
resolved.nil? || resolved.empty? ? [token] : resolved
|
|
650
657
|
end
|
|
651
658
|
|
|
652
|
-
# rubocop:disable Metrics/ClassLength
|
|
659
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
653
660
|
class << self
|
|
654
661
|
private
|
|
655
662
|
|
|
@@ -659,9 +666,14 @@ module Rigor
|
|
|
659
666
|
scope = scope_index[call_node]
|
|
660
667
|
return nil if scope.nil?
|
|
661
668
|
|
|
662
|
-
#
|
|
663
|
-
#
|
|
664
|
-
|
|
669
|
+
# The two exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
|
|
670
|
+
# branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
|
|
671
|
+
# gate belongs where that one consult is rather than duplicated into each shape's path. On the
|
|
672
|
+
# union side that is placement, not demonstrated coverage: no fixture reaches the gate through
|
|
673
|
+
# `union_undefined_method_diagnostic` today — every plugin-typed union anyone has built carries a
|
|
674
|
+
# `Singleton` arm, which `union_arm_blocks_undefined_fire?` declines on outright.
|
|
675
|
+
# See {#call_site_exempt?}.
|
|
676
|
+
return nil if call_site_exempt?(call_node, scope)
|
|
665
677
|
|
|
666
678
|
# N3 — a safe-navigation call (`recv&.m`) never dispatches on the
|
|
667
679
|
# nil edge of its receiver: at runtime it short-circuits to nil.
|
|
@@ -705,7 +717,7 @@ module Rigor
|
|
|
705
717
|
# because the real type is empty (the real `DRb` has
|
|
706
718
|
# `start_service`), so enumerating it to prove a call
|
|
707
719
|
# "undefined" would be a false positive.
|
|
708
|
-
return nil if
|
|
720
|
+
return nil if unenumerable_receiver?(class_name, scope)
|
|
709
721
|
|
|
710
722
|
# Slice 7 phase 12 — suppress when the user has
|
|
711
723
|
# declared the method in source (`def` /
|
|
@@ -730,21 +742,73 @@ module Rigor
|
|
|
730
742
|
method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
|
|
731
743
|
return nil if method_def
|
|
732
744
|
|
|
733
|
-
|
|
734
|
-
# `MethodDispatcher#user_class_fallback_receiver`'s module
|
|
735
|
-
# path): an instance method on a module-mixin like
|
|
736
|
-
# `PP::ObjectMixin` observes Kernel / Object methods
|
|
737
|
-
# through every concrete includer's ancestor chain, so an
|
|
738
|
-
# unresolved `self.inspect` / `self.respond_to?` /
|
|
739
|
-
# `self.class` MUST NOT fire `undefined-method`. Retry
|
|
740
|
-
# against Object before the rule fires.
|
|
741
|
-
return nil if module_mixin_receiver?(receiver_type, scope) &&
|
|
742
|
-
lookup_method(receiver_type, "Object", call_node.name, scope)
|
|
745
|
+
return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind)
|
|
743
746
|
|
|
744
747
|
definition_site = project_definition_site(scope, class_name, call_node.name, kind)
|
|
748
|
+
# Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
|
|
749
|
+
# BUNDLED declaration (core / stdlib / a gem) the class is not the project's, so a project `def`
|
|
750
|
+
# on it is the ADR-17 monkey-patch this rule surfaces with the `pre_eval:` nudge. On the
|
|
751
|
+
# project's OWN sidecar `sig/` the declaration describes the source being analysed, the "patch"
|
|
752
|
+
# is just another of the project's files, and the nudge is not something a user can act on: it
|
|
753
|
+
# asks them to `pre_eval:` their own application. `rigor sig-gen --write` on redmine turned 29
|
|
754
|
+
# `call.undefined-method` into 171 through exactly this reading, 49 of them here.
|
|
755
|
+
return nil if project_sidecar_owns_method?(scope, class_name, call_node.name, kind)
|
|
756
|
+
|
|
745
757
|
build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
|
|
746
758
|
end
|
|
747
759
|
|
|
760
|
+
# The two probes that run only once every cheaper answer has come back "absent", kept together
|
|
761
|
+
# because they share that position and nothing else.
|
|
762
|
+
#
|
|
763
|
+
# - Issue #739 — an instance-side MODULE receiver. A value typed as a mixin module is an instance of
|
|
764
|
+
# some class that includes it, and that class contributes an arbitrary surface: inside
|
|
765
|
+
# `module Attachable::InstanceMethods`, `self.project` is defined by the ActiveRecord model that
|
|
766
|
+
# includes the module, and in RBS a parameter typed `Taggable` means "something whose class
|
|
767
|
+
# includes Taggable", not "something whose methods are Taggable's". The surface is not
|
|
768
|
+
# enumerable, so nothing here can prove a method absent. This probe used to RETRY against Object
|
|
769
|
+
# — enough for `self.inspect` / `self.class`, and nothing else — which left 45 firings on
|
|
770
|
+
# redmine's `acts_as_*` mixins the moment a `sig/` declared them.
|
|
771
|
+
#
|
|
772
|
+
# The union twin already declines these arms outright (`union_arm_blocks_undefined_fire?`, "a
|
|
773
|
+
# module mixin whose Object-inherited methods the per-arm lookup would miss"). The two rules
|
|
774
|
+
# answering the same question differently about the same receiver is the fork this closes; the
|
|
775
|
+
# union's reading is the correct one.
|
|
776
|
+
#
|
|
777
|
+
# Singleton-side module receivers are NOT covered and must not be: `M.typo` on a namespace module
|
|
778
|
+
# with `module_function` / `def self.` has a real, enumerable surface and keeps firing.
|
|
779
|
+
# - Issue #723 — the project's own ancestry. Everything above answered "the RBS does not have
|
|
780
|
+
# it"; a project ancestor still might, and the typer has already resolved the site through
|
|
781
|
+
# exactly this walk. It is last because it is the only probe here that walks the class graph
|
|
782
|
+
# (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
|
|
783
|
+
def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind)
|
|
784
|
+
return true if module_mixin_receiver?(receiver_type, scope)
|
|
785
|
+
return true if mixin_self_class_receiver?(call_node, scope)
|
|
786
|
+
return true if unknown_mixin_includer?(class_name, scope)
|
|
787
|
+
|
|
788
|
+
ancestry_declares_method?(scope, class_name, call_node.name, kind)
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Issue #739 — the singleton twin of the module-mixin receiver, and the only shape that reaches the
|
|
792
|
+
# singleton side legitimately. `self.class` inside a mixin's instance method is the INCLUDER's class
|
|
793
|
+
# at runtime; the engine types it `Singleton[<the module>]`, one hop along from the same wrong
|
|
794
|
+
# `self`. redmine's `acts_as_*` mixins reach their configuration that way
|
|
795
|
+
# (`self.class.attachable_options`), 17 sites of it.
|
|
796
|
+
#
|
|
797
|
+
# Keyed on the call-site SYNTAX rather than on the receiver type, because `Singleton[M]` is also
|
|
798
|
+
# what a namespace module's own `M.helper` produces, and that surface is real and enumerable — a
|
|
799
|
+
# type-only test would silence every `M.typo` in the project. Here the question is narrower: was
|
|
800
|
+
# this singleton produced by `.class` on a value whose type is a mixin module?
|
|
801
|
+
def mixin_self_class_receiver?(call_node, scope)
|
|
802
|
+
receiver = call_node.receiver
|
|
803
|
+
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :class
|
|
804
|
+
return false unless receiver.arguments.nil?
|
|
805
|
+
|
|
806
|
+
inner = receiver.receiver
|
|
807
|
+
return false if inner.nil?
|
|
808
|
+
|
|
809
|
+
module_mixin_receiver?(scope.type_of(inner), scope)
|
|
810
|
+
end
|
|
811
|
+
|
|
748
812
|
# ADR-17 — when the project itself defines this method on the
|
|
749
813
|
# receiver class somewhere in the analyzed file set (a reopened
|
|
750
814
|
# core/stdlib/gem class the dispatcher does not apply cross-
|
|
@@ -761,6 +825,38 @@ module Rigor
|
|
|
761
825
|
scope.user_def_site_for(class_name, method_name)
|
|
762
826
|
end
|
|
763
827
|
|
|
828
|
+
# Issue #735 — the site is evidence, and whose RBS the class has decides what it is evidence OF. On
|
|
829
|
+
# a BUNDLED declaration (core / stdlib / a gem) the class is not the project's, so a project `def`
|
|
830
|
+
# on it is the ADR-17 monkey-patch this rule surfaces with the `pre_eval:` nudge. On the project's
|
|
831
|
+
# OWN sidecar `sig/` the declaration describes the source being analysed, the "patch" is just
|
|
832
|
+
# another of the project's files, and the nudge is not something a user can act on: it asks them to
|
|
833
|
+
# `pre_eval:` their own application. `rigor sig-gen --write` on redmine turned 29
|
|
834
|
+
# `call.undefined-method` into 171 through exactly this reading, 49 of them here.
|
|
835
|
+
def project_sidecar_owns_method?(scope, class_name, method_name, kind)
|
|
836
|
+
Rigor::Reflection.project_declared_class?(class_name, scope: scope) &&
|
|
837
|
+
project_defines_method?(scope, class_name, method_name, kind)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Issue #735 — "does the project define this method, on either side, somewhere in its own files".
|
|
841
|
+
# Distinct from {#project_definition_site}, which answers the narrower question the ADR-17 MESSAGE
|
|
842
|
+
# asks (instance-side only, because that is the monkey-patch shape whose `pre_eval:` advice applies).
|
|
843
|
+
# The suppression needs both sides: `def self.available_search_types` in one file and its call in
|
|
844
|
+
# another is the same "second file of my own class" shape as the instance case, and on redmine it is
|
|
845
|
+
# the larger half — modules with class-method surfaces are how that codebase is organised.
|
|
846
|
+
def project_defines_method?(scope, class_name, method_name, kind)
|
|
847
|
+
if kind == :singleton
|
|
848
|
+
# The singleton side has no ancestor walk to ask (#731); it answers for the receiver's own name.
|
|
849
|
+
!scope.user_singleton_def_site_for(class_name, method_name).nil?
|
|
850
|
+
else
|
|
851
|
+
# Through the project's ancestry, not the receiver's name alone: a base class defining a helper
|
|
852
|
+
# its subclasses call is ordinary Ruby, and a sidecar `sig/` that declares the SUBCLASS without
|
|
853
|
+
# that helper was reporting it (`AbstractAdapter#url`, called from `FilesystemAdapter#target`).
|
|
854
|
+
# The def-node walk is what has to answer here — `discovered_methods` deliberately withholds a
|
|
855
|
+
# plain cross-file `def` (`ScopeIndexer#finalize_def_index`), which is exactly this shape.
|
|
856
|
+
!scope.user_def_through_ancestors(class_name, method_name).first.nil?
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
764
860
|
def module_mixin_receiver?(receiver_type, scope)
|
|
765
861
|
return false unless receiver_type.is_a?(Type::Nominal)
|
|
766
862
|
return false if scope.environment.nil?
|
|
@@ -783,6 +879,22 @@ module Rigor
|
|
|
783
879
|
project_patched_method?(scope, class_name, method_name, kind)
|
|
784
880
|
end
|
|
785
881
|
|
|
882
|
+
# Issue #723 — the same question as {#source_declared_method?}, asked through the project's own
|
|
883
|
+
# ANCESTRY rather than the receiver's name alone. A class whose `sig/` declares it without its
|
|
884
|
+
# project superclass typed `:admin_val` under `dump_type` and drew `undefined method 'admin_val'`
|
|
885
|
+
# at the same column of the same run: the probe read a name-keyed table while the typer resolved
|
|
886
|
+
# the call through the class graph.
|
|
887
|
+
#
|
|
888
|
+
# It is deliberately NOT folded into `source_declared_method?`, which every `call.*` rule consults
|
|
889
|
+
# early on its hot path. The walk reads `Scope#superclass_of` / `#includes_of`, and those record an
|
|
890
|
+
# ADR-46 file-level ancestry edge; running it for every call would have made `Widget.new` — a call
|
|
891
|
+
# RBS answers, that never reaches a verdict — record an ancestry dependency on `Widget`'s
|
|
892
|
+
# declaring file, coarsening incremental invalidation for the whole project. Asked HERE, one step
|
|
893
|
+
# before the diagnostic is built, the edge is recorded exactly where the answer depended on it.
|
|
894
|
+
def ancestry_declares_method?(scope, class_name, method_name, kind)
|
|
895
|
+
scope.discovered_method_through_ancestors?(class_name, method_name, kind)
|
|
896
|
+
end
|
|
897
|
+
|
|
786
898
|
# ADR-17 § "Inference contract" — consults
|
|
787
899
|
# `Environment#project_patched_methods` so a `def` declared
|
|
788
900
|
# in a `pre_eval:` file suppresses the diagnostic at the
|
|
@@ -853,6 +965,52 @@ module Rigor
|
|
|
853
965
|
)
|
|
854
966
|
end
|
|
855
967
|
|
|
968
|
+
# Issue #742 — the receivers whose method surface this rule cannot enumerate, in one question.
|
|
969
|
+
#
|
|
970
|
+
# {#unbounded_receiver_surface?} covers the ADR-26 open receivers and Rigor's own synthesized stubs.
|
|
971
|
+
# The generic metaclasses join it here: a value typed `Class` or `Module` is SOME class or module
|
|
972
|
+
# object, and its singleton methods cannot be read off the metaclass — `def self.included(base)`
|
|
973
|
+
# receives the includer, so `base.class_attribute :main_menu` and `base.main_menu = true` are calls
|
|
974
|
+
# on whatever included the module. The union twin has declined these arms since it was written
|
|
975
|
+
# (`METACLASS_ARMS`, "a `plugin_class : Class` really holds a `Plugin` subclass with `.manifest`");
|
|
976
|
+
# the scalar rule enumerated `Module`'s own RBS instead and reported them.
|
|
977
|
+
#
|
|
978
|
+
# Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has eight
|
|
979
|
+
# other callers (arity, the `raise` verdicts), and widening all of them is a separate change with
|
|
980
|
+
# its own evidence to gather.
|
|
981
|
+
def unenumerable_receiver?(class_name, scope)
|
|
982
|
+
METACLASS_ARMS.include?(class_name) || unbounded_receiver_surface?(class_name, scope)
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# Issue #746 — a class that includes a module the environment does not know. `SudoMode::Form`
|
|
986
|
+
# includes `ActiveModel::Validations`, which no RBS in redmine's environment declares, and `valid?`
|
|
987
|
+
# comes from it: the ancestors are not fully known, so the method table is not enumerable and a
|
|
988
|
+
# verdict read off the part we can see is unsound. Same reasoning `unbounded_receiver_surface?`
|
|
989
|
+
# already applies to an ADR-26 open receiver and to Rigor's own synthesized stubs.
|
|
990
|
+
#
|
|
991
|
+
# Asked LATE, with {#ancestry_declares_method?} and for the same reason: `Scope#includes_of` records
|
|
992
|
+
# an ADR-46 file-level ancestry edge, so running it on the hot path made `Widget.new` — a call RBS
|
|
993
|
+
# answers, that never reaches a verdict — depend on `Widget`'s declaring file.
|
|
994
|
+
# `dependency_recorder_spec` caught it here exactly as it caught #723's walk.
|
|
995
|
+
#
|
|
996
|
+
# The receiver's OWN includes only. A project ancestor's unknown include makes the surface just as
|
|
997
|
+
# unknown; walking the chain is a deliberate deferral rather than an oversight.
|
|
998
|
+
def unknown_mixin_includer?(class_name, scope)
|
|
999
|
+
includes = scope.includes_of(class_name)
|
|
1000
|
+
return false if includes.empty?
|
|
1001
|
+
|
|
1002
|
+
includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def known_mixin?(raw_name, class_name, scope)
|
|
1006
|
+
scope.ancestor_name_candidates(class_name, raw_name).any? do |candidate|
|
|
1007
|
+
next true if scope.discovered_classes.key?(candidate)
|
|
1008
|
+
|
|
1009
|
+
Rigor::Reflection.rbs_class_known?(candidate, scope: scope) &&
|
|
1010
|
+
!synthesized_stub_receiver?(candidate, scope)
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
1013
|
+
|
|
856
1014
|
# Returns a qualified class name for the in-scope check.
|
|
857
1015
|
# Nominal / Singleton carry a single-class identity
|
|
858
1016
|
# directly. Constant projects to its value's class.
|
|
@@ -918,16 +1076,43 @@ module Rigor
|
|
|
918
1076
|
nil
|
|
919
1077
|
end
|
|
920
1078
|
|
|
1079
|
+
# ADR-26's own counterpart, not a use of it — issue #660 tracks whether this deserves a first-class
|
|
1080
|
+
# mechanism of its own rather than living here as a constant. `open_receivers:` (the manifest field,
|
|
1081
|
+
# below) only takes effect once a project LOADS the plugin that names it, but a gem overlay
|
|
1082
|
+
# (`data/gem_overlay/`, ADR-72) is auto-applied to every project that locks the gem WITHOUT opting
|
|
1083
|
+
# into a plugin — there is no manifest in that path for an `open_receivers:` entry to live on. This
|
|
1084
|
+
# is that path's counterpart: the class names a BUNDLED overlay declares knowing the declaration is
|
|
1085
|
+
# partial, protected independent of any plugin manifest.
|
|
1086
|
+
#
|
|
1087
|
+
# MEMBERSHIP ALONE is not enough, though: `unbounded_receiver_surface?` (below) also requires the
|
|
1088
|
+
# overlay directory that made the entry true to have actually LOADED this run — otherwise a project
|
|
1089
|
+
# that never locks the gem, and happens to own a class of the same name itself, would silently lose
|
|
1090
|
+
# `call.undefined-method` (and inherited-name `wrong-arity`) on that class, the exact regression this
|
|
1091
|
+
# gate exists to avoid (ADR-26 WD1: the protection is active exactly when the RBS it protects is).
|
|
1092
|
+
#
|
|
1093
|
+
# `ActiveSupport::Duration` is the first (and, as of #632, only) member: it forwards any method its
|
|
1094
|
+
# own class doesn't define to the wrapped numeric via `method_missing` (audited against
|
|
1095
|
+
# ActiveSupport 8.1.3.1's `lib/active_support/duration.rb` — see
|
|
1096
|
+
# `data/gem_overlay/activesupport/core_ext.rbs`'s `ActiveSupport::Duration` comment for the full
|
|
1097
|
+
# audit), so declaring even its reader surface makes it RBS-known — and, without this, every member
|
|
1098
|
+
# the declaration omits would become a false `call.undefined-method` for every project that locks
|
|
1099
|
+
# activesupport, the overwhelming majority of which never opt into the plugin at all.
|
|
1100
|
+
GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration"].freeze
|
|
1101
|
+
private_constant :GEM_OVERLAY_OPEN_RECEIVERS
|
|
1102
|
+
|
|
921
1103
|
# ADR-26 — whether `class_name` is declared "open" by a
|
|
922
1104
|
# loaded plugin (manifest `open_receivers:`). An open
|
|
923
1105
|
# class responds beyond its RBS surface, so the
|
|
924
1106
|
# `call.undefined-method` rule must not fire for it.
|
|
925
1107
|
# True when the receiver class responds beyond an enumerable
|
|
926
1108
|
# RBS method table, so proving a call "undefined" against it is
|
|
927
|
-
# unsound: a plugin-declared open receiver,
|
|
928
|
-
#
|
|
1109
|
+
# unsound: a plugin-declared open receiver, a Rigor-bundled-
|
|
1110
|
+
# overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
|
|
1111
|
+
# bundled directory this run actually loaded, or a
|
|
1112
|
+
# Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`).
|
|
929
1113
|
def unbounded_receiver_surface?(class_name, scope)
|
|
930
|
-
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope)
|
|
1114
|
+
open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
|
|
1115
|
+
(GEM_OVERLAY_OPEN_RECEIVERS.include?(class_name.to_s.sub(/\A::/, "")) && gem_overlay_loaded?(scope))
|
|
931
1116
|
end
|
|
932
1117
|
|
|
933
1118
|
def open_receiver?(class_name, scope)
|
|
@@ -937,6 +1122,79 @@ module Rigor
|
|
|
937
1122
|
registry.open_receiver?(class_name)
|
|
938
1123
|
end
|
|
939
1124
|
|
|
1125
|
+
# Whether THIS run's RBS environment actually includes one of Rigor's OWN partial declarations of the
|
|
1126
|
+
# overlaid gem — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see that
|
|
1127
|
+
# constant's comment). `RbsLoader#signature_paths` is the same list `Environment.for_project` builds
|
|
1128
|
+
# from `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
|
|
1129
|
+
# two of its members can carry that declaration:
|
|
1130
|
+
#
|
|
1131
|
+
# - a `data/gem_overlay/<gem>/` directory, which is a member if and only if `gem_overlay_paths`
|
|
1132
|
+
# picked it (the gem is locked, `:missing` coverage, and its plugin twin is NOT reachable —
|
|
1133
|
+
# ADR-72), so the `RbsLoader.under_gem_overlay_root?` prefix match is exact: no other signature
|
|
1134
|
+
# source lives under that root;
|
|
1135
|
+
# - the bundled plugin twin's own `sig/`, when a project wires it through `signature_paths:` instead
|
|
1136
|
+
# of `plugins:`. Before #672 that route loaded BOTH halves and `Duration` collapsed outright, so
|
|
1137
|
+
# nothing fired and nothing needed protecting; now the overlay stands down and the twin's
|
|
1138
|
+
# declaration is the one that loads — with no manifest behind it, so `open_receiver?` above stays
|
|
1139
|
+
# false and this is the only thing standing between a real `Duration` receiver and a false
|
|
1140
|
+
# `call.undefined-method` on every member the declaration omits.
|
|
1141
|
+
#
|
|
1142
|
+
# When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the second
|
|
1143
|
+
# goes true — its `sig/` rides in as a plugin signature path — but nothing here decides that case:
|
|
1144
|
+
# `open_receiver?`'s manifest-driven check short-circuits above it. The two answers agree, which is
|
|
1145
|
+
# the point; this predicate asks only whether a Rigor-authored partial declaration is in the
|
|
1146
|
+
# environment, never how the project asked for it.
|
|
1147
|
+
#
|
|
1148
|
+
# The two arms are asked in that order, over the WHOLE list each time, rather than interleaved per
|
|
1149
|
+
# path: the overlay arm is a string prefix and the twin arm canonicalises paths on the filesystem,
|
|
1150
|
+
# so the overwhelmingly common overlay-only run never reaches a syscall here.
|
|
1151
|
+
def gem_overlay_loaded?(scope)
|
|
1152
|
+
loader = scope.environment&.rbs_loader
|
|
1153
|
+
return false if loader.nil? || !loader.respond_to?(:signature_paths)
|
|
1154
|
+
|
|
1155
|
+
paths = loader.signature_paths
|
|
1156
|
+
paths.any? { |path| Rigor::Environment::RbsLoader.under_gem_overlay_root?(path) } ||
|
|
1157
|
+
Rigor::Environment::RbsLoader.gem_overlay_twin_signatures_loaded?(paths)
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
# The `call.undefined-method` exemptions that depend only on the CALL SITE, never on the receiver's
|
|
1161
|
+
# type or class — so they are decided before the rule looks at the receiver at all:
|
|
1162
|
+
#
|
|
1163
|
+
# - ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound, so firing
|
|
1164
|
+
# undefined-method against it is a false positive by construction.
|
|
1165
|
+
# - Issue #653 — a plugin answered this call site ({#plugin_typed_call?}).
|
|
1166
|
+
def call_site_exempt?(call_node, scope)
|
|
1167
|
+
inferred_param_receiver?(call_node, scope) || plugin_typed_call?(call_node, scope)
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# Issue #653 — true when a plugin's `dynamic_return` supplied the return type for THIS call node
|
|
1171
|
+
# (recorded by `MethodDispatcher` on `Scope#plugin_typed_calls` during the typing pass, exactly the
|
|
1172
|
+
# way `Scope#void_origins` is recorded and consumed).
|
|
1173
|
+
#
|
|
1174
|
+
# Why the rule declines. The plugin tier sits ABOVE `RbsDispatch` in `MethodDispatcher#resolve`, so
|
|
1175
|
+
# a plugin answer means the RBS never dispatched the call: the type at the site is the plugin's.
|
|
1176
|
+
# Reading that same receiver's RBS afterwards to prove the call undefined puts the two subsystems in
|
|
1177
|
+
# direct contradiction on one line of one run — the plugin says `Rails.logger` is an
|
|
1178
|
+
# `ActiveSupport::BroadcastLogger`, the rule says `logger` does not exist. Under ADR-5 a
|
|
1179
|
+
# partially-declared receiver is not a closed world, and incompleteness is exactly what a plugin is
|
|
1180
|
+
# loaded to cover, so the plugin's answer is the honest one. Without this, adding a four-line
|
|
1181
|
+
# partial `sig/` for a class a plugin already covered turned every covered call site into an error
|
|
1182
|
+
# — writing more RBS made a project's diagnostics worse.
|
|
1183
|
+
#
|
|
1184
|
+
# Deliberately NOT scoped to "the RBS is silent about this method": the precedence recorded here is
|
|
1185
|
+
# the dispatcher's own, and the dispatcher does not consult the RBS before letting a plugin answer.
|
|
1186
|
+
# For THIS rule the two readings coincide anyway — a method the RBS declares is found by
|
|
1187
|
+
# `lookup_method` below and never reaches the diagnostic — so the broader statement costs no
|
|
1188
|
+
# diagnostic and keeps the contract stated in one direction. `docs/internal-spec/plugin.md` §
|
|
1189
|
+
# "Return-type and narrowing contributions" is where it binds.
|
|
1190
|
+
#
|
|
1191
|
+
# Unlike {#unbounded_receiver_surface?} this is per CALL SITE, not per receiver class: a plugin
|
|
1192
|
+
# answering `Rails.logger` exempts that call and nothing else, so `Rails.no_such_reader` on the same
|
|
1193
|
+
# receiver still fires.
|
|
1194
|
+
def plugin_typed_call?(call_node, scope)
|
|
1195
|
+
scope.plugin_typed_call?(call_node)
|
|
1196
|
+
end
|
|
1197
|
+
|
|
940
1198
|
def synthesized_stub_receiver?(class_name, scope)
|
|
941
1199
|
loader = scope.environment&.rbs_loader
|
|
942
1200
|
return false if loader.nil? || !loader.respond_to?(:synthesized_type_names)
|
|
@@ -944,6 +1202,49 @@ module Rigor
|
|
|
944
1202
|
loader.synthesized_type_names.include?(class_name.to_s.sub(/\A::/, ""))
|
|
945
1203
|
end
|
|
946
1204
|
|
|
1205
|
+
# ADR-26 — the SIGNATURE half of the open-receiver policy. `call.undefined-method` already declines
|
|
1206
|
+
# on an unbounded surface: a plugin vouches that the class answers names its RBS cannot enumerate
|
|
1207
|
+
# (an `ActiveRecord::Relation` delegates every user-declared `scope` to its model). The rules that
|
|
1208
|
+
# read a RESOLVED SIGNATURE — wrong-arity and argument-type-mismatch — must decline on the same
|
|
1209
|
+
# ground, but only where the name resolved through an ANCESTOR rather than the open class itself.
|
|
1210
|
+
#
|
|
1211
|
+
# The failure it closes: `Issue.visible.open` is a declared scope taking `*args`, but `open` is also
|
|
1212
|
+
# `Kernel#open`, so resolution walks the Relation's ancestry, finds the IO opener and checks the
|
|
1213
|
+
# call against `(String, ...)` — `wrong number of arguments (given 0, expected 1..3)` on correct
|
|
1214
|
+
# code, plus `argument type mismatch: expected String, got false` on `open(false)`. The collision
|
|
1215
|
+
# set is every Object/Kernel name, so any project whose scope is called `open`, `select`, `test`,
|
|
1216
|
+
# `format`, `p`, `system`, … inherits the error the moment the first hop types. That is a wrong
|
|
1217
|
+
# signature applied to a method the receiver does not actually run — not a strict reading of a real
|
|
1218
|
+
# one — and it fires on code that works.
|
|
1219
|
+
#
|
|
1220
|
+
# A method the open class DECLARES stays checked: `Relation#limit` is in the plugin's own bundled
|
|
1221
|
+
# RBS, so `relation.limit` with no arguments still fires. The suppression is therefore bounded to
|
|
1222
|
+
# names the open class inherited, where the resolution was never authoritative to begin with.
|
|
1223
|
+
# The RBS method definition this call may be validated against, or nil when there is none worth
|
|
1224
|
+
# trusting: the lookup found nothing, it returned {#lookup_method}'s defensive `true` sentinel, or
|
|
1225
|
+
# the name is one an open receiver INHERITED rather than declared. Shared by the two rules that read
|
|
1226
|
+
# a resolved signature — `call.wrong-arity` and `call.argument-type-mismatch` — so they cannot drift
|
|
1227
|
+
# on which signatures are authoritative.
|
|
1228
|
+
def trustworthy_signature(receiver_type, class_name, call_node, scope)
|
|
1229
|
+
method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
|
|
1230
|
+
return nil if method_def.nil? || method_def == true
|
|
1231
|
+
return nil if unauthoritative_inherited_signature?(class_name, method_def, scope)
|
|
1232
|
+
|
|
1233
|
+
method_def
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
def unauthoritative_inherited_signature?(class_name, method_def, scope)
|
|
1237
|
+
return false unless unbounded_receiver_surface?(class_name, scope)
|
|
1238
|
+
return true unless method_def.respond_to?(:defined_in)
|
|
1239
|
+
|
|
1240
|
+
defined_in = method_def.defined_in
|
|
1241
|
+
return true if defined_in.nil?
|
|
1242
|
+
|
|
1243
|
+
strip_root(defined_in.to_s) != strip_root(class_name)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
def strip_root(name) = name.to_s.sub(/\A::/, "")
|
|
1247
|
+
|
|
947
1248
|
def definition_available?(receiver_type, class_name, scope)
|
|
948
1249
|
if receiver_type.is_a?(Type::Singleton)
|
|
949
1250
|
!Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
|
|
@@ -1026,6 +1327,10 @@ module Rigor
|
|
|
1026
1327
|
# an unrelated namespace.
|
|
1027
1328
|
def parent_matches?(child, parent, class_name)
|
|
1028
1329
|
parent_name = parent.to_s
|
|
1330
|
+
# Issue #722 residue 1 — a rooted parent (`< ::Base`) names the top level, so the namespace walk
|
|
1331
|
+
# below must not run for it. This peel is one of the two that predate
|
|
1332
|
+
# `Scope#ancestor_name_candidates` and resolve their own pair.
|
|
1333
|
+
return parent_name.delete_prefix("::") == class_name if parent_name.start_with?("::")
|
|
1029
1334
|
return true if parent_name == class_name
|
|
1030
1335
|
|
|
1031
1336
|
segments = child.to_s.split("::")[0...-1]
|
|
@@ -1120,7 +1425,7 @@ module Rigor
|
|
|
1120
1425
|
# by `undefined_method_diagnostic`; it returns nil
|
|
1121
1426
|
# when the call's receiver / RBS coverage / call shape
|
|
1122
1427
|
# disqualifies the rule.
|
|
1123
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
1428
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
1124
1429
|
def wrong_arity_diagnostic(path, call_node, scope_index)
|
|
1125
1430
|
return nil if call_node.receiver.nil?
|
|
1126
1431
|
return nil unless plain_positional_call?(call_node)
|
|
@@ -1151,8 +1456,8 @@ module Rigor
|
|
|
1151
1456
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1152
1457
|
return nil unless definition_available?(receiver_type, class_name, scope)
|
|
1153
1458
|
|
|
1154
|
-
method_def =
|
|
1155
|
-
return nil if method_def.nil?
|
|
1459
|
+
method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
|
|
1460
|
+
return nil if method_def.nil?
|
|
1156
1461
|
|
|
1157
1462
|
arity_envelope = compute_arity_envelope(method_def)
|
|
1158
1463
|
return nil if arity_envelope.nil?
|
|
@@ -1163,7 +1468,6 @@ module Rigor
|
|
|
1163
1468
|
|
|
1164
1469
|
build_arity_diagnostic(path, call_node, class_name, min, max, actual)
|
|
1165
1470
|
end
|
|
1166
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
1167
1471
|
|
|
1168
1472
|
# True for the outer `.new` of a chained `Struct.new(...).new`:
|
|
1169
1473
|
# `class_name`/`kind` already pin the receiver to
|
|
@@ -1313,7 +1617,7 @@ module Rigor
|
|
|
1313
1617
|
# method must be absent from `NilClass` (so the nil path really raises).
|
|
1314
1618
|
def nil_bearing_union_witnesses?(receiver_type, method_name, scope)
|
|
1315
1619
|
union_contains_nil?(receiver_type) &&
|
|
1316
|
-
union_has_nameable_non_nil_arm?(receiver_type) &&
|
|
1620
|
+
union_has_nameable_non_nil_arm?(receiver_type, scope) &&
|
|
1317
1621
|
union_method_present_on_non_nil?(receiver_type, method_name, scope) &&
|
|
1318
1622
|
!nil_class_has_method?(method_name, scope)
|
|
1319
1623
|
end
|
|
@@ -1355,8 +1659,22 @@ module Rigor
|
|
|
1355
1659
|
# `String | nil` keeps firing, and so does `Dynamic | String | nil` —
|
|
1356
1660
|
# the nameless arm stays permissive inside the all-arms check, which is
|
|
1357
1661
|
# only about the arms' method surface.
|
|
1358
|
-
def union_has_nameable_non_nil_arm?(union)
|
|
1359
|
-
union.members.any?
|
|
1662
|
+
def union_has_nameable_non_nil_arm?(union, scope)
|
|
1663
|
+
union.members.any? do |m|
|
|
1664
|
+
next false if nil_member?(m)
|
|
1665
|
+
|
|
1666
|
+
class_name = concrete_class_name(m)
|
|
1667
|
+
next false if class_name.nil?
|
|
1668
|
+
|
|
1669
|
+
# Issue #574 — nameable is not the same as ANSWERABLE. `method_present_anywhere?` reports
|
|
1670
|
+
# "present" for a named arm nothing knows the surface of, purely because nothing is known —
|
|
1671
|
+
# which is the same vacuity this gate's docstring says it exists to prevent, one level in. A
|
|
1672
|
+
# plugin-minted nominal like `ActionController::Parameters` ships no RBS and has no project
|
|
1673
|
+
# source, so it counted as a full witness and `q = params[:q]; q.strip` fired at ERROR severity
|
|
1674
|
+
# on correct Rails code.
|
|
1675
|
+
Rigor::Reflection.rbs_class_known?(class_name, scope: scope) ||
|
|
1676
|
+
scope.discovered_classes.key?(class_name)
|
|
1677
|
+
end
|
|
1360
1678
|
end
|
|
1361
1679
|
|
|
1362
1680
|
# The non-nil members must collectively support the
|
|
@@ -1379,8 +1697,11 @@ module Rigor
|
|
|
1379
1697
|
return true if scope.discovered_method?(class_name, method_name, :instance)
|
|
1380
1698
|
return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
1381
1699
|
return true unless definition_available?(member, class_name, scope)
|
|
1700
|
+
return true if lookup_method(member, class_name, method_name, scope)
|
|
1382
1701
|
|
|
1383
|
-
|
|
1702
|
+
# Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
|
|
1703
|
+
# the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
|
|
1704
|
+
ancestry_declares_method?(scope, class_name, method_name, :instance)
|
|
1384
1705
|
end
|
|
1385
1706
|
|
|
1386
1707
|
def nil_class_has_method?(method_name, scope)
|
|
@@ -2126,8 +2447,8 @@ module Rigor
|
|
|
2126
2447
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
2127
2448
|
return nil unless definition_available?(receiver_type, class_name, scope)
|
|
2128
2449
|
|
|
2129
|
-
method_def =
|
|
2130
|
-
return nil if method_def.nil?
|
|
2450
|
+
method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
|
|
2451
|
+
return nil if method_def.nil?
|
|
2131
2452
|
|
|
2132
2453
|
param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
|
|
2133
2454
|
mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
|
|
@@ -2516,8 +2837,8 @@ module Rigor
|
|
|
2516
2837
|
function.trailing_positionals.empty?
|
|
2517
2838
|
end
|
|
2518
2839
|
|
|
2519
|
-
def translate_param_type(rbs_type,
|
|
2520
|
-
Inference::RbsTypeTranslator.translate(rbs_type)
|
|
2840
|
+
def translate_param_type(rbs_type, environment)
|
|
2841
|
+
Inference::RbsTypeTranslator.translate(rbs_type, alias_expander: environment&.rbs_loader)
|
|
2521
2842
|
rescue StandardError
|
|
2522
2843
|
Type::Combinator.untyped
|
|
2523
2844
|
end
|
|
@@ -2651,7 +2972,7 @@ module Rigor
|
|
|
2651
2972
|
return nil unless self_type.respond_to?(:class_name)
|
|
2652
2973
|
|
|
2653
2974
|
method_def =
|
|
2654
|
-
if def_node.receiver.nil?
|
|
2975
|
+
if def_node.receiver.nil? && !singleton_context_def?(scope, self_type.class_name, def_node.name)
|
|
2655
2976
|
Reflection.instance_method_definition(self_type.class_name, def_node.name, scope: scope)
|
|
2656
2977
|
else
|
|
2657
2978
|
Reflection.singleton_method_definition(self_type.class_name, def_node.name, scope: scope)
|
|
@@ -2664,16 +2985,27 @@ module Rigor
|
|
|
2664
2985
|
declared_return_union(method_def, scope.environment)
|
|
2665
2986
|
end
|
|
2666
2987
|
|
|
2988
|
+
# A receiverless `def` inside `class << self` is a singleton def; the def NODE alone cannot say
|
|
2989
|
+
# so, but the discovery walk recorded it under the singleton kind (and not the instance kind).
|
|
2990
|
+
# Without this, `class << self; def load` compared its body against `Kernel#load: -> bool` — the
|
|
2991
|
+
# inherited INSTANCE method — instead of its own `def self.load` signature. A name defined on
|
|
2992
|
+
# both facets stays on the instance lookup (the pre-existing conservative reading).
|
|
2993
|
+
def singleton_context_def?(scope, class_name, method_name)
|
|
2994
|
+
scope.discovered_method?(class_name, method_name, :singleton) &&
|
|
2995
|
+
!scope.discovered_method?(class_name, method_name, :instance)
|
|
2996
|
+
end
|
|
2997
|
+
|
|
2667
2998
|
# `type_vars:` — ADR-35 WD9 tier 1. When the caller supplies a generic-instantiation
|
|
2668
2999
|
# substitution map (parent-side, keyed by the parent class's declared type-parameter names),
|
|
2669
3000
|
# a `-> T` return is translated at its instantiated type (`-> Integer`) rather than degrading
|
|
2670
3001
|
# to `Dynamic[Top]`. The default empty map is the pre-WD9 behaviour, under which any type
|
|
2671
3002
|
# variable degrades to `Dynamic[Top]` and the rule stays silent.
|
|
2672
|
-
def declared_return_union(method_def,
|
|
3003
|
+
def declared_return_union(method_def, environment, type_vars: {})
|
|
2673
3004
|
translated = method_def.method_types.filter_map do |mt|
|
|
2674
3005
|
Inference::RbsTypeTranslator.translate(
|
|
2675
3006
|
mt.type.return_type,
|
|
2676
|
-
self_type: nil, instance_type: nil, type_vars: type_vars
|
|
3007
|
+
self_type: nil, instance_type: nil, type_vars: type_vars,
|
|
3008
|
+
alias_expander: environment&.rbs_loader
|
|
2677
3009
|
)
|
|
2678
3010
|
rescue StandardError
|
|
2679
3011
|
nil
|
|
@@ -2807,8 +3139,9 @@ module Rigor
|
|
|
2807
3139
|
end
|
|
2808
3140
|
|
|
2809
3141
|
# Direct ancestors of `class_name` as project-discovered, qualified names: included / prepended
|
|
2810
|
-
# modules first, then the superclass. As-written names are resolved against the
|
|
2811
|
-
#
|
|
3142
|
+
# modules first, then the superclass. As-written names are resolved against the nesting the
|
|
3143
|
+
# subclass's declaration header is written in (`Scope#ancestor_name_candidates`, the single owner of
|
|
3144
|
+
# that order); names that resolve to no project class/module (RBS-known / third-party) are dropped.
|
|
2812
3145
|
def ancestor_class_names(scope, class_name)
|
|
2813
3146
|
names = []
|
|
2814
3147
|
scope.includes_of(class_name).each do |raw|
|
|
@@ -2824,11 +3157,10 @@ module Rigor
|
|
|
2824
3157
|
end
|
|
2825
3158
|
|
|
2826
3159
|
def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
end
|
|
3160
|
+
resolved = scope.ancestor_name_candidates(subclass_qualified, raw_ancestor)
|
|
3161
|
+
.find { |candidate| known_user_class?(scope, candidate) }
|
|
3162
|
+
return resolved if resolved
|
|
3163
|
+
|
|
2832
3164
|
# ADR-46 slice 3 — the override checker reads the class graph
|
|
2833
3165
|
# directly (not through the recorder's `Scope` choke points), and
|
|
2834
3166
|
# short-circuits when the ancestor resolves to no project class, so
|
|
@@ -2971,7 +3303,10 @@ module Rigor
|
|
|
2971
3303
|
return {} if param_names.empty? || param_names.size != args.size
|
|
2972
3304
|
|
|
2973
3305
|
translated = args.map do |arg|
|
|
2974
|
-
Inference::RbsTypeTranslator.translate(
|
|
3306
|
+
Inference::RbsTypeTranslator.translate(
|
|
3307
|
+
arg, self_type: nil, instance_type: nil, type_vars: {},
|
|
3308
|
+
alias_expander: scope.environment&.rbs_loader
|
|
3309
|
+
)
|
|
2975
3310
|
rescue StandardError
|
|
2976
3311
|
nil
|
|
2977
3312
|
end
|
|
@@ -3028,8 +3363,9 @@ module Rigor
|
|
|
3028
3363
|
|
|
3029
3364
|
scope, override_method, parent_class, parent_method = resolved
|
|
3030
3365
|
parent_type_vars = ancestor_instantiation_type_vars(scope, parent_class)
|
|
3031
|
-
override_params = positional_param_types(override_method)
|
|
3032
|
-
parent_params = positional_param_types(parent_method, type_vars: parent_type_vars
|
|
3366
|
+
override_params = positional_param_types(override_method, environment: scope.environment)
|
|
3367
|
+
parent_params = positional_param_types(parent_method, type_vars: parent_type_vars,
|
|
3368
|
+
environment: scope.environment)
|
|
3033
3369
|
return nil if override_params.nil? || parent_params.nil?
|
|
3034
3370
|
|
|
3035
3371
|
index = first_narrowed_param_index(override_params, parent_params)
|
|
@@ -3049,7 +3385,7 @@ module Rigor
|
|
|
3049
3385
|
# map (parent class's type params → the subclass's instantiation) so a parent `(T)` parameter
|
|
3050
3386
|
# is compared at its instantiated type. Empty (the default, and the override side) keeps the
|
|
3051
3387
|
# pre-WD9 degrade-to-`Dynamic[Top]` behaviour.
|
|
3052
|
-
def positional_param_types(method_def, type_vars: {})
|
|
3388
|
+
def positional_param_types(method_def, type_vars: {}, environment: nil)
|
|
3053
3389
|
method_types = method_def.method_types
|
|
3054
3390
|
return nil unless method_types.size == 1
|
|
3055
3391
|
|
|
@@ -3058,7 +3394,8 @@ module Rigor
|
|
|
3058
3394
|
|
|
3059
3395
|
(func.required_positionals + func.optional_positionals).map do |param|
|
|
3060
3396
|
Inference::RbsTypeTranslator.translate(
|
|
3061
|
-
param.type, self_type: nil, instance_type: nil, type_vars: type_vars
|
|
3397
|
+
param.type, self_type: nil, instance_type: nil, type_vars: type_vars,
|
|
3398
|
+
alias_expander: environment&.rbs_loader
|
|
3062
3399
|
)
|
|
3063
3400
|
rescue StandardError
|
|
3064
3401
|
nil
|
|
@@ -3096,8 +3433,6 @@ module Rigor
|
|
|
3096
3433
|
)
|
|
3097
3434
|
end
|
|
3098
3435
|
end
|
|
3099
|
-
# rubocop:enable Metrics/ClassLength
|
|
3100
3436
|
end
|
|
3101
|
-
# rubocop:enable Metrics/ModuleLength
|
|
3102
3437
|
end
|
|
3103
3438
|
end
|