rigortype 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
|
|
8
|
+
# ({ScopeIndexer::DeclaredParamWrites}), which must not type an ivar a writer the class-ivar census never walks may
|
|
9
|
+
# replace. The standard is parity with a literal write: `@x = 1` in `initialize` already reads `1` in every sibling
|
|
10
|
+
# method whatever an unknown object's code does, so only the writers that literal writes would also have to answer
|
|
11
|
+
# for are recorded, and an unknown-receiver writer records nothing (as `ReflectiveIvarWrite` leaves
|
|
12
|
+
# `o.instance_variable_set` alone). Three entry kinds go into the census `Set`:
|
|
13
|
+
#
|
|
14
|
+
# - `[:ivar_set, name]` — a literal `o.instance_variable_set(:@x, v)` (or the same through a `send` that spells
|
|
15
|
+
# it) on an explicit receiver other than `self`, and an `@x` write in the literal block of an
|
|
16
|
+
# `o.instance_eval { … }` / `instance_exec`: code that names the ivar it writes.
|
|
17
|
+
# - `[:ivar_set_class, class_name]` — an open-world writer ({#open_world?}) with an implicit or `self` receiver
|
|
18
|
+
# inside an instance body of the class — a `def` or a `define_method` block in its class body, in the block of a
|
|
19
|
+
# `K = Struct.new` / `Data.define` / `Class.new` constant, or in a `K.class_eval` block (the host walk passes
|
|
20
|
+
# that class as `owner`): it may write any ivar of the class's instances. A writer in a class body, a
|
|
21
|
+
# `def self.` or at the top level records nothing.
|
|
22
|
+
# - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
|
|
23
|
+
# {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
|
|
24
|
+
# file writes the same ivar of the same class.
|
|
25
|
+
class IvarWriterCensus
|
|
26
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
27
|
+
# The methods a `send` reaching which is an open-world writer.
|
|
28
|
+
OPEN_WORLD_SENDS = %i[instance_variable_set instance_eval instance_exec extend
|
|
29
|
+
define_singleton_method].to_set.freeze
|
|
30
|
+
EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
|
|
31
|
+
# The calls on a `singleton_class` receiver that give the object methods or code the census cannot read.
|
|
32
|
+
SINGLETON_CLASS_CALLS = %i[
|
|
33
|
+
class_eval class_exec module_eval module_exec define_method include prepend extend attr attr_reader
|
|
34
|
+
attr_writer attr_accessor send public_send __send__
|
|
35
|
+
].to_set.freeze
|
|
36
|
+
NO_ARGUMENTS = [].freeze
|
|
37
|
+
|
|
38
|
+
# The call names any rule here reads, so every other call returns at one Set probe.
|
|
39
|
+
# (`define_method` is in {SINGLETON_CLASS_CALLS}.)
|
|
40
|
+
WATCHED_CALLS = (SEND_CALLS | OPEN_WORLD_SENDS | EVAL_CALLS | SINGLETON_CLASS_CALLS).freeze
|
|
41
|
+
|
|
42
|
+
def initialize(census)
|
|
43
|
+
@census = census
|
|
44
|
+
@instance_ranges = []
|
|
45
|
+
@owner_names = {}.compare_by_identity
|
|
46
|
+
@own_writes = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
|
|
50
|
+
# or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
|
|
51
|
+
# instance (`class << self`, the top level, an opaque block).
|
|
52
|
+
def visit(node, owner)
|
|
53
|
+
case node
|
|
54
|
+
when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
|
|
55
|
+
when Prism::SingletonClassNode then visit_singleton_class(node)
|
|
56
|
+
when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
|
|
57
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
58
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
59
|
+
record_own_write(node) unless @instance_ranges.empty?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# One entry per class and ivar: a repeated write costs a Hash probe, not a location and an Array.
|
|
66
|
+
def record_own_write(node)
|
|
67
|
+
owner = instance_owner(node)
|
|
68
|
+
return if owner.nil?
|
|
69
|
+
|
|
70
|
+
seen = (@own_writes[owner] ||= {})
|
|
71
|
+
return if seen[node.name]
|
|
72
|
+
|
|
73
|
+
seen[node.name] = true
|
|
74
|
+
@census << [:own_write, owner_name(owner), node.name].freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
|
|
78
|
+
|
|
79
|
+
def visit_call(node, owner)
|
|
80
|
+
remember(node.block.location, owner) if owner && define_method_block?(node)
|
|
81
|
+
if self_receiver?(node) || own_singleton_class_call?(node)
|
|
82
|
+
return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
|
|
83
|
+
|
|
84
|
+
instance = instance_owner(node)
|
|
85
|
+
record_class(instance) if instance
|
|
86
|
+
else
|
|
87
|
+
visit_foreign(node)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `class << self` inside an instance `def` opens the instance's singleton class.
|
|
92
|
+
def visit_singleton_class(node)
|
|
93
|
+
return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
|
|
94
|
+
|
|
95
|
+
instance = instance_owner(node)
|
|
96
|
+
record_class(instance) if instance
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
|
|
100
|
+
|
|
101
|
+
# The self writers whose target or code the census cannot read: a `send` with a computed method name or one
|
|
102
|
+
# naming {OPEN_WORLD_SENDS}; `instance_eval` / `instance_exec` given a block argument, and `instance_eval`
|
|
103
|
+
# given a string; `extend`; `define_singleton_method` with a body; and an `instance_variable_set` whose name no
|
|
104
|
+
# literal spells. A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` counts as well.
|
|
105
|
+
def open_world?(node)
|
|
106
|
+
name = node.name
|
|
107
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
108
|
+
return open_world_send?(arguments) if SEND_CALLS.include?(name)
|
|
109
|
+
return open_world_eval?(node, arguments) if EVAL_CALLS.include?(name)
|
|
110
|
+
# #1931's census already makes every ivar of the class gradual for one in the same file; this entry covers one
|
|
111
|
+
# in a reopening in another file, which that census does not see.
|
|
112
|
+
return ivar_name(arguments.first).nil? if name == :instance_variable_set
|
|
113
|
+
|
|
114
|
+
name == :extend || (name == :define_singleton_method && (!node.block.nil? || arguments.size > 1))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def open_world_send?(arguments)
|
|
118
|
+
return false if arguments.empty?
|
|
119
|
+
|
|
120
|
+
sent = literal_name(arguments.first)
|
|
121
|
+
sent.nil? || OPEN_WORLD_SENDS.include?(sent)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def open_world_eval?(node, arguments)
|
|
125
|
+
node.block.is_a?(Prism::BlockArgumentNode) || (node.name == :instance_eval && !arguments.empty?)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A call on another receiver: a named foreign write, or `self.singleton_class.class_eval` and its kin.
|
|
129
|
+
def visit_foreign(node)
|
|
130
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
131
|
+
name = node.name
|
|
132
|
+
if SEND_CALLS.include?(name) && arguments.first && literal_name(arguments.first) == :instance_variable_set
|
|
133
|
+
name = :instance_variable_set
|
|
134
|
+
arguments = arguments.drop(1)
|
|
135
|
+
end
|
|
136
|
+
if name == :instance_variable_set
|
|
137
|
+
ivar = ivar_name(arguments.first)
|
|
138
|
+
@census << [:ivar_set, ivar].freeze if ivar
|
|
139
|
+
elsif EVAL_CALLS.include?(name) && node.block.is_a?(Prism::BlockNode)
|
|
140
|
+
record_block_ivar_writes(node.block)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def record_block_ivar_writes(node)
|
|
145
|
+
return unless node.is_a?(Prism::Node)
|
|
146
|
+
return if node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
147
|
+
|
|
148
|
+
case node
|
|
149
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
150
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
151
|
+
@census << [:ivar_set, node.name].freeze
|
|
152
|
+
end
|
|
153
|
+
node.rigor_each_child { |child| record_block_ivar_writes(child) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def define_method_block?(node)
|
|
157
|
+
node.name == :define_method && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# An instance body (`def` or `define_method` block) and the class it belongs to. A `define_method` block's own
|
|
161
|
+
# nodes may reach the walk with no owner (the walk reads its `self` as opaque), so the class rides the range.
|
|
162
|
+
def remember(location, owner)
|
|
163
|
+
@instance_ranges << [(location.start_offset...location.end_offset), owner]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The class of the innermost instance body covering `node` (pre-order, so the last covering range), or nil.
|
|
167
|
+
def instance_owner(node)
|
|
168
|
+
offset = node.location.start_offset
|
|
169
|
+
@instance_ranges.reverse_each { |(range, owner)| return owner if range.cover?(offset) }
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
174
|
+
|
|
175
|
+
# A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` (`singleton_class.class_eval { … }`).
|
|
176
|
+
def own_singleton_class_call?(node)
|
|
177
|
+
receiver = node.receiver
|
|
178
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver) &&
|
|
179
|
+
SINGLETON_CLASS_CALLS.include?(node.name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def literal_name(argument)
|
|
183
|
+
return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
184
|
+
|
|
185
|
+
text = argument.unescaped
|
|
186
|
+
text.to_sym if text.valid_encoding?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def ivar_name(argument)
|
|
190
|
+
name = literal_name(argument)
|
|
191
|
+
name if name.to_s.start_with?("@")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# The value the statement evaluator last gave a loop node, and the scope it evaluated the loop from, so
|
|
6
|
+
# `ExpressionTyper#type_of_loop` reads it instead of evaluating the loop again (issue #2031 review). A loop's value
|
|
7
|
+
# costs an evaluation of its body, its fixpoint and its `break` passes included, and the typer types a loop again
|
|
8
|
+
# wherever it types a node that holds one: the value of a write that wraps it (`id(z = (while …))`), a call it is
|
|
9
|
+
# the receiver of (`(while …).itself`), each narrowing and rule that types that operand. Each such typing of a
|
|
10
|
+
# loop nested in another loop re-ran the inner loop's passes, once more per level of nesting.
|
|
11
|
+
#
|
|
12
|
+
# The record is reused only for the very scope object the loop was evaluated from, so it answers exactly what an
|
|
13
|
+
# evaluation would. It is not compared with `Scope#==`, which ignores several fields (`match_frame`,
|
|
14
|
+
# `declared_refinements`, `struct_fold_safe_locals`, the opaque-block flags, `lexical_nesting`, the origins tables,
|
|
15
|
+
# `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
|
|
16
|
+
# all (issue #2262). It lives in a per-thread map whose keys are held weakly, so a record goes when its parse tree
|
|
17
|
+
# does.
|
|
18
|
+
module LoopValueRecord
|
|
19
|
+
KEY = :rigor_loop_value_record
|
|
20
|
+
private_constant :KEY
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
COUNT_KEY = :rigor_loop_value_count
|
|
25
|
+
private_constant :COUNT_KEY
|
|
26
|
+
|
|
27
|
+
def record(node, scope, type)
|
|
28
|
+
Thread.current[COUNT_KEY] = loops_evaluated + 1
|
|
29
|
+
conditional(node, scope, type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
|
|
33
|
+
def loops_evaluated = Thread.current[COUNT_KEY] || 0
|
|
34
|
+
|
|
35
|
+
# The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
|
|
36
|
+
# conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
|
|
37
|
+
# branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
|
|
38
|
+
def conditional(node, scope, type)
|
|
39
|
+
(Thread.current[KEY] ||= ObjectSpace::WeakKeyMap.new)[node] = [scope, type]
|
|
40
|
+
type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The recorded value of `node` evaluated from `scope` itself, or nil.
|
|
44
|
+
def fetch(node, scope)
|
|
45
|
+
slot = Thread.current[KEY]&.[](node)
|
|
46
|
+
return nil if slot.nil?
|
|
47
|
+
|
|
48
|
+
recorded, type = slot
|
|
49
|
+
recorded.equal?(scope) ? type : nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -47,6 +47,25 @@ module Rigor
|
|
|
47
47
|
# same call, another plugin's included, is not consulted. A `:lexical` entry (issue #1667) keeps the caller's
|
|
48
48
|
# `self`, so its `self_type` is the calling scope's.
|
|
49
49
|
def match_for(scope:, call_node:, receiver_type:)
|
|
50
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
51
|
+
return nil if entry.nil?
|
|
52
|
+
|
|
53
|
+
receiver_name = singleton_receiver_class_name(receiver_type) || nominal_receiver_class_name(receiver_type)
|
|
54
|
+
entry_match(entry, receiver_name, scope, scope.environment)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether `call_node`'s block runs as an instance method of the receiver class (issue #1768): its first matching
|
|
58
|
+
# entry binds `:receiver_instance`, so the block body's `self` is the instance {#match_for} narrows to and its
|
|
59
|
+
# `@x` writes are that instance's ivars, as a `def` body's are. A `:lexical` or named-`self_type` entry runs the
|
|
60
|
+
# block on another object, and no entry means no narrowing.
|
|
61
|
+
def runs_as_receiver_instance_method?(scope:, call_node:, receiver_type:)
|
|
62
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
63
|
+
!entry.nil? && entry.self_type == Plugin::Macro::BlockAsMethod::SELF_TYPE_RECEIVER_INSTANCE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The first registered entry that matches `call_node` on `receiver_type`, in (plugin registration, declaration)
|
|
67
|
+
# order, or nil.
|
|
68
|
+
def first_matching_entry(scope, call_node, receiver_type)
|
|
50
69
|
return nil if receiver_type.nil?
|
|
51
70
|
|
|
52
71
|
environment = scope&.environment
|
|
@@ -61,12 +80,7 @@ module Rigor
|
|
|
61
80
|
# (plugin registration, declaration) order; the method-name membership is guaranteed
|
|
62
81
|
# by the table key.
|
|
63
82
|
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
64
|
-
entries.
|
|
65
|
-
next unless entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
66
|
-
|
|
67
|
-
return entry_match(entry, singleton_name || nominal_name, scope, environment)
|
|
68
|
-
end
|
|
69
|
-
nil
|
|
83
|
+
entries.find { |entry| entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment) }
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
# The {Match} a matching entry contributes: a `:lexical` one keeps the calling scope's `self`, unknown where the
|
|
@@ -88,6 +88,18 @@ module Rigor
|
|
|
88
88
|
@forwarded_block == name
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Issue #1157 — whether a call's `&expr` block argument hands on the block THIS method was called with: the
|
|
92
|
+
# anonymous `&` of a method that declares an anonymous `&` parameter, or {#forwarded_block?} for `&name`.
|
|
93
|
+
# Anything else (`&:sym`, a proc made here, a rebound name) is a block of this frame's own making.
|
|
94
|
+
def forwards_block?(block_argument)
|
|
95
|
+
expression = block_argument.expression
|
|
96
|
+
case expression
|
|
97
|
+
when nil then anonymous_block_parameter?
|
|
98
|
+
when Prism::LocalVariableReadNode then forwarded_block?(expression.name)
|
|
99
|
+
else false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
# The scan of `node` under `scope`, kept while `scope`'s local and instance-variable tables are the same
|
|
92
104
|
# objects: a lookup argument's answer reads them ({MatchRebinding.may_match?}), and a rebuild that leaves
|
|
93
105
|
# them alone passes the same tables on. `kind` keeps another reading of the same node apart (issue #1365:
|
|
@@ -125,6 +137,13 @@ module Rigor
|
|
|
125
137
|
name
|
|
126
138
|
end
|
|
127
139
|
|
|
140
|
+
# True when the method declares a bare `&` parameter, which only an anonymous `&` argument can read.
|
|
141
|
+
def anonymous_block_parameter?
|
|
142
|
+
parameters = @parameters
|
|
143
|
+
block = parameters.is_a?(Prism::ParametersNode) ? parameters.block : nil
|
|
144
|
+
!block.nil? && block.name.nil?
|
|
145
|
+
end
|
|
146
|
+
|
|
128
147
|
# The method's own `&block` parameter's name, or nil.
|
|
129
148
|
def block_parameter_name
|
|
130
149
|
parameters = @parameters
|