rigortype 0.3.2 → 0.3.4
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/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +93 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "label_intent"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# One `effect.unknown-label` finding, rendered (ADR-103 WD1 / WD14; #384).
|
|
8
|
+
#
|
|
9
|
+
# It carries the spelling, the nearest recognised label and the retirement record, and knows how
|
|
10
|
+
# to say all three in one sentence. What it deliberately does NOT carry is where the label was
|
|
11
|
+
# written or what the surface was: an envelope in `.rbs`, an rbs-inline annotation in `.rb` and a
|
|
12
|
+
# `.rigor.yml` list all reduce to "a place named this label, and now it means nothing", so the
|
|
13
|
+
# caller supplies the `subject` and the `consequence` and this supplies the vocabulary judgment.
|
|
14
|
+
# That is the seam [#385](https://github.com/rigortype/rigor/issues/385)'s `envelopes[].effect`
|
|
15
|
+
# and `attribution:` values point at.
|
|
16
|
+
#
|
|
17
|
+
# {.for} answers `nil` unless {LabelIntent} says the spelling is evidently a label — the whole
|
|
18
|
+
# point of the diagnostic is that it fires where intent is evident and nowhere else.
|
|
19
|
+
class UnknownLabelReport < Data.define(:token, :suggestion, :retirement)
|
|
20
|
+
# @param token [String] the spelling as written.
|
|
21
|
+
# @param registry [Rigor::Effects::Registry, nil] the vocabulary after plugin load.
|
|
22
|
+
# @param siblings [Array<String>] the other tokens written alongside it.
|
|
23
|
+
# @return [UnknownLabelReport, nil]
|
|
24
|
+
def self.for(token:, registry:, siblings: [])
|
|
25
|
+
return nil unless LabelIntent.evident?(token, registry, siblings: siblings)
|
|
26
|
+
|
|
27
|
+
new(
|
|
28
|
+
token: token.to_s,
|
|
29
|
+
suggestion: registry.suggest(token.to_s),
|
|
30
|
+
retirement: Array(registry.retired(token.to_s)).map(&:to_s).freeze
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether the vocabulary once carried this spelling and moved it. A retirement outranks a
|
|
35
|
+
# nearest-neighbour guess: the registry KNOWS where this label went, so guessing would be
|
|
36
|
+
# strictly worse information.
|
|
37
|
+
def retired?
|
|
38
|
+
!retirement.nil? && !retirement.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The sentence a reader can act on. `subject` names the declaration ("Effect envelope on
|
|
42
|
+
# `Foo#bar`"), `consequence` says what the degradation cost ("the annotation now bounds
|
|
43
|
+
# nothing") — the fail-open reading stated out loud, because a silently-unbounded envelope is
|
|
44
|
+
# exactly what this diagnostic exists to make visible.
|
|
45
|
+
def message(subject:, consequence:)
|
|
46
|
+
"#{subject} names #{token}, which is not a known effect label#{qualifier}; #{consequence}."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def qualifier
|
|
52
|
+
return " (#{token} is retired; write #{retirement.join(' or ')} instead)" if retired?
|
|
53
|
+
return " (did you mean #{suggestion}?)" if suggestion
|
|
54
|
+
|
|
55
|
+
""
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "effects/label"
|
|
4
|
+
require_relative "effects/label_set"
|
|
5
|
+
require_relative "effects/registry"
|
|
6
|
+
require_relative "effects/taint_cause"
|
|
7
|
+
require_relative "effects/attribution"
|
|
8
|
+
require_relative "effects/catalog"
|
|
9
|
+
require_relative "effects/collector"
|
|
10
|
+
require_relative "effects/config_envelopes"
|
|
11
|
+
require_relative "effects/discharge"
|
|
12
|
+
require_relative "effects/effect_table"
|
|
13
|
+
require_relative "effects/envelope"
|
|
14
|
+
require_relative "effects/envelope_check"
|
|
15
|
+
require_relative "effects/envelope_index"
|
|
16
|
+
require_relative "effects/file_collection"
|
|
17
|
+
require_relative "effects/liskov_check"
|
|
18
|
+
require_relative "effects/method_key"
|
|
19
|
+
require_relative "effects/origin"
|
|
20
|
+
require_relative "effects/propagator"
|
|
21
|
+
require_relative "effects/summary"
|
|
22
|
+
|
|
23
|
+
module Rigor
|
|
24
|
+
# The effect-label vocabulary (ADR-103; normative in
|
|
25
|
+
# `docs/type-specification/effect-labels.md`).
|
|
26
|
+
#
|
|
27
|
+
# Two layers live here. The label *language* — the grammar, the subsumption relation, the label
|
|
28
|
+
# sets a summary and an envelope are made of, the registry that says which spellings are
|
|
29
|
+
# recognised, and the closed enum of taint causes. And, on top of it, the observational machinery
|
|
30
|
+
# of #379: the {Collector} that records what the typer decided, the {Summary} / {FileCollection}
|
|
31
|
+
# values it produces, the built-in {Catalog}, and the {Propagator} that closes them into an
|
|
32
|
+
# {EffectTable}. On top of both sits the one thing here that DOES judge: an {Envelope} — an
|
|
33
|
+
# author-declared upper bound read off the project's RBS or written by convention in `.rigor.yml`
|
|
34
|
+
# ({ConfigEnvelopes}) — and the two checks that judge one: {EnvelopeCheck} compares a method against
|
|
35
|
+
# its own bound (`effect.envelope-exceeded`, #383 / #385) and {LiskovCheck} an override against the
|
|
36
|
+
# bound it inherits (`effect.liskov-widened`, #386). The same declarations are read a third way, which
|
|
37
|
+
# judges nothing: {EnvelopeIndex} resolves them per CALL SITE, so a caller reads what its callee
|
|
38
|
+
# promised as a `≤` bound. Beside them sit the project's policy
|
|
39
|
+
# surfaces: {Attribution} colours code Rigor never analysed, into the declared lane, and {Discharge}
|
|
40
|
+
# applies `effects.tolerated:` per origin at judgment time. The effect snapshot (#381) and the
|
|
41
|
+
# vocabulary diagnostic (#384) are their own slices.
|
|
42
|
+
#
|
|
43
|
+
# "Effect label", "effect summary" and "effect envelope" are trapped compounds in `CONTEXT.md`;
|
|
44
|
+
# bare "effect" still names `Rigor::FlowContribution`'s flow-effect bundle.
|
|
45
|
+
module Effects
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1256,6 +1256,40 @@ module Rigor
|
|
|
1256
1256
|
end
|
|
1257
1257
|
end
|
|
1258
1258
|
|
|
1259
|
+
# ADR-103 WD6 / #386 — yields `[class name, RBS::AST::Members::MethodDefinition]` for every method
|
|
1260
|
+
# member in the built environment that carries at least one annotation.
|
|
1261
|
+
#
|
|
1262
|
+
# The one reason this walk lives here rather than in its caller: `#env` is private, and it stays
|
|
1263
|
+
# private. The effect-envelope reader needs the *annotations* a gem's shipped signatures and
|
|
1264
|
+
# Rigor's bundled overlays declare — the accepted stratum of ADR-103 WD6, which discharges a call
|
|
1265
|
+
# site's taint because the same trust is already extended to those files' types — and nothing more
|
|
1266
|
+
# of the environment. Handing out the environment to get at them would trade a several-MB mutable
|
|
1267
|
+
# object for a read the loader can perform itself.
|
|
1268
|
+
#
|
|
1269
|
+
# Nested declarations are not descended into: `RBS::Environment` already flattens a nested
|
|
1270
|
+
# `class Foo::Bar` into its own `class_decls` entry, so a descent would key one member twice.
|
|
1271
|
+
#
|
|
1272
|
+
# Fail-soft, like every other query here: no environment (a build failure) yields nothing.
|
|
1273
|
+
def each_annotated_method_member
|
|
1274
|
+
environment = env
|
|
1275
|
+
return if environment.nil?
|
|
1276
|
+
|
|
1277
|
+
environment.class_decls.each do |type_name, entry|
|
|
1278
|
+
class_name = type_name.to_s.sub(/\A::/, "")
|
|
1279
|
+
self.class.entry_declarations(entry).each do |decl|
|
|
1280
|
+
members = decl.respond_to?(:members) ? decl.members : nil
|
|
1281
|
+
next if members.nil?
|
|
1282
|
+
|
|
1283
|
+
members.each do |member|
|
|
1284
|
+
next unless member.is_a?(::RBS::AST::Members::MethodDefinition)
|
|
1285
|
+
next if member.annotations.nil? || member.annotations.empty?
|
|
1286
|
+
|
|
1287
|
+
yield class_name, member
|
|
1288
|
+
end
|
|
1289
|
+
end
|
|
1290
|
+
end
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1259
1293
|
private
|
|
1260
1294
|
|
|
1261
1295
|
# The `::`-stripped names of every class/module entry whose declarations ALL originated from the given
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -311,11 +311,18 @@ module Rigor
|
|
|
311
311
|
# activesupport, the Rack status table) can then require the analyzed project's own locked gems when
|
|
312
312
|
# Rigor's host gem environment does not carry them — the standalone `gem install rigortype` case,
|
|
313
313
|
# where activesupport is deliberately not a runtime dependency.
|
|
314
|
+
# The hand-off is a module ivar, which a non-main Ractor may not write (`Ractor::IsolationError`) —
|
|
315
|
+
# and every Ractor-pool worker builds its Environment inside its own Ractor, so an unguarded write
|
|
316
|
+
# killed the whole backend at `WorkerSession#initialize` (#414). The resolution is deterministic per
|
|
317
|
+
# configuration and `Runner`'s pre-passes already perform it on the main Ractor before the pool
|
|
318
|
+
# spawns, so a worker reads the value the main set (a frozen String, hence shareable) and skips the
|
|
319
|
+
# write. The local return value is unaffected, so the missing-gem constant index still gets its root
|
|
320
|
+
# even when the write is skipped.
|
|
314
321
|
def resolve_target_bundle_root(bundle_path:, project_root:, auto_detect:)
|
|
315
322
|
bundle_root = BundleSigDiscovery.resolve_bundle_path(
|
|
316
323
|
bundle_path: bundle_path, project_root: project_root, auto_detect: auto_detect
|
|
317
324
|
)&.to_s
|
|
318
|
-
Plugin::Isolation.target_bundle_root = bundle_root
|
|
325
|
+
Plugin::Isolation.target_bundle_root = bundle_root if Ractor.current == Ractor.main
|
|
319
326
|
bundle_root
|
|
320
327
|
end
|
|
321
328
|
|
|
@@ -13,13 +13,13 @@ module Rigor
|
|
|
13
13
|
# for the slice-3 normative description.
|
|
14
14
|
class MergeResult
|
|
15
15
|
attr_reader :return_type, :truthy_facts, :falsey_facts, :post_return_facts,
|
|
16
|
-
:mutations, :invalidations, :exceptional, :role_conformance,
|
|
16
|
+
:mutations, :invalidations, :exceptional, :role_conformance, :effects,
|
|
17
17
|
:provenances, :conflicts
|
|
18
18
|
|
|
19
19
|
# rubocop:disable Metrics/ParameterLists
|
|
20
20
|
def initialize(return_type: nil, truthy_facts: [], falsey_facts: [],
|
|
21
21
|
post_return_facts: [], mutations: [], invalidations: [],
|
|
22
|
-
exceptional: nil, role_conformance: [],
|
|
22
|
+
exceptional: nil, role_conformance: [], effects: nil,
|
|
23
23
|
provenances: [], conflicts: [])
|
|
24
24
|
# rubocop:enable Metrics/ParameterLists
|
|
25
25
|
@return_type = return_type
|
|
@@ -30,6 +30,7 @@ module Rigor
|
|
|
30
30
|
@invalidations = invalidations.dup.freeze
|
|
31
31
|
@exceptional = exceptional
|
|
32
32
|
@role_conformance = role_conformance.dup.freeze
|
|
33
|
+
@effects = effects
|
|
33
34
|
@provenances = provenances.dup.freeze
|
|
34
35
|
@conflicts = conflicts.dup.freeze
|
|
35
36
|
freeze
|
|
@@ -42,7 +43,7 @@ module Rigor
|
|
|
42
43
|
def empty?
|
|
43
44
|
@return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
|
|
44
45
|
@post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
|
|
45
|
-
@exceptional.nil? && @role_conformance.empty?
|
|
46
|
+
@exceptional.nil? && @role_conformance.empty? && @effects.nil?
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def to_h
|
|
@@ -55,6 +56,7 @@ module Rigor
|
|
|
55
56
|
"invalidations" => invalidations,
|
|
56
57
|
"exceptional" => exceptional,
|
|
57
58
|
"role_conformance" => role_conformance,
|
|
59
|
+
"effects" => effects,
|
|
58
60
|
"provenances" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p },
|
|
59
61
|
"conflicts" => conflicts.map(&:to_h)
|
|
60
62
|
}
|
|
@@ -99,6 +99,7 @@ module Rigor
|
|
|
99
99
|
fold_effects(state, contribution)
|
|
100
100
|
fold_exceptional(state, contribution, tier)
|
|
101
101
|
fold_role_conformance(state, contribution)
|
|
102
|
+
fold_effect_labels(state, contribution)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def fold_return_type(state, contribution, tier)
|
|
@@ -182,6 +183,18 @@ module Rigor
|
|
|
182
183
|
accumulate(state.role_conformance, contribution.role_conformance)
|
|
183
184
|
end
|
|
184
185
|
|
|
186
|
+
# ADR-103 WD5 — the `effects` slot merges by UNION, unconditionally and across every authority
|
|
187
|
+
# tier. A label set is an upper bound on what a call does, and two sources that each name part of
|
|
188
|
+
# a footprint together name more of it; there is no reading under which one source's claim
|
|
189
|
+
# cancels another's, so there is no conflict to report either. `nil` ("says nothing") stays nil
|
|
190
|
+
# until some contribution speaks.
|
|
191
|
+
def fold_effect_labels(state, contribution)
|
|
192
|
+
incoming = contribution.effects
|
|
193
|
+
return if incoming.nil?
|
|
194
|
+
|
|
195
|
+
state.effects = state.effects.nil? ? incoming : state.effects.join(incoming)
|
|
196
|
+
end
|
|
197
|
+
|
|
185
198
|
def accumulate(target, incoming)
|
|
186
199
|
Array(incoming).each do |item|
|
|
187
200
|
target << item unless target.include?(item)
|
|
@@ -217,7 +230,7 @@ module Rigor
|
|
|
217
230
|
# {MergeResult} at the end via {#to_result}.
|
|
218
231
|
class MergeState
|
|
219
232
|
attr_accessor :return_type, :return_type_tier, :return_type_provenance,
|
|
220
|
-
:exceptional, :exceptional_tier, :exceptional_provenance
|
|
233
|
+
:exceptional, :exceptional_tier, :exceptional_provenance, :effects
|
|
221
234
|
attr_reader :truthy_facts, :falsey_facts, :post_return_facts,
|
|
222
235
|
:mutations, :invalidations, :role_conformance,
|
|
223
236
|
:provenances, :conflicts
|
|
@@ -235,6 +248,7 @@ module Rigor
|
|
|
235
248
|
@exceptional_tier = nil
|
|
236
249
|
@exceptional_provenance = nil
|
|
237
250
|
@role_conformance = []
|
|
251
|
+
@effects = nil
|
|
238
252
|
@provenances = []
|
|
239
253
|
@conflicts = []
|
|
240
254
|
end
|
|
@@ -253,6 +267,7 @@ module Rigor
|
|
|
253
267
|
invalidations: @invalidations,
|
|
254
268
|
exceptional: @exceptional,
|
|
255
269
|
role_conformance: @role_conformance,
|
|
270
|
+
effects: @effects,
|
|
256
271
|
provenances: @provenances,
|
|
257
272
|
conflicts: @conflicts
|
|
258
273
|
)
|
|
@@ -9,7 +9,7 @@ module Rigor
|
|
|
9
9
|
# [ADR-2 § "Plugin Contribution Merging"](../../docs/adr/2-extension-api.md) rather than letting any one
|
|
10
10
|
# source override another silently.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# Nine content slots plus a {Provenance} block. A slot left as `nil` (or, for collection-shaped slots, an
|
|
13
13
|
# empty collection) means the contribution does not assert anything in that dimension; the merge policy
|
|
14
14
|
# treats it as absent.
|
|
15
15
|
#
|
|
@@ -38,8 +38,15 @@ module Rigor
|
|
|
38
38
|
invalidations
|
|
39
39
|
exceptional
|
|
40
40
|
role_conformance
|
|
41
|
+
effects
|
|
41
42
|
].freeze
|
|
42
43
|
|
|
44
|
+
# `effects` is scalar-shaped like `return_type` and `exceptional`: only `nil` means "asserts
|
|
45
|
+
# nothing". An EMPTY {Rigor::Effects::LabelSet} is the positive claim that the call performs no
|
|
46
|
+
# effects — the reading of `%a{pure}` — so it must not be mistaken for an unset slot the way an
|
|
47
|
+
# empty fact Array is.
|
|
48
|
+
SCALAR_SLOTS = %i[return_type exceptional effects].freeze
|
|
49
|
+
|
|
43
50
|
attr_reader(*SLOT_NAMES, :provenance)
|
|
44
51
|
|
|
45
52
|
# @param return_type [Object, nil] normal-edge return type. Use `nil` when the contribution does not
|
|
@@ -54,12 +61,17 @@ module Rigor
|
|
|
54
61
|
# imply.
|
|
55
62
|
# @param exceptional [Object, nil] non-returning, raising, or unreachable effect.
|
|
56
63
|
# @param role_conformance [Array, nil] capability-role conformance facts the contribution provides.
|
|
64
|
+
# @param effects [Rigor::Effects::LabelSet, nil] ADR-103 WD5 — the effect labels this call edge
|
|
65
|
+
# attributes to its callee, as an upper bound. `nil` means "says nothing about effects", which is
|
|
66
|
+
# NOT the same as the empty set (which asserts the call performs none). Merged by union, the
|
|
67
|
+
# conservative direction: two sources that each name part of a call's footprint together name
|
|
68
|
+
# more of it, and neither can shrink the other's claim.
|
|
57
69
|
# @param provenance [Provenance] source-family, plugin-id, node, and cache-descriptor metadata. Defaults
|
|
58
70
|
# to `Provenance.builtin`.
|
|
59
71
|
# rubocop:disable Metrics/ParameterLists
|
|
60
72
|
def initialize(return_type: nil, truthy_facts: nil, falsey_facts: nil,
|
|
61
73
|
post_return_facts: nil, mutations: nil, invalidations: nil,
|
|
62
|
-
exceptional: nil, role_conformance: nil,
|
|
74
|
+
exceptional: nil, role_conformance: nil, effects: nil,
|
|
63
75
|
provenance: Provenance.builtin)
|
|
64
76
|
# rubocop:enable Metrics/ParameterLists
|
|
65
77
|
@return_type = return_type
|
|
@@ -70,6 +82,7 @@ module Rigor
|
|
|
70
82
|
@invalidations = freeze_collection(invalidations)
|
|
71
83
|
@exceptional = exceptional
|
|
72
84
|
@role_conformance = freeze_collection(role_conformance)
|
|
85
|
+
@effects = effects
|
|
73
86
|
@provenance = provenance
|
|
74
87
|
freeze
|
|
75
88
|
end
|
|
@@ -77,7 +90,7 @@ module Rigor
|
|
|
77
90
|
# @return [Boolean] true when every content slot is unset (nil or an empty collection). Provenance does
|
|
78
91
|
# not count toward emptiness — an empty bundle still carries source attribution.
|
|
79
92
|
def empty?
|
|
80
|
-
SLOT_NAMES.all? { |slot| slot_empty?(public_send(slot)) }
|
|
93
|
+
SLOT_NAMES.all? { |slot| slot_empty?(slot, public_send(slot)) }
|
|
81
94
|
end
|
|
82
95
|
|
|
83
96
|
def to_h
|
|
@@ -102,6 +115,7 @@ module Rigor
|
|
|
102
115
|
# | invalidations | normal | invalidation | (per-fact target) |
|
|
103
116
|
# | exceptional | exceptional | exception | :raise |
|
|
104
117
|
# | role_conformance | normal | role | (per-role target) |
|
|
118
|
+
# | effects | normal | effects | :effects |
|
|
105
119
|
#
|
|
106
120
|
# @return [Array<Element>]
|
|
107
121
|
def to_element_list # rubocop:disable Metrics/AbcSize
|
|
@@ -116,6 +130,7 @@ module Rigor
|
|
|
116
130
|
Array(invalidations).each { |i| elements << element_for(fact_target(i), :normal, :invalidation, i) }
|
|
117
131
|
elements << element_for(:raise, :exceptional, :exception, exceptional) unless exceptional.nil?
|
|
118
132
|
Array(role_conformance).each { |r| elements << element_for(fact_target(r), :normal, :role, r) }
|
|
133
|
+
elements << element_for(:effects, :normal, :effects, effects) unless effects.nil?
|
|
119
134
|
elements.freeze
|
|
120
135
|
end
|
|
121
136
|
|
|
@@ -149,8 +164,9 @@ module Rigor
|
|
|
149
164
|
value.dup.freeze
|
|
150
165
|
end
|
|
151
166
|
|
|
152
|
-
def slot_empty?(value)
|
|
167
|
+
def slot_empty?(slot, value)
|
|
153
168
|
return true if value.nil?
|
|
169
|
+
return false if SCALAR_SLOTS.include?(slot)
|
|
154
170
|
return value.empty? if value.respond_to?(:empty?)
|
|
155
171
|
|
|
156
172
|
false
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type/anonymous_class_name"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# #319 — identity for the class a `Class.new do ... end` / `Module.new do ... end` creates away from
|
|
10
|
+
# constant-write position.
|
|
11
|
+
#
|
|
12
|
+
# Ruby evaluates such a block with `self` bound to the freshly created class (`class_eval` semantics): a
|
|
13
|
+
# `def` inside it defines an instance method on that class and `attr_reader` runs as a class-level macro.
|
|
14
|
+
# `ScopeIndexer` has always walked that body as a class body — but only through its
|
|
15
|
+
# `Prism::ConstantWriteNode` branch, because the constant supplied the name the discovered-method tables
|
|
16
|
+
# are keyed by. At every other position (`klass = Class.new do ... end`, a bare expression, a method
|
|
17
|
+
# return) there was no name, so the body fell through to the enclosing — usually top-level — scope. Two
|
|
18
|
+
# false positives followed on correct code: `attr_reader` became a `call.unresolved-toplevel` warning
|
|
19
|
+
# (`Scope#toplevel?` is `self_type.nil?`), and the class itself typed as the bare `Singleton[Object]`
|
|
20
|
+
# `MethodDispatcher#class_new_lift` hands back, taking `Object`'s zero-arity `new` with it.
|
|
21
|
+
#
|
|
22
|
+
# This module supplies the missing name: a per-call-site synthetic one, spelled so that no Ruby constant
|
|
23
|
+
# path can collide with it in the discovery tables, and stable across the index pass and the dispatch
|
|
24
|
+
# pass because both derive it from the same `(call node, source path)` pair.
|
|
25
|
+
module AnonymousMetaClass
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# The class-creating meta calls whose block body is a class body. `Struct.new` / `Data.define` are here
|
|
29
|
+
# because the body semantics are identical — the block is `class_eval`'d on the generated subclass — not
|
|
30
|
+
# because their anonymous *value* is modelled: those keep the `StructClass` / `DataClass` carriers
|
|
31
|
+
# {MethodDispatcher::StructFolding} / {MethodDispatcher::DataFolding} build for them.
|
|
32
|
+
META_NEW_SELECTORS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
|
|
33
|
+
|
|
34
|
+
# The receiver constant name (`:Class`, `:Module`, `:Struct`, `:Data`) of a class-creating meta call that
|
|
35
|
+
# carries a literal block, or nil for anything else. The receiver MUST be the bare constant (or its
|
|
36
|
+
# `::`-rooted spelling) — a call through a variable or another call's return has no statically known
|
|
37
|
+
# identity, exactly as `ScopeIndexer#meta_constant_receiver?` requires.
|
|
38
|
+
#
|
|
39
|
+
# Argument shapes are deliberately NOT inspected: `Class.new(Base) { ... }` and
|
|
40
|
+
# `Data.define(*members) { ... }` evaluate their block as a class body whatever the arguments are, and a
|
|
41
|
+
# narrower gate here would leave the body in top-level scope again.
|
|
42
|
+
def block_form_receiver(node)
|
|
43
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
44
|
+
return nil unless node.block.is_a?(Prism::BlockNode)
|
|
45
|
+
|
|
46
|
+
receiver_name = literal_constant_name(node.receiver)
|
|
47
|
+
return nil if receiver_name.nil?
|
|
48
|
+
return nil unless META_NEW_SELECTORS[receiver_name] == node.name
|
|
49
|
+
|
|
50
|
+
receiver_name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The synthetic class name for `node`'s block body, or nil when `node` is not a recognised block form.
|
|
54
|
+
# {Type::AnonymousClassName} owns the spelling; this decides which call sites earn one and what the key
|
|
55
|
+
# holds.
|
|
56
|
+
#
|
|
57
|
+
# `source_path` is part of the key so two files whose anonymous classes happen to share a line and column
|
|
58
|
+
# do not merge their method sets when the per-file tables are folded into the cross-file seed. It is
|
|
59
|
+
# omitted when the caller has no path (unit-level probes over a bare `Prism` program) — passed through
|
|
60
|
+
# verbatim rather than defaulted, so every pass over the same file derives the same name.
|
|
61
|
+
def name_for(node, source_path = nil)
|
|
62
|
+
receiver_name = block_form_receiver(node)
|
|
63
|
+
return nil if receiver_name.nil?
|
|
64
|
+
|
|
65
|
+
location = node.location
|
|
66
|
+
key = [source_path, location.start_line, location.start_column].compact.join(":")
|
|
67
|
+
Type::AnonymousClassName.build(receiver_name, key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The bare-constant receiver's name (`Class`, `::Class`), or nil for every other receiver shape.
|
|
71
|
+
def literal_constant_name(node)
|
|
72
|
+
case node
|
|
73
|
+
when Prism::ConstantReadNode
|
|
74
|
+
node.name
|
|
75
|
+
when Prism::ConstantPathNode
|
|
76
|
+
node.name if node.parent.nil?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -13,12 +13,15 @@ require_relative "body_fixpoint"
|
|
|
13
13
|
require_relative "budget_trace"
|
|
14
14
|
require_relative "dynamic_origin"
|
|
15
15
|
require_relative "origin_lookup"
|
|
16
|
+
require_relative "../effects/collector"
|
|
16
17
|
require_relative "fallback"
|
|
17
18
|
require_relative "flow_tracer"
|
|
18
19
|
require_relative "indexed_narrowing"
|
|
19
20
|
require_relative "macro_block_self_type"
|
|
20
21
|
require_relative "method_dispatcher"
|
|
21
22
|
require_relative "narrowing"
|
|
23
|
+
require_relative "singleton_object_constant"
|
|
24
|
+
require_relative "optimistic_origin"
|
|
22
25
|
require_relative "struct_fold_safety"
|
|
23
26
|
|
|
24
27
|
module Rigor
|
|
@@ -609,16 +612,11 @@ module Rigor
|
|
|
609
612
|
elide_or_union(node.predicate, else_type, then_type)
|
|
610
613
|
end
|
|
611
614
|
|
|
612
|
-
# Issue #286 — the effective optimistic-nil-free cause of an expression
|
|
613
|
-
#
|
|
615
|
+
# Issue #286 — the effective optimistic-nil-free cause of an expression. {OptimisticOrigin.resolve} owns
|
|
616
|
+
# the judgment, shared verbatim with `StatementEvaluator#optimistic_origin_for` and the
|
|
617
|
+
# `flow.always-truthy-condition` collector.
|
|
614
618
|
def optimistic_origin_for(node)
|
|
615
|
-
|
|
616
|
-
return recorded if recorded
|
|
617
|
-
|
|
618
|
-
case node
|
|
619
|
-
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
620
|
-
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
621
|
-
end
|
|
619
|
+
OptimisticOrigin.resolve(node, scope)
|
|
622
620
|
end
|
|
623
621
|
|
|
624
622
|
def if_else_type(subsequent)
|
|
@@ -663,7 +661,7 @@ module Rigor
|
|
|
663
661
|
# fallback is preserved.
|
|
664
662
|
def type_of_and_or(node)
|
|
665
663
|
left_type = type_of(node.left)
|
|
666
|
-
polarity =
|
|
664
|
+
polarity = left_operand_polarity(node.left, left_type)
|
|
667
665
|
return short_circuit_for(node, left_type, polarity) if polarity
|
|
668
666
|
|
|
669
667
|
# The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
|
|
@@ -689,6 +687,19 @@ module Rigor
|
|
|
689
687
|
end
|
|
690
688
|
end
|
|
691
689
|
|
|
690
|
+
# Issue #313 — the node-aware wrapper the `&&` / `||` short-circuit reads. The spec's exclusion binds
|
|
691
|
+
# this gate as much as it binds `flow.always-truthy-condition`, and a `Constant`-only gate is not by
|
|
692
|
+
# itself enough to honour it: a literal hash whose values share one type reads as a lone `Constant`
|
|
693
|
+
# (`UNIFORM[key]` → `Constant[1]`), so the gate would judge the left operand of `UNIFORM[key] || key`
|
|
694
|
+
# provably truthy and discard the author's fallback — the counter-example the spec names verbatim.
|
|
695
|
+
# Declining returns the union of both operands, which is what `StatementEvaluator#eval_and_or` produces
|
|
696
|
+
# anyway, so the two `&&` / `||` typers stay in agreement.
|
|
697
|
+
def left_operand_polarity(left_node, left_type)
|
|
698
|
+
return nil unless optimistic_origin_for(left_node).nil?
|
|
699
|
+
|
|
700
|
+
constant_value_polarity(left_type)
|
|
701
|
+
end
|
|
702
|
+
|
|
692
703
|
# Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
|
|
693
704
|
# `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
|
|
694
705
|
# node, so the same predicate analysis can be reused in both contexts.
|
|
@@ -1092,8 +1103,12 @@ module Rigor
|
|
|
1092
1103
|
# through `Enumerable#select` / `Object#select` and the caller observes `Array[Elem]` instead of the
|
|
1093
1104
|
# helper's actual return type. The check fires only for `node.receiver.nil?` (true implicit self), so
|
|
1094
1105
|
# explicit-receiver dispatch is unaffected.
|
|
1106
|
+
#
|
|
1107
|
+
# Issue #316 — the lookup goes through the confidence-gated `Scope#bindable_top_level_def_for`, not the
|
|
1108
|
+
# raw table: inside a block whose `self` is unmodelled, a top-level `def` from ANOTHER file is not
|
|
1109
|
+
# evidence about which method the call reaches, so the bind is declined and the call widens.
|
|
1095
1110
|
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1096
|
-
local_def = node.receiver.nil? ? scope.
|
|
1111
|
+
local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
|
|
1097
1112
|
return nil unless local_def
|
|
1098
1113
|
|
|
1099
1114
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
@@ -1120,6 +1135,11 @@ module Rigor
|
|
|
1120
1135
|
arg_types = call_arg_types(node)
|
|
1121
1136
|
block_type = block_return_type_for(node, receiver, arg_types)
|
|
1122
1137
|
|
|
1138
|
+
# ADR-103 WD13 — the effect collector's one hot-path site. Purely observational: it reads the
|
|
1139
|
+
# receiver type the typer has just computed and the `dynamic_origins` cause already recorded, and
|
|
1140
|
+
# asks nothing further of dispatch. Off (the default) this is one integer read.
|
|
1141
|
+
Effects::Collector.record_call(node, receiver, scope) if Effects::Collector.active?
|
|
1142
|
+
|
|
1123
1143
|
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1124
1144
|
return local_def_result if local_def_result
|
|
1125
1145
|
|
|
@@ -1159,7 +1179,7 @@ module Rigor
|
|
|
1159
1179
|
# `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
|
|
1160
1180
|
# tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
|
|
1161
1181
|
# project-defined singleton methods reach here.
|
|
1162
|
-
singleton_inference =
|
|
1182
|
+
singleton_inference = try_project_singleton_inference(receiver, node, arg_types)
|
|
1163
1183
|
return singleton_inference if singleton_inference
|
|
1164
1184
|
|
|
1165
1185
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
|
|
@@ -1167,11 +1187,19 @@ module Rigor
|
|
|
1167
1187
|
# semantic outcome, not a fail-soft compromise, so it MUST NOT record a tracer event.
|
|
1168
1188
|
return inherit_receiver_origin(node) if receiver.is_a?(Type::Dynamic)
|
|
1169
1189
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1190
|
+
unresolved_call_result(node, receiver)
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
# The engine choke-point where a call has exhausted every resolution tier (RBS dispatch + user-class
|
|
1194
|
+
# ancestor walk) and falls through to `Dynamic[top]`. Two observational recorders read it, both a
|
|
1195
|
+
# plain integer read when inactive, and neither changes the answer:
|
|
1196
|
+
#
|
|
1197
|
+
# - ADR-24 slice 4a captures an implicit-self miss so a later slice's closed-class gate can flag it;
|
|
1198
|
+
# - ADR-103 WD13 retracts the effect collector's optimistic `resolved: true`, which is what separates
|
|
1199
|
+
# an implicit-self call the closed world has no definition for from an ordinary inherited one.
|
|
1200
|
+
def unresolved_call_result(node, receiver)
|
|
1174
1201
|
record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
|
|
1202
|
+
Effects::Collector.record_unresolved(node, scope.source_path) if Effects::Collector.active?
|
|
1175
1203
|
|
|
1176
1204
|
fallback_for(node, family: :prism)
|
|
1177
1205
|
end
|
|
@@ -1336,6 +1364,31 @@ module Rigor
|
|
|
1336
1364
|
nil
|
|
1337
1365
|
end
|
|
1338
1366
|
|
|
1367
|
+
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|
|
1368
|
+
# body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
|
|
1369
|
+
# `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
|
|
1370
|
+
# object (#320) — so the two are mutually exclusive and consulting both is one resolution attempt.
|
|
1371
|
+
def try_project_singleton_inference(receiver, call_node, arg_types)
|
|
1372
|
+
try_singleton_method_inference(receiver, call_node, arg_types) ||
|
|
1373
|
+
try_singleton_object_constant_inference(receiver, call_node, arg_types)
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
# #320 — resolves a call whose receiver is a constant holding an ordinary object with a `class << Const`
|
|
1377
|
+
# singleton body, re-typing that body with the call's argument types bound. `self` inside the body IS
|
|
1378
|
+
# that object, so the receiver carrier is passed through unchanged. Own-constant only, and only for a
|
|
1379
|
+
# name the project actually recorded — a miss degrades to today's `Dynamic[top]`, never a false
|
|
1380
|
+
# resolution. `Singleton` receivers never reach here: {#try_singleton_method_inference} owns them.
|
|
1381
|
+
def try_singleton_object_constant_inference(receiver, call_node, arg_types)
|
|
1382
|
+
return nil unless receiver.is_a?(Type::Nominal)
|
|
1383
|
+
|
|
1384
|
+
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, call_node.name, scope)
|
|
1385
|
+
return nil if def_node.nil?
|
|
1386
|
+
|
|
1387
|
+
infer_user_method_return(def_node, receiver, arg_types)
|
|
1388
|
+
rescue StandardError
|
|
1389
|
+
nil
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1339
1392
|
# ADR-24 slice 2 — resolves `method_name` against `class_name`'s own `def`s, then walks the user-class
|
|
1340
1393
|
# ancestor chain: included / prepended modules (transitive) and the superclass chain. RBS-known
|
|
1341
1394
|
# ancestors are NOT walked here — the `MethodDispatcher` RBS tier runs before
|
|
@@ -2444,7 +2497,11 @@ module Rigor
|
|
|
2444
2497
|
case block_arg
|
|
2445
2498
|
when Prism::BlockNode
|
|
2446
2499
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
|
|
2447
|
-
|
|
2500
|
+
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
2501
|
+
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
|
|
2502
|
+
block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
|
|
2503
|
+
acc.with_local(name, type)
|
|
2504
|
+
end
|
|
2448
2505
|
block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
|
|
2449
2506
|
type_block_body(block_arg, block_scope)
|
|
2450
2507
|
when Prism::BlockArgumentNode
|