rigortype 0.4.3 → 0.4.5
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/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -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/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +120 -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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -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 +56 -8
- 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 +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- 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 +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- 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 +1 -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-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-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-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -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 +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- 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/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -28,7 +28,10 @@ module Rigor
|
|
|
28
28
|
next [] if use.name.nil?
|
|
29
29
|
|
|
30
30
|
name = use.name.delete_prefix("::")
|
|
31
|
-
targets = use.
|
|
31
|
+
targets = if use.anywhere then anywhere_targets(name)
|
|
32
|
+
elsif use.within then literal_targets(use, name)
|
|
33
|
+
else [nil]
|
|
34
|
+
end
|
|
32
35
|
targets.map do |target|
|
|
33
36
|
Scan::Reference.new(as_written: target || name, nesting: [].freeze, from: nil,
|
|
34
37
|
role: use.role, path: use.path, line: use.line,
|
|
@@ -50,11 +53,103 @@ module Rigor
|
|
|
50
53
|
return [nil] if receiver.nil?
|
|
51
54
|
|
|
52
55
|
local = @shadows[use.path]
|
|
56
|
+
runtime = use.receiver_open ? runtime_targets(receiver, name, use.inherit, local) : []
|
|
53
57
|
own = declared_member(receiver, name, local)
|
|
54
|
-
return [own] if own
|
|
55
|
-
return
|
|
58
|
+
return [own, *runtime].uniq if own
|
|
59
|
+
return runtime unless use.inherit
|
|
60
|
+
return (anywhere_targets(name) + runtime).uniq if unknown_ancestry?(receiver)
|
|
56
61
|
|
|
57
|
-
plausible_ancestors(receiver).filter_map { |scope| declared_member(scope, name, local) }
|
|
62
|
+
(plausible_ancestors(receiver).filter_map { |scope| declared_member(scope, name, local) } + runtime)
|
|
63
|
+
.uniq + [nil]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every declaration a literal `name` can name from a receiver the walk cannot bound (#1946): each one a
|
|
67
|
+
# scope's `name` (or a declared prefix of it) could be, and the top level.
|
|
68
|
+
def anywhere_targets(name)
|
|
69
|
+
segments = name.split("::")
|
|
70
|
+
found = segments.length.downto(1).flat_map do |count|
|
|
71
|
+
suffix = segments.first(count).join("::")
|
|
72
|
+
by_last_segment.fetch(segments[count - 1], []).select do |fqn|
|
|
73
|
+
fqn == suffix || fqn.end_with?("::#{suffix}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
found.uniq + [nil]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def by_last_segment
|
|
80
|
+
@by_last_segment ||= @by_fqn.keys.group_by { |fqn| fqn.split("::").last }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The declarations a descendant of `receiver` holding `name` adds (#1878): a `self` standing for the
|
|
84
|
+
# enclosing declaration is whichever subclass, includer, prepender or extender the method runs on, and
|
|
85
|
+
# Ruby looks the name up there first. Each descendant is searched with its own ancestors unless the call
|
|
86
|
+
# passes `inherit = false`. The descendants are over-approximated as {#plausible_ancestors} is.
|
|
87
|
+
#
|
|
88
|
+
# A module some unknown receiver includes or extends (`k.include(Mix)`, an `include` in an `included do`
|
|
89
|
+
# block) has descendants the walk cannot list, so the name may be any declaration's (#1946). A class
|
|
90
|
+
# with an ancestor the walk cannot name (`K.include(mods.first)`) may be a descendant of any module, so
|
|
91
|
+
# it is searched too.
|
|
92
|
+
def runtime_targets(receiver, name, inherit, local)
|
|
93
|
+
return anywhere_targets(name) if open_descendants?(receiver)
|
|
94
|
+
|
|
95
|
+
(descendants_of(receiver) | unknown_ancestry_classes).flat_map do |descendant|
|
|
96
|
+
scopes = inherit ? [descendant, *plausible_ancestors(descendant)] : [descendant]
|
|
97
|
+
scopes.filter_map { |scope| declared_member(scope, name, local) }
|
|
98
|
+
end.uniq
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def descendants_of(fqn)
|
|
102
|
+
@plausible_descendants ||= (@by_fqn.keys | ancestry.edges.keys)
|
|
103
|
+
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |declared, out|
|
|
104
|
+
plausible_ancestors(declared).each { |ancestor| out[ancestor] << declared }
|
|
105
|
+
end
|
|
106
|
+
@plausible_descendants.fetch(fqn, [])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def open_descendants?(fqn)
|
|
110
|
+
ancestry.open.include?(fqn) || ancestry.open.any? { |mod| plausible_ancestors(mod).include?(fqn) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Whether `fqn` or one of its ancestors has an ancestor the walk cannot name, which may declare anything.
|
|
114
|
+
def unknown_ancestry?(fqn)
|
|
115
|
+
([fqn] + plausible_ancestors(fqn)).any? { |scope| ancestry.unknown.include?(scope) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def unknown_ancestry_classes
|
|
119
|
+
@unknown_ancestry_classes ||= ancestry.unknown.flat_map { |fqn| [fqn, *descendants_of(fqn)] }.uniq
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The {Scan::Ancestry} edges read (#1946): `edges` maps a class to the modules and superclass it gains
|
|
123
|
+
# outside its declaration; `open` holds the modules an unknown receiver gains, and `unknown` the classes
|
|
124
|
+
# that gain an ancestor the walk cannot name. A target is every scope its written name could be, declared
|
|
125
|
+
# or not (a gem class reopened as `class Foo::X` declares `Foo::X` alone); an ancestor, every scope that
|
|
126
|
+
# declares it.
|
|
127
|
+
AncestryIndex = Data.define(:edges, :open, :unknown)
|
|
128
|
+
private_constant :AncestryIndex
|
|
129
|
+
|
|
130
|
+
def ancestry
|
|
131
|
+
@ancestry ||= begin
|
|
132
|
+
edges = Hash.new { |h, k| h[k] = [] }
|
|
133
|
+
open = Set.new
|
|
134
|
+
unknown = Set.new
|
|
135
|
+
@ancestries.each do |edge|
|
|
136
|
+
targets = edge.target && written_scopes(edge.target)
|
|
137
|
+
ancestors = edge.ancestor && written_scopes(edge.ancestor).select { |fqn| @by_fqn.key?(fqn) }
|
|
138
|
+
if targets.nil? then open.merge(ancestors)
|
|
139
|
+
elsif ancestors.nil? then unknown.merge(targets)
|
|
140
|
+
else targets.each { |target| edges[target].concat(ancestors - [target]) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
AncestryIndex.new(edges: edges, open: open, unknown: unknown)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Every scope a written reference could name: rooted, itself; otherwise its name under each enclosing scope.
|
|
148
|
+
def written_scopes(ref)
|
|
149
|
+
name = ref.as_written.delete_prefix("::")
|
|
150
|
+
return [name] if ref.rooted
|
|
151
|
+
|
|
152
|
+
ref.nesting.length.downto(0).map { |i| (ref.nesting.first(i) + [name]).join("::") }
|
|
58
153
|
end
|
|
59
154
|
|
|
60
155
|
# `scope::name`, or the longest declared prefix of it, when one is declared.
|
|
@@ -79,16 +174,15 @@ module Rigor
|
|
|
79
174
|
out = []
|
|
80
175
|
until queue.empty?
|
|
81
176
|
current = queue.shift
|
|
82
|
-
@by_fqn.fetch(current, []).
|
|
83
|
-
(decl.includes + [decl.superclass]).compact.
|
|
84
|
-
|
|
85
|
-
|
|
177
|
+
resolved = @by_fqn.fetch(current, []).flat_map do |decl|
|
|
178
|
+
(decl.includes + [decl.superclass]).compact.flat_map { |raw| plausible_resolutions(current, raw) }
|
|
179
|
+
end
|
|
180
|
+
(resolved + ancestry.edges.fetch(current, [])).each do |ancestor|
|
|
181
|
+
next if seen.include?(ancestor)
|
|
86
182
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
end
|
|
91
|
-
end
|
|
183
|
+
seen << ancestor
|
|
184
|
+
out << ancestor
|
|
185
|
+
queue << ancestor
|
|
92
186
|
end
|
|
93
187
|
end
|
|
94
188
|
out.freeze
|
|
@@ -154,9 +248,12 @@ module Rigor
|
|
|
154
248
|
# A literal-argument site contributes a real reference; a dynamic one taints a namespace (WD4).
|
|
155
249
|
# @param shadows — declarations made in files outside the analysed paths. They are never nodes, but a
|
|
156
250
|
# reference written in the SAME file resolves to them first, as Ruby would (#1732, see {#resolve_ref}).
|
|
157
|
-
|
|
251
|
+
# @param ancestries — {Scan::Ancestry} edges written outside a declaration, read only by the dynamic
|
|
252
|
+
# lookup (#1946).
|
|
253
|
+
def initialize(declarations:, references:, root_fqns: [], dynamic_uses: [], shadows: [], ancestries: [],
|
|
158
254
|
foreign: ->(_fqn) { false })
|
|
159
255
|
@declarations = declarations
|
|
256
|
+
@ancestries = ancestries
|
|
160
257
|
@root_fqns = root_fqns.to_set
|
|
161
258
|
@foreign = foreign
|
|
162
259
|
@by_fqn = declarations.group_by(&:fqn)
|
|
@@ -57,8 +57,21 @@ module Rigor
|
|
|
57
57
|
# matching. A literal `name` keeps `within` too, and {Graph} looks it up from there (#1761). `inherit`
|
|
58
58
|
# is false when the call passes `inherit = false`, which confines the lookup to the receiver itself;
|
|
59
59
|
# otherwise the receiver's ancestors are searched as well. `role` is the referring file's (WD8).
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
#
|
|
61
|
+
# `receiver_open` is true when `within` is the enclosing declaration standing for a runtime `self` that
|
|
62
|
+
# may be one of its descendants (#1878): a `def self.x = const_get(:X)` runs on whichever subclass it
|
|
63
|
+
# is called on, and a module's method on whichever class includes, prepends or extends it. {Graph}
|
|
64
|
+
# then looks the literal name up in those descendants as well.
|
|
65
|
+
#
|
|
66
|
+
# `anywhere` is true when the receiver is a `self` the walk cannot name at all — the block of an eval call
|
|
67
|
+
# on a local or another computed receiver (`k.class_eval { const_get(:E) }`, #1946). {Graph} then keeps
|
|
68
|
+
# every declaration the literal could name.
|
|
69
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role,
|
|
70
|
+
:receiver_open, :anywhere) do
|
|
71
|
+
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false,
|
|
72
|
+
anywhere: false, **)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
62
75
|
|
|
63
76
|
# Whether this site's evidence reaches `fqn`.
|
|
64
77
|
def taints?(fqn)
|
|
@@ -94,7 +107,18 @@ module Rigor
|
|
|
94
107
|
end
|
|
95
108
|
end
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
# An ancestry edge written outside a declaration's header and body statements (#1946): `K.include(Mix)`,
|
|
111
|
+
# `K.send(:include, Mix)` and the `prepend` / `extend` / `public_send` / `__send__` forms, an `include`
|
|
112
|
+
# in an eval block on `K` or nested in a conditional, and the superclass of `K = Class.new(Base)`. `target`
|
|
113
|
+
# is the class the module joins, `ancestor` the module (or superclass); each is a {Reference} resolved in
|
|
114
|
+
# the call's nesting, or nil when it is not a constant the walk can name — a local receiver, an
|
|
115
|
+
# `include` whose `self` is unknown, or a computed argument (`K.include(mods.first)`). {Graph} reads
|
|
116
|
+
# these only to widen a `const_get` on `self`'s reach, never for ordinary constant resolution.
|
|
117
|
+
Ancestry = Data.define(:target, :ancestor, :path, :line)
|
|
118
|
+
|
|
119
|
+
Result = Data.define(:declarations, :references, :dynamic_uses, :ancestries) do
|
|
120
|
+
def initialize(ancestries: [].freeze, **) = super
|
|
121
|
+
end
|
|
98
122
|
|
|
99
123
|
# Roles a referring file can have (ADR-102 WD8). A reference edge carries its referrer's role so
|
|
100
124
|
# "used only by its own test" is a reportable category rather than a bucket boundary.
|
|
@@ -155,7 +179,7 @@ module Rigor
|
|
|
155
179
|
walker = Walker.new(path: path, role: role || role_for(path, root: root))
|
|
156
180
|
walker.walk(parsed.value, [])
|
|
157
181
|
Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
|
|
158
|
-
dynamic_uses: walker.dynamic_uses.freeze)
|
|
182
|
+
dynamic_uses: walker.dynamic_uses.freeze, ancestries: walker.ancestries.freeze)
|
|
159
183
|
end
|
|
160
184
|
|
|
161
185
|
# The values a local variable can hold, read off its assignments in one local scope, for a
|
|
@@ -251,10 +275,51 @@ module Rigor
|
|
|
251
275
|
module_exec instance_eval instance_exec].freeze
|
|
252
276
|
private_constant :SELF_REBINDING
|
|
253
277
|
|
|
278
|
+
# The `*_eval` / `*_exec` family: on a constant receiver, the block's `self` IS that constant (#1878).
|
|
279
|
+
EVAL_FAMILY = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
280
|
+
private_constant :EVAL_FAMILY
|
|
281
|
+
|
|
282
|
+
# Calls that define a method from their block: `define_method`'s block is an instance method's body,
|
|
283
|
+
# `define_singleton_method`'s a singleton method's (#1946).
|
|
284
|
+
METHOD_DEFINERS = { define_method: :instance, define_singleton_method: :singleton }.freeze
|
|
285
|
+
private_constant :METHOD_DEFINERS
|
|
286
|
+
|
|
254
287
|
# A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
|
|
255
288
|
def record_call(node, nesting)
|
|
256
289
|
record_dynamic_use(node, nesting)
|
|
257
|
-
|
|
290
|
+
record_mixin(node, nesting)
|
|
291
|
+
return unless node.block.is_a?(Prism::BlockNode)
|
|
292
|
+
|
|
293
|
+
classify_block(node, nesting)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def classify_block(node, nesting)
|
|
297
|
+
block = node.block
|
|
298
|
+
if rebinds_self?(node)
|
|
299
|
+
@rebinding_blocks << block
|
|
300
|
+
frame = eval_frame(node, nesting)
|
|
301
|
+
@eval_frames[block] = frame if frame
|
|
302
|
+
elsif meta_new_call?(node)
|
|
303
|
+
@meta_new_blocks << block
|
|
304
|
+
elsif METHOD_DEFINERS.key?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
305
|
+
@definer_blocks[block] = METHOD_DEFINERS.fetch(node.name)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The `self` of an eval call's block (#1878, #1946): the receiver when it is a constant, `:unknown` when
|
|
310
|
+
# it is a local or another computed value, and nil — the reading the walk had before — on `self`,
|
|
311
|
+
# `self.class`, an implicit receiver or `Object`. `singleton_defs` is whether a plain `def` in the block
|
|
312
|
+
# defines a singleton method of the receiver (`instance_eval`) rather than an instance method.
|
|
313
|
+
def eval_frame(call, nesting)
|
|
314
|
+
return anonymous_subclass_frame(call, nesting) if call.name == :new
|
|
315
|
+
return nil unless EVAL_FAMILY.include?(call.name)
|
|
316
|
+
|
|
317
|
+
singleton_defs = %i[instance_eval instance_exec].include?(call.name)
|
|
318
|
+
receiver = call.receiver
|
|
319
|
+
return EvalFrame.new(receiver_reference(receiver, nesting), singleton_defs) if constant_receiver?(receiver)
|
|
320
|
+
return nil if receiver.nil? || self_receiver?(receiver) || constant_path?(receiver)
|
|
321
|
+
|
|
322
|
+
EvalFrame.new(:unknown, singleton_defs)
|
|
258
323
|
end
|
|
259
324
|
|
|
260
325
|
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
@@ -287,9 +352,11 @@ module Rigor
|
|
|
287
352
|
else []
|
|
288
353
|
end
|
|
289
354
|
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
355
|
+
record_anywhere(node, name, reason)
|
|
290
356
|
bounds.each do |prefix, scope, within|
|
|
291
357
|
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
292
358
|
inherit: anchors.include?(TOP_LEVEL), role: @role,
|
|
359
|
+
receiver_open: !within.nil? && runtime_self?(node),
|
|
293
360
|
site: site(node), path: @path, line: node.location.start_line,
|
|
294
361
|
reason: reason)
|
|
295
362
|
end
|
|
@@ -318,12 +385,55 @@ module Rigor
|
|
|
318
385
|
end
|
|
319
386
|
end
|
|
320
387
|
|
|
388
|
+
# A literal `const_get` on a `self` the walk cannot name at all reaches any declaration of that name.
|
|
389
|
+
def record_anywhere(node, name, reason)
|
|
390
|
+
return unless name && !name.start_with?("::") && unknown_self?(node)
|
|
391
|
+
|
|
392
|
+
@dynamic_uses << DynamicUse.new(name: name, prefix: nil, anywhere: true, role: @role, site: site(node),
|
|
393
|
+
path: @path, line: node.location.start_line, reason: reason)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Whether a `const_get` on `self` can run on a descendant of the enclosing declaration (#1878): in a
|
|
397
|
+
# method, or a block that rebinds `self`, `self` is whichever subclass or includer the method was
|
|
398
|
+
# called on. Directly in a class body, or inside `Other.class_eval { }`, it is the declaration itself.
|
|
399
|
+
def runtime_self?(node)
|
|
400
|
+
node.name == :const_get && self_receiver?(node.receiver) && @eval_frame.nil? &&
|
|
401
|
+
!(@self_kind == :declaration && !@in_block && !@self_rebound)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Whether a `const_get` on `self` runs on a receiver the walk cannot name at all (#1946): directly in the
|
|
405
|
+
# block of an eval call on a computed receiver, or in a method that block defines when the receiver form
|
|
406
|
+
# names the eval receiver ({#method_receiver?}).
|
|
407
|
+
def unknown_self?(node)
|
|
408
|
+
return false unless node.name == :const_get && self_receiver?(node.receiver)
|
|
409
|
+
return @eval_frame.receiver == :unknown && !node.receiver.is_a?(Prism::CallNode) if @eval_frame
|
|
410
|
+
|
|
411
|
+
!@eval_method.nil? && @eval_method.receiver == :unknown && method_receiver?(node.receiver)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Whether `receiver` names the eval receiver in a method an eval block defines: `self.class` in an
|
|
415
|
+
# instance method (`self` is an instance of the receiver or of a descendant), an implicit receiver or
|
|
416
|
+
# `self` in a singleton method. An implicit `const_get` in an instance method raises `NoMethodError`.
|
|
417
|
+
def method_receiver?(receiver)
|
|
418
|
+
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
419
|
+
@eval_method.kind == :instance ? class_of_self : !class_of_self
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Inside a method an eval block on a constant defines, the receiver form that names that constant reads
|
|
423
|
+
# it as a runtime `self` that may be any of its descendants (#1946).
|
|
424
|
+
def eval_method_references(receiver)
|
|
425
|
+
return [] unless @eval_method&.receiver.is_a?(Reference) && method_receiver?(receiver)
|
|
426
|
+
|
|
427
|
+
[@eval_method.receiver]
|
|
428
|
+
end
|
|
429
|
+
|
|
321
430
|
# Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
|
|
322
431
|
# when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
|
|
323
432
|
# `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
|
|
324
433
|
# evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
|
|
325
434
|
# tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
|
|
326
435
|
def self_lookup(receiver)
|
|
436
|
+
return :declaration if @eval_frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
|
|
327
437
|
return :unknown if @in_block || @self_rebound
|
|
328
438
|
|
|
329
439
|
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
@@ -345,7 +455,7 @@ module Rigor
|
|
|
345
455
|
receiver = node.receiver
|
|
346
456
|
namespaces =
|
|
347
457
|
if self_receiver?(receiver)
|
|
348
|
-
self_references(node, nesting)
|
|
458
|
+
eval_self_references(receiver) || (self_references(node, nesting) + eval_method_references(receiver))
|
|
349
459
|
elsif constant_receiver?(receiver)
|
|
350
460
|
[receiver_reference(receiver, nesting)]
|
|
351
461
|
else
|
|
@@ -357,6 +467,12 @@ module Rigor
|
|
|
357
467
|
inherit.is_a?(Prism::FalseNode) ? namespaces : namespaces + [TOP_LEVEL]
|
|
358
468
|
end
|
|
359
469
|
|
|
470
|
+
# Inside `Other.class_eval { }` an implicit or `self` receiver is `Other` (#1878).
|
|
471
|
+
def eval_self_references(receiver)
|
|
472
|
+
frame = @eval_frame
|
|
473
|
+
[frame.receiver] if frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
|
|
474
|
+
end
|
|
475
|
+
|
|
360
476
|
# The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
|
|
361
477
|
# receiver and can search its ancestors. Inside a meta-new rvalue `self` is the declared class in its
|
|
362
478
|
# block but the outer scope in its arguments; the walk does not tell the two apart, so both are taken.
|
|
@@ -373,9 +489,13 @@ module Rigor
|
|
|
373
489
|
def rebinds_self?(call)
|
|
374
490
|
return true if SELF_REBINDING.include?(call.name)
|
|
375
491
|
|
|
492
|
+
@owner.nil? && meta_new_call?(call)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def meta_new_call?(call)
|
|
376
496
|
recv = call.receiver
|
|
377
497
|
name = recv && Source::ConstantPath.qualified_name_or_nil(recv)&.delete_prefix("::")
|
|
378
|
-
|
|
498
|
+
!name.nil? && META_NEW[name] == call.name
|
|
379
499
|
end
|
|
380
500
|
|
|
381
501
|
def self_receiver?(receiver)
|
|
@@ -456,11 +576,127 @@ module Rigor
|
|
|
456
576
|
end
|
|
457
577
|
end
|
|
458
578
|
|
|
579
|
+
# #1946 — the {Walker}'s reading of ancestry written outside a declaration's own statements, as
|
|
580
|
+
# {Ancestry} edges: receiver-form and `send`-spelled mixin calls, a mixin call whose `self` is not the
|
|
581
|
+
# enclosing declaration body, and the superclass of `K = Class.new(Base)`.
|
|
582
|
+
module AncestrySites
|
|
583
|
+
private
|
|
584
|
+
|
|
585
|
+
# An anonymous `Class.new(Base) { }` block's `self` is a subclass of `Base` that declares no constant of
|
|
586
|
+
# its own, so a `const_get` on it is read at `Base` (#1946).
|
|
587
|
+
def anonymous_subclass_frame(call, nesting)
|
|
588
|
+
superclass = call.arguments&.arguments&.first
|
|
589
|
+
return nil unless call.receiver.is_a?(Prism::ConstantReadNode) && call.receiver.name == :Class &&
|
|
590
|
+
constant_receiver?(superclass)
|
|
591
|
+
|
|
592
|
+
EvalFrame.new(receiver_reference(superclass, nesting), false)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def constant_path?(node)
|
|
596
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Mixin calls: the ones a class body writes, and their `send` spellings.
|
|
600
|
+
MIXINS = %i[include prepend extend].freeze
|
|
601
|
+
private_constant :MIXINS
|
|
602
|
+
|
|
603
|
+
SENDS = %i[send public_send __send__].freeze
|
|
604
|
+
private_constant :SENDS
|
|
605
|
+
|
|
606
|
+
# Records an {Ancestry} for a mixin call the declaration's own statements do not carry (#1946). A
|
|
607
|
+
# receiver-less `include` directly in a class body is already one of its `includes` when every argument
|
|
608
|
+
# is a constant; a computed one (`include Mods.pick`) is an ancestor the walk cannot name. A `self`
|
|
609
|
+
# argument is the `self` the call is written in (`Foo.include(self)` in `module Mix`), not the receiver.
|
|
610
|
+
def record_mixin(node, nesting)
|
|
611
|
+
modules = mixin_arguments(node)
|
|
612
|
+
return if modules.nil? || modules.empty?
|
|
613
|
+
return if node.receiver.nil? && !SENDS.include?(node.name) && @body_statements.include?(node) &&
|
|
614
|
+
modules.all? { |arg| mixin_reference(arg, nesting) }
|
|
615
|
+
|
|
616
|
+
target = mixin_target(node, nesting)
|
|
617
|
+
modules.each do |arg|
|
|
618
|
+
ancestor = arg.is_a?(Prism::SelfNode) ? self_target(node, nesting) : mixin_reference(arg, nesting)
|
|
619
|
+
next if target.nil? && ancestor.nil?
|
|
620
|
+
|
|
621
|
+
@ancestries << Ancestry.new(target: target, ancestor: ancestor, path: @path,
|
|
622
|
+
line: node.location.start_line)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# The module arguments of a mixin call, or nil when it is not one.
|
|
627
|
+
def mixin_arguments(node)
|
|
628
|
+
args = node.arguments&.arguments || []
|
|
629
|
+
return args if MIXINS.include?(node.name)
|
|
630
|
+
return nil unless SENDS.include?(node.name)
|
|
631
|
+
|
|
632
|
+
verb = args.first
|
|
633
|
+
name = verb.unescaped if verb.is_a?(Prism::SymbolNode) || verb.is_a?(Prism::StringNode)
|
|
634
|
+
# Compared as strings: `to_sym` raises on a literal that is not valid in its encoding (`send("\xFF")`).
|
|
635
|
+
MIXINS.any? { |mixin| mixin.name == name } ? args.drop(1) : nil
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def mixin_reference(arg, nesting)
|
|
639
|
+
return nil unless constant_path?(arg) && Source::ConstantPath.qualified_name_or_nil(arg)
|
|
640
|
+
|
|
641
|
+
receiver_reference(arg, nesting)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# The class a mixin call adds to: a constant receiver, or for `self` and an implicit receiver whatever
|
|
645
|
+
# `self` is — the eval receiver in an eval block, the enclosing declaration in its body. Anything else,
|
|
646
|
+
# the top level (`Object`, whose descendants are every class) included, is unknown (nil).
|
|
647
|
+
def mixin_target(node, nesting)
|
|
648
|
+
receiver = node.receiver
|
|
649
|
+
return mixin_reference(receiver, nesting) unless receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
650
|
+
|
|
651
|
+
self_target(node, nesting)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# What `self` is at `node` as a {Reference}, where the walk can name it; nil otherwise.
|
|
655
|
+
def self_target(node, nesting)
|
|
656
|
+
frame = @eval_frame
|
|
657
|
+
return frame&.receiver.is_a?(Reference) ? frame.receiver : nil if frame || @eval_method
|
|
658
|
+
return nil if @in_block || @self_rebound || @self_kind != :declaration || nesting.empty?
|
|
659
|
+
|
|
660
|
+
rooted_reference(nesting.join("::"), node)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def rooted_reference(fqn, node)
|
|
664
|
+
Reference.new(as_written: fqn, nesting: [].freeze, from: nil, role: @role, path: @path,
|
|
665
|
+
line: node.location.start_line, rooted: true)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# `K = Class.new(Base)` makes `Base` an ancestor of `K` (#1946). It stays out of the declaration's
|
|
669
|
+
# `superclass`, which ordinary constant resolution reads: the engine does not resolve a constant
|
|
670
|
+
# through a `Class.new` superclass either, and the two are pinned to agree.
|
|
671
|
+
def record_class_new_superclass(call, fqn, nesting)
|
|
672
|
+
superclass = call.arguments&.arguments&.first
|
|
673
|
+
return unless call.receiver.name == :Class && superclass
|
|
674
|
+
|
|
675
|
+
ancestor = if superclass.is_a?(Prism::SelfNode) then self_target(call, nesting)
|
|
676
|
+
else mixin_reference(superclass, nesting)
|
|
677
|
+
end
|
|
678
|
+
@ancestries << Ancestry.new(target: rooted_reference(fqn, call), ancestor: ancestor,
|
|
679
|
+
path: @path, line: call.location.start_line)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# The receiver-less statements directly in a declaration body.
|
|
683
|
+
def body_statements(body)
|
|
684
|
+
body.child_nodes.select { |stmt| stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? }
|
|
685
|
+
.to_set.compare_by_identity
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# The `self` of an eval block ({DynamicSites#eval_frame}): a {Reference}, or `:unknown`.
|
|
690
|
+
EvalFrame = Data.define(:receiver, :singleton_defs)
|
|
691
|
+
# A method an eval block defines on its receiver, and whether it is an `:instance` or `:singleton` method.
|
|
692
|
+
EvalMethod = Data.define(:receiver, :kind)
|
|
693
|
+
|
|
459
694
|
# Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
|
|
460
695
|
class Walker
|
|
461
696
|
include DynamicSites
|
|
697
|
+
include AncestrySites
|
|
462
698
|
|
|
463
|
-
attr_reader :declarations, :references, :dynamic_uses
|
|
699
|
+
attr_reader :declarations, :references, :dynamic_uses, :ancestries
|
|
464
700
|
|
|
465
701
|
def initialize(path:, role:)
|
|
466
702
|
@path = path
|
|
@@ -468,6 +704,10 @@ module Rigor
|
|
|
468
704
|
@declarations = []
|
|
469
705
|
@references = []
|
|
470
706
|
@dynamic_uses = []
|
|
707
|
+
@ancestries = []
|
|
708
|
+
# The receiver-less statements directly in the declaration body being walked, which its own
|
|
709
|
+
# `includes` already carry. See {#record_mixin}.
|
|
710
|
+
@body_statements = Set.new.compare_by_identity
|
|
471
711
|
# The declaration a meta-new rvalue is being walked for, or nil. See {#walk_constant_write}.
|
|
472
712
|
@owner = nil
|
|
473
713
|
# The node whose subtree holds the local variables in scope: the file, a method, or a class body.
|
|
@@ -478,6 +718,15 @@ module Rigor
|
|
|
478
718
|
# `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
|
|
479
719
|
@self_rebound = false
|
|
480
720
|
@rebinding_blocks = Set.new.compare_by_identity
|
|
721
|
+
# The {EvalFrame} of each `*_eval` block, and the one whose `self` the walk is directly in (nil when
|
|
722
|
+
# `self` is not an eval receiver). See {#eval_frame}.
|
|
723
|
+
@eval_frames = {}.compare_by_identity
|
|
724
|
+
@eval_frame = nil
|
|
725
|
+
# Inside a method an eval block defines (a `def`, or a `define_method` / `define_singleton_method`
|
|
726
|
+
# block), the eval receiver and the method's kind; nil elsewhere. See {#eval_method_references}.
|
|
727
|
+
@eval_method = nil
|
|
728
|
+
@meta_new_blocks = Set.new.compare_by_identity
|
|
729
|
+
@definer_blocks = {}.compare_by_identity
|
|
481
730
|
# What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
|
|
482
731
|
# level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
|
|
483
732
|
# `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
|
|
@@ -545,14 +794,18 @@ module Rigor
|
|
|
545
794
|
|
|
546
795
|
# The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
|
|
547
796
|
# this declaration sits in must not leak into it.
|
|
548
|
-
|
|
549
|
-
|
|
797
|
+
walk_body(node.body, nesting + [name], module_body)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def walk_body(body, nesting, module_body)
|
|
801
|
+
saved = [@owner, @self_rebound, @in_block, @module_body, @body_statements]
|
|
550
802
|
@owner = nil
|
|
551
803
|
@self_rebound = @in_block = false
|
|
552
804
|
@module_body = module_body
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
805
|
+
@body_statements = body_statements(body)
|
|
806
|
+
in_local_scope(body, :declaration) { walk(body, nesting) }
|
|
807
|
+
ensure
|
|
808
|
+
@owner, @self_rebound, @in_block, @module_body, @body_statements = saved
|
|
556
809
|
end
|
|
557
810
|
|
|
558
811
|
# A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
|
|
@@ -585,6 +838,7 @@ module Rigor
|
|
|
585
838
|
fqn = (nesting + [node.name.to_s]).join("::")
|
|
586
839
|
@declarations << Declaration.new(fqn: fqn, path: @path,
|
|
587
840
|
line: node.location.start_line, superclass: nil, includes: [].freeze)
|
|
841
|
+
record_class_new_superclass(call, fqn, nesting)
|
|
588
842
|
fqn
|
|
589
843
|
end
|
|
590
844
|
|
|
@@ -592,23 +846,35 @@ module Rigor
|
|
|
592
846
|
# top level of the body is inspected: a mixin applied inside a conditional or a nested def is not a
|
|
593
847
|
# static ancestor edge.
|
|
594
848
|
def mixin_names(body)
|
|
595
|
-
body.child_nodes.
|
|
596
|
-
next unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
|
|
597
|
-
next if stmt.receiver
|
|
849
|
+
body.child_nodes.flat_map do |stmt|
|
|
850
|
+
next [] unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
|
|
851
|
+
next [] if stmt.receiver
|
|
598
852
|
|
|
599
|
-
|
|
600
|
-
arg && Source::ConstantPath.qualified_name_or_nil(arg)
|
|
853
|
+
(stmt.arguments&.arguments || []).filter_map { |arg| Source::ConstantPath.qualified_name_or_nil(arg) }
|
|
601
854
|
end
|
|
602
855
|
end
|
|
603
856
|
|
|
604
857
|
def in_local_scope(scope, self_kind = self_kind_in(scope))
|
|
605
|
-
saved = [@local_scope, @block_params, @self_kind]
|
|
858
|
+
saved = [@local_scope, @block_params, @self_kind, @eval_frame, @eval_method]
|
|
859
|
+
@eval_method = eval_def(scope)
|
|
860
|
+
@eval_frame = nil
|
|
606
861
|
@local_scope = scope
|
|
607
862
|
@block_params = Set.new.freeze
|
|
608
863
|
@self_kind = self_kind
|
|
609
864
|
yield
|
|
610
865
|
ensure
|
|
611
|
-
@local_scope, @block_params, @self_kind = saved
|
|
866
|
+
@local_scope, @block_params, @self_kind, @eval_frame, @eval_method = saved
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# The method a `def` directly in an eval block defines on the eval receiver (#1946): a plain `def` is an
|
|
870
|
+
# instance method, or a singleton one under `instance_eval`; a `def self.x` is a singleton method.
|
|
871
|
+
def eval_def(scope)
|
|
872
|
+
frame = @eval_frame
|
|
873
|
+
return nil unless frame && scope.is_a?(Prism::DefNode)
|
|
874
|
+
|
|
875
|
+
if scope.receiver.nil? then EvalMethod.new(frame.receiver, frame.singleton_defs ? :singleton : :instance)
|
|
876
|
+
elsif scope.receiver.is_a?(Prism::SelfNode) then EvalMethod.new(frame.receiver, :singleton)
|
|
877
|
+
end
|
|
612
878
|
end
|
|
613
879
|
|
|
614
880
|
# The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
|
|
@@ -642,14 +908,30 @@ module Rigor
|
|
|
642
908
|
end
|
|
643
909
|
|
|
644
910
|
def in_block(block)
|
|
645
|
-
saved = [@block_params, @self_rebound, @in_block]
|
|
911
|
+
saved = [@block_params, @self_rebound, @in_block, @eval_frame, @eval_method]
|
|
646
912
|
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
647
913
|
@block_params = (@block_params | names).freeze unless names.empty?
|
|
648
|
-
|
|
914
|
+
enter_block_self(block)
|
|
649
915
|
@in_block = true
|
|
650
916
|
yield
|
|
651
917
|
ensure
|
|
652
|
-
@block_params, @self_rebound, @in_block = saved
|
|
918
|
+
@block_params, @self_rebound, @in_block, @eval_frame, @eval_method = saved
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# A block whose `self` is another object drops the eval reading around it: an eval block sets its own
|
|
922
|
+
# (#1878), and a `Class.new` / `Struct.new` block's `self` is the class it builds. A `define_method` /
|
|
923
|
+
# `define_singleton_method` block directly in an eval block is a method of the eval receiver (#1946).
|
|
924
|
+
def enter_block_self(block)
|
|
925
|
+
if @rebinding_blocks.include?(block)
|
|
926
|
+
@self_rebound = true
|
|
927
|
+
@eval_frame = @eval_frames[block]
|
|
928
|
+
@eval_method = nil
|
|
929
|
+
elsif @meta_new_blocks.include?(block)
|
|
930
|
+
@eval_frame = @eval_method = nil
|
|
931
|
+
elsif @eval_frame && @definer_blocks.key?(block)
|
|
932
|
+
@eval_method = EvalMethod.new(@eval_frame.receiver, @definer_blocks.fetch(block))
|
|
933
|
+
@eval_frame = nil
|
|
934
|
+
end
|
|
653
935
|
end
|
|
654
936
|
|
|
655
937
|
def site(node)
|