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,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../scope"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "scope_indexer"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issue #352 / [ADR-17](../../../docs/adr/17-monkey-patch-pre-evaluation.md) — the constant half of the
|
|
12
|
+
# `pre_eval:` publication surface.
|
|
13
|
+
#
|
|
14
|
+
# ADR-17 slice 2 gave `pre_eval:` a project-wide **method** registry ({ProjectPatchedMethods}). A
|
|
15
|
+
# constant declared in the same file stayed invisible across the file boundary: `Scope#in_source_constants`
|
|
16
|
+
# is per-file and is deliberately not part of `Runner#project_scope_seed_tables`, so only class-shaped
|
|
17
|
+
# constants (a `class` / `module` declaration, or one of the four meta-new forms `ScopeIndexer`'s
|
|
18
|
+
# `record_class_new_constant_decl` promotes) ever crossed. `TIMEOUT = 30` in a listed file read as
|
|
19
|
+
# `Dynamic[top]` everywhere else.
|
|
20
|
+
#
|
|
21
|
+
# This collector closes that half. It walks the `pre_eval:` files with the SAME constant pre-pass the
|
|
22
|
+
# per-file path uses ({ScopeIndexer.build_in_source_constants}) and publishes the result — widened — into
|
|
23
|
+
# the project seed. Files not listed under `pre_eval:` are untouched, so the feature is opt-in by
|
|
24
|
+
# construction and its cost stays proportional to the listed file count (ADR-17 WD1's cost-bounded
|
|
25
|
+
# argument, which is also why slice 5's full-project two-pass stayed rejectable).
|
|
26
|
+
#
|
|
27
|
+
# ## The widening rule — why the published type is not the same-file type
|
|
28
|
+
#
|
|
29
|
+
# Same-file constant propagation is **value-pinned**: `42`, `"hello"`, a `Tuple`, a `HashShape`. Carrying
|
|
30
|
+
# that verbatim across a file boundary would land diagnostics in files whose author never opened the
|
|
31
|
+
# constant's definition — `CONFIG = { a: 1 }` arriving as a closed `HashShape` makes `CONFIG.fetch(:b)`
|
|
32
|
+
# fire, `ARR = [1, 2]` arriving as a `Tuple` routes through `ShapeDispatch` instead of the RBS overload a
|
|
33
|
+
# call site relied on. Today's cross-file `Dynamic[top]` is false-positive-free *by construction*, so
|
|
34
|
+
# precision here can only spend that budget; AGENTS.md § "Implementation Guidelines" ranks false positives
|
|
35
|
+
# above worst-case static reading, so this slice spends as little of it as possible.
|
|
36
|
+
#
|
|
37
|
+
# {.widen} therefore publishes the **erased class**, never the value:
|
|
38
|
+
#
|
|
39
|
+
# | Declared | Same file | Published cross-file |
|
|
40
|
+
# | --- | --- | --- |
|
|
41
|
+
# | `INT_LIT = 42` | `42` | `Integer` |
|
|
42
|
+
# | `STR_LIT = "hello"` | `"hello"` | `String` |
|
|
43
|
+
# | `ARR_LIT = [1, 2]` | `[1, 2]` (Tuple) | `Array` (raw, elements dropped) |
|
|
44
|
+
# | `HSH_LIT = { a: 1 }` | `{ a: 1 }` (HashShape) | `Hash` (raw, keys dropped) |
|
|
45
|
+
# | `ALIAS_CLS = String` | `singleton(String)` | `singleton(String)` |
|
|
46
|
+
# | `Nest::NESTED_INT = 7` | `7` | `Integer` |
|
|
47
|
+
#
|
|
48
|
+
# Anything the rule does not recognise **declines** — the name is simply not published and reads exactly
|
|
49
|
+
# as it does today. Declining is always the safe answer here, which is why the `else` arm is `nil` rather
|
|
50
|
+
# than a fallback. `Constant[nil]` declines on purpose: a `X = nil` at declaration position is a
|
|
51
|
+
# placeholder for a value assigned at runtime far more often than it is a genuine `NilClass`, and
|
|
52
|
+
# publishing `NilClass` project-wide would make every use of it a diagnostic.
|
|
53
|
+
#
|
|
54
|
+
# Value-pinning a *provably* frozen single-write literal cross-file is a strictly later question; it needs
|
|
55
|
+
# its own FP measurement and is deliberately not attempted here.
|
|
56
|
+
#
|
|
57
|
+
# ## The multi-file write rule — widen on conflict, all the way to `Dynamic[top]`
|
|
58
|
+
#
|
|
59
|
+
# Within one file, `ScopeIndexer#record_constant_write` unions repeated writes; that union is widened as a
|
|
60
|
+
# whole (`X = 1; X = "a"` in one file publishes `Integer | String`). ACROSS files the same union would be
|
|
61
|
+
# a type neither author can see, so the rule is **widen on conflict**: when two listed files publish the
|
|
62
|
+
# same qualified name with different widened types, the name is dropped from the table entirely and reads
|
|
63
|
+
# as `Dynamic[top]` — the widest type there is, and the one the name already had. Agreeing writes (`X = 1`
|
|
64
|
+
# here, `X = 2` there — both `Integer`) are not a conflict at all, which is the point of widening first:
|
|
65
|
+
# `1 | 2` is never produced.
|
|
66
|
+
#
|
|
67
|
+
# ## Ordering
|
|
68
|
+
#
|
|
69
|
+
# The published table seeds `Scope#in_source_constants`, which `Reflection.constant_type_at` consults
|
|
70
|
+
# AFTER the class registry and `discovered_classes`. A published entry can therefore never mask a
|
|
71
|
+
# class-shaped constant that already crossed, and the per-file table always wins over the seed (see
|
|
72
|
+
# {ScopeIndexer.index}'s merge) — same-file remains the most specific authority.
|
|
73
|
+
module PreEvalConstants
|
|
74
|
+
EMPTY = {}.freeze
|
|
75
|
+
|
|
76
|
+
module_function
|
|
77
|
+
|
|
78
|
+
# Collects and widens the constants every `pre_eval:` file declares.
|
|
79
|
+
#
|
|
80
|
+
# @param paths [Array<String>] absolute paths to the `pre_eval:` files that exist on disk.
|
|
81
|
+
# @param scope_builder [#call] `path -> Rigor::Scope`; the caller supplies a project-seeded, environment-
|
|
82
|
+
# bound scope so the rvalue typer resolves cross-file classes exactly as per-file analysis would.
|
|
83
|
+
# @param target_ruby [String, nil] the Prism parse version (`Configuration#target_ruby`).
|
|
84
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil] editor-mode binding; when set, a listed file that
|
|
85
|
+
# matches the in-flight buffer is read from its physical bytes.
|
|
86
|
+
# @return [Hash{String => Rigor::Type}] frozen qualified-name -> published type table.
|
|
87
|
+
def collect(paths:, scope_builder:, target_ruby: nil, buffer: nil)
|
|
88
|
+
published = {}
|
|
89
|
+
conflicted = {}
|
|
90
|
+
paths.each do |path|
|
|
91
|
+
file_constants(path, scope_builder: scope_builder, target_ruby: target_ruby, buffer: buffer)
|
|
92
|
+
.each { |name, type| merge_publication(published, conflicted, name, type) }
|
|
93
|
+
end
|
|
94
|
+
published.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Folds one declaration into the accumulator under the widen-on-conflict rule. A name that has already
|
|
98
|
+
# conflicted stays out for the rest of the collection — a later agreeing write must not resurrect it.
|
|
99
|
+
def merge_publication(published, conflicted, name, type)
|
|
100
|
+
return if conflicted.key?(name)
|
|
101
|
+
|
|
102
|
+
widened = widen(type)
|
|
103
|
+
return if widened.nil?
|
|
104
|
+
|
|
105
|
+
existing = published[name]
|
|
106
|
+
return published[name] = widened if existing.nil?
|
|
107
|
+
return if existing == widened
|
|
108
|
+
|
|
109
|
+
published.delete(name)
|
|
110
|
+
conflicted[name] = true
|
|
111
|
+
end
|
|
112
|
+
private_class_method :merge_publication
|
|
113
|
+
|
|
114
|
+
# The per-file constant table, typed through the same pre-pass the per-file path runs. Deliberately NOT
|
|
115
|
+
# a full `ScopeIndexer.index`: ADR-17 WD4 keeps the pre-eval pass a discovery walk, and the constant
|
|
116
|
+
# pre-pass (plus the declaration artifacts it needs to resolve in-file class references) is exactly the
|
|
117
|
+
# discovery facet this feature consumes. Fails soft to the empty table — a pre-eval file that cannot be
|
|
118
|
+
# read or parsed already surfaces a `pre-eval.parse-error` warning from {ProjectPatchedScanner}, and it
|
|
119
|
+
# must never break the run.
|
|
120
|
+
def file_constants(path, scope_builder:, target_ruby: nil, buffer: nil)
|
|
121
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
122
|
+
parse_result = Prism.parse(File.read(physical), filepath: path, version: target_ruby)
|
|
123
|
+
return EMPTY unless parse_result.errors.empty?
|
|
124
|
+
|
|
125
|
+
root = parse_result.value
|
|
126
|
+
ScopeIndexer.build_in_source_constants(root, declaration_seeded_scope(root, scope_builder.call(path)))
|
|
127
|
+
rescue StandardError
|
|
128
|
+
EMPTY
|
|
129
|
+
end
|
|
130
|
+
private_class_method :file_constants
|
|
131
|
+
|
|
132
|
+
# Mirrors the head of {ScopeIndexer.index}: seed the file's own declaration overrides and discovered
|
|
133
|
+
# classes onto the project-seeded scope so a `CONST = SomeClassDefinedRightHere.new` rvalue types the
|
|
134
|
+
# same way it does during real analysis.
|
|
135
|
+
def declaration_seeded_scope(root, scope)
|
|
136
|
+
declared_types, discovered_classes = ScopeIndexer.build_declaration_artifacts(root)
|
|
137
|
+
scope.with_discovery(
|
|
138
|
+
scope.discovery.with(
|
|
139
|
+
declared_types: declared_types,
|
|
140
|
+
discovered_classes: scope.discovered_classes.merge(discovered_classes)
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
private_class_method :declaration_seeded_scope
|
|
145
|
+
|
|
146
|
+
# The publication widening. Returns the type to publish, or `nil` to decline (the name keeps today's
|
|
147
|
+
# `Dynamic[top]` cross-file reading). See the module doc for why declining is the safe default.
|
|
148
|
+
def widen(type)
|
|
149
|
+
case type
|
|
150
|
+
when Type::Constant then widen_constant(type)
|
|
151
|
+
when Type::Refined then widen(type.base)
|
|
152
|
+
when Type::IntegerRange then Type::Combinator.nominal_of("Integer")
|
|
153
|
+
when Type::Tuple then Type::Combinator.nominal_of("Array")
|
|
154
|
+
when Type::HashShape then Type::Combinator.nominal_of("Hash")
|
|
155
|
+
when Type::Nominal then type.type_args.empty? ? type : Type::Combinator.nominal_of(type.class_name)
|
|
156
|
+
when Type::Singleton then type
|
|
157
|
+
when Type::DataInstance, Type::StructInstance then nominal_for_class_name(type.class_name)
|
|
158
|
+
when Type::Union then widen_union(type)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `Constant[v]` publishes `v`'s class. `nil` declines (see the module doc); so does a value whose class
|
|
163
|
+
# is anonymous, which has no name to publish under.
|
|
164
|
+
def widen_constant(type)
|
|
165
|
+
return nil if type.value.nil?
|
|
166
|
+
|
|
167
|
+
nominal_for_class_name(type.value.class.name)
|
|
168
|
+
end
|
|
169
|
+
private_class_method :widen_constant
|
|
170
|
+
|
|
171
|
+
def nominal_for_class_name(class_name)
|
|
172
|
+
return nil unless class_name.is_a?(String) && !class_name.empty?
|
|
173
|
+
|
|
174
|
+
Type::Combinator.nominal_of(class_name)
|
|
175
|
+
end
|
|
176
|
+
private_class_method :nominal_for_class_name
|
|
177
|
+
|
|
178
|
+
# A union publishes only when EVERY member widens: one unrecognised arm means the union's real extent is
|
|
179
|
+
# unknown, and a partial union would be narrower than the truth — the false-positive direction.
|
|
180
|
+
def widen_union(type)
|
|
181
|
+
widened = type.members.map { |member| widen(member) }
|
|
182
|
+
return nil if widened.any?(&:nil?)
|
|
183
|
+
|
|
184
|
+
Type::Combinator.union(*widened)
|
|
185
|
+
end
|
|
186
|
+
private_class_method :widen_union
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -7,6 +7,7 @@ require_relative "../type"
|
|
|
7
7
|
require_relative "../source/constant_path"
|
|
8
8
|
require_relative "../source/node_children"
|
|
9
9
|
require_relative "../cache/file_digest"
|
|
10
|
+
require_relative "anonymous_meta_class"
|
|
10
11
|
require_relative "def_handle"
|
|
11
12
|
require_relative "mutation_widening"
|
|
12
13
|
require_relative "narrowing"
|
|
@@ -87,10 +88,13 @@ module Rigor
|
|
|
87
88
|
# program and types its rvalue under a scope that carries the surrounding qualified prefix as `self_type`, so
|
|
88
89
|
# the rvalue typer sees in-class references resolve correctly. Multiple writes to the same qualified name union
|
|
89
90
|
# via `Type::Combinator.union`.
|
|
91
|
+
# Issue #352 — the per-file table merges OVER whatever the base scope already published (the
|
|
92
|
+
# `pre_eval:` constant seed `Runner#project_scope_seed_tables` applies). Same-file declarations are the
|
|
93
|
+
# most specific authority, exactly as `merged_classes` above resolves the same collision. Without the
|
|
94
|
+
# merge, the assignment below would silently drop the project seed on every file.
|
|
90
95
|
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
96
|
+
merged_constants = merge_seeded_constants(default_scope.in_source_constants, in_source_constants)
|
|
97
|
+
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(in_source_constants: merged_constants))
|
|
94
98
|
|
|
95
99
|
# Slice 7 phase 12. In-source method discovery. Walks every class/module body for `Prism::DefNode` and
|
|
96
100
|
# recognised `define_method` calls and records the introduced method names. `rigor check` consults the table to
|
|
@@ -133,7 +137,7 @@ module Rigor
|
|
|
133
137
|
# file_def_nodes]` so the caller can thread the def-node table into {#merge_project_method_indexes} without
|
|
134
138
|
# walking the file a second time.
|
|
135
139
|
def seed_discovered_methods(seeded_scope, default_scope, root)
|
|
136
|
-
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
140
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root, default_scope.source_path)
|
|
137
141
|
discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
|
|
138
142
|
scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
|
|
139
143
|
[scope, file_def_nodes]
|
|
@@ -161,7 +165,7 @@ module Rigor
|
|
|
161
165
|
build_discovered_singleton_def_nodes(root)
|
|
162
166
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
163
167
|
superclasses = default_scope.discovered_superclasses.merge(
|
|
164
|
-
build_discovered_superclasses(root)
|
|
168
|
+
build_discovered_superclasses(root, default_scope.source_path)
|
|
165
169
|
)
|
|
166
170
|
includes = default_scope.discovered_includes.merge(
|
|
167
171
|
build_discovered_includes(root)
|
|
@@ -1205,6 +1209,15 @@ module Rigor
|
|
|
1205
1209
|
accumulator.freeze
|
|
1206
1210
|
end
|
|
1207
1211
|
|
|
1212
|
+
# Issue #352 — folds the project-wide `pre_eval:` constant seed under this file's own table. Returns the
|
|
1213
|
+
# per-file table unchanged (same frozen object) when nothing was seeded, so a run without `pre_eval:`
|
|
1214
|
+
# constants allocates and compares exactly what it did before.
|
|
1215
|
+
def merge_seeded_constants(seeded, per_file)
|
|
1216
|
+
return per_file if seeded.nil? || seeded.empty?
|
|
1217
|
+
|
|
1218
|
+
seeded.merge(per_file).freeze
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1208
1221
|
def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
|
|
1209
1222
|
return unless node.is_a?(Prism::Node)
|
|
1210
1223
|
|
|
@@ -1266,10 +1279,10 @@ module Rigor
|
|
|
1266
1279
|
# `walk_methods` and `walk_def_nodes` had byte-identical class / module / singleton / meta-block descents (both
|
|
1267
1280
|
# stop at `DefNode`), so a single combined walk records both accumulators at once instead of traversing every file
|
|
1268
1281
|
# twice.
|
|
1269
|
-
def build_methods_and_def_nodes(root)
|
|
1282
|
+
def build_methods_and_def_nodes(root, source_path = nil)
|
|
1270
1283
|
methods = {}
|
|
1271
1284
|
def_nodes = {}
|
|
1272
|
-
walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
|
|
1285
|
+
walk_methods_and_def_nodes(root, [], false, methods, def_nodes, source_path)
|
|
1273
1286
|
apply_alias_def_nodes(root, def_nodes)
|
|
1274
1287
|
[methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
|
|
1275
1288
|
end
|
|
@@ -1315,7 +1328,8 @@ module Rigor
|
|
|
1315
1328
|
# right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
|
|
1316
1329
|
# carry no def / class node, so not descending them is byte-identical for `def_nodes` too). See
|
|
1317
1330
|
# {#build_methods_and_def_nodes}.
|
|
1318
|
-
def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc
|
|
1331
|
+
def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
|
|
1332
|
+
source_path = nil)
|
|
1319
1333
|
return unless node.is_a?(Prism::Node)
|
|
1320
1334
|
|
|
1321
1335
|
case node
|
|
@@ -1324,21 +1338,27 @@ module Rigor
|
|
|
1324
1338
|
if name
|
|
1325
1339
|
child_prefix = qualified_prefix + [name]
|
|
1326
1340
|
record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
|
|
1327
|
-
|
|
1341
|
+
if node.body
|
|
1342
|
+
walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc, source_path)
|
|
1343
|
+
end
|
|
1328
1344
|
return
|
|
1329
1345
|
end
|
|
1330
1346
|
when Prism::SingletonClassNode
|
|
1331
1347
|
if node.body
|
|
1332
1348
|
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
1333
1349
|
if singleton_prefix
|
|
1334
|
-
walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
|
|
1350
|
+
walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc, source_path)
|
|
1335
1351
|
return
|
|
1336
1352
|
end
|
|
1337
1353
|
end
|
|
1338
1354
|
when Prism::ConstantWriteNode
|
|
1339
1355
|
if meta_new_block_body(node)
|
|
1340
1356
|
child_prefix = qualified_prefix + [node.name.to_s]
|
|
1341
|
-
walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc
|
|
1357
|
+
walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc,
|
|
1358
|
+
source_path)
|
|
1359
|
+
# No anonymous registration here: the constant IS the name, and `StatementEvaluator` never routes a
|
|
1360
|
+
# constant-write rvalue through the block-body narrowing (its scope index still shows `self` as
|
|
1361
|
+
# `Dynamic[top]` inside such a body), so the two passes agree on the constant name alone.
|
|
1342
1362
|
return
|
|
1343
1363
|
end
|
|
1344
1364
|
when Prism::DefNode
|
|
@@ -1349,14 +1369,43 @@ module Rigor
|
|
|
1349
1369
|
record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
1350
1370
|
return
|
|
1351
1371
|
when Prism::CallNode
|
|
1352
|
-
|
|
1353
|
-
if
|
|
1354
|
-
|
|
1372
|
+
anonymous = record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
|
|
1373
|
+
if anonymous
|
|
1374
|
+
walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
|
|
1375
|
+
def_nodes_acc, source_path)
|
|
1376
|
+
return
|
|
1355
1377
|
end
|
|
1356
1378
|
end
|
|
1357
1379
|
|
|
1358
1380
|
node.rigor_each_child do |child|
|
|
1359
|
-
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc
|
|
1381
|
+
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
|
|
1382
|
+
source_path)
|
|
1383
|
+
end
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
# The `Prism::CallNode` leaf actions of {#walk_methods_and_def_nodes}: the `define_method` / `attr_*` macro
|
|
1387
|
+
# recorders, plus the {AnonymousMetaClass} name of a class-creating meta call carrying a block (nil for
|
|
1388
|
+
# every other call), which the caller uses to decide whether the block body needs the anonymous-class-body
|
|
1389
|
+
# descent.
|
|
1390
|
+
def record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
|
|
1391
|
+
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
1392
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if ATTR_MACROS.include?(node.name)
|
|
1393
|
+
AnonymousMetaClass.name_for(node, source_path)
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
# #319 — walks a `Class.new do ... end` / `Module.new do ... end` / `Struct.new(*sym) do ... end` /
|
|
1397
|
+
# `Data.define(*sym) do ... end` block body as the class body it is at runtime, keyed by the call site's
|
|
1398
|
+
# synthetic anonymous `name`; the call's other children (receiver, arguments) keep the enclosing prefix.
|
|
1399
|
+
def walk_anonymous_meta_block(call_node, name, qualified_prefix, in_singleton_class, methods_acc,
|
|
1400
|
+
def_nodes_acc, source_path)
|
|
1401
|
+
call_node.rigor_each_child do |child|
|
|
1402
|
+
if child.equal?(call_node.block)
|
|
1403
|
+
body = call_node.block.body
|
|
1404
|
+
walk_methods_and_def_nodes(body, [name], false, methods_acc, def_nodes_acc, source_path) if body
|
|
1405
|
+
else
|
|
1406
|
+
walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc,
|
|
1407
|
+
source_path)
|
|
1408
|
+
end
|
|
1360
1409
|
end
|
|
1361
1410
|
end
|
|
1362
1411
|
|
|
@@ -1366,21 +1415,38 @@ module Rigor
|
|
|
1366
1415
|
# (semantically `class << self`).
|
|
1367
1416
|
# - `class << Foo` not nested in `class Foo` returns `[Foo]`
|
|
1368
1417
|
# so methods defined inside register on Foo's singleton.
|
|
1418
|
+
# - `class << Foo = <expr>` (#320, the private-singleton-object
|
|
1419
|
+
# idiom) is the same case: Ruby evaluates the assignment, then
|
|
1420
|
+
# opens the singleton of the resulting object — which is the
|
|
1421
|
+
# object `Foo` now holds — so the body's methods are reachable
|
|
1422
|
+
# as `Foo.<name>` exactly as for a plain constant read.
|
|
1369
1423
|
# - Any other expression (variable, method call) returns nil
|
|
1370
1424
|
# so the walker falls through and skips the body.
|
|
1371
1425
|
def singleton_class_prefix(node, qualified_prefix)
|
|
1372
|
-
|
|
1373
|
-
|
|
1426
|
+
return qualified_prefix if node.expression.is_a?(Prism::SelfNode)
|
|
1427
|
+
|
|
1428
|
+
rendered = singleton_receiver_constant_name(node.expression)
|
|
1429
|
+
return nil unless rendered
|
|
1430
|
+
|
|
1431
|
+
if !qualified_prefix.empty? && qualified_prefix.last == rendered
|
|
1374
1432
|
qualified_prefix
|
|
1375
|
-
|
|
1376
|
-
rendered
|
|
1377
|
-
|
|
1433
|
+
else
|
|
1434
|
+
rendered.split("::")
|
|
1435
|
+
end
|
|
1436
|
+
end
|
|
1378
1437
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1438
|
+
# The constant a `class << X` operand names, or nil when the operand is not constant-shaped. Both the
|
|
1439
|
+
# read spellings (`Foo`, `A::Foo`, `::Foo`) and the two constant-*write* spellings (`Foo = expr`,
|
|
1440
|
+
# `A::Foo = expr`) resolve to the same unqualified rendering the read branch uses, so a body opened on
|
|
1441
|
+
# the assignment and one opened on a later plain read land on the same table key.
|
|
1442
|
+
def singleton_receiver_constant_name(expression)
|
|
1443
|
+
case expression
|
|
1444
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1445
|
+
Source::ConstantPath.qualified_name(expression)
|
|
1446
|
+
when Prism::ConstantWriteNode
|
|
1447
|
+
expression.name.to_s
|
|
1448
|
+
when Prism::ConstantPathWriteNode
|
|
1449
|
+
Source::ConstantPath.qualified_name(expression.target)
|
|
1384
1450
|
end
|
|
1385
1451
|
end
|
|
1386
1452
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
@@ -1483,6 +1549,24 @@ module Rigor
|
|
|
1483
1549
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
1484
1550
|
accumulator[class_name] ||= {}
|
|
1485
1551
|
accumulator[class_name][def_node.name] = def_node
|
|
1552
|
+
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
1556
|
+
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
1557
|
+
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
1558
|
+
# (see {TOP_LEVEL_DEF_KEY}), and it is what lets an implicit-self call elsewhere in the file resolve
|
|
1559
|
+
# against a method the anonymous module contributes to some other object's `self` — the
|
|
1560
|
+
# `Module.new { def start; end }` mixed into a spawned actor environment, then called from the sibling
|
|
1561
|
+
# `spawn(...) { start }` block. Giving the body a class of its own must not silently retract it: the call
|
|
1562
|
+
# resolves at runtime, and `call.unresolved-toplevel` firing on it would be a new false positive traded
|
|
1563
|
+
# for the ones this change retires. Never clobbers a real top-level `def` of the same name.
|
|
1564
|
+
def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
|
|
1565
|
+
return unless qualified_prefix.length == 1
|
|
1566
|
+
return unless Type::AnonymousClassName.match?(qualified_prefix.first)
|
|
1567
|
+
|
|
1568
|
+
table = (accumulator[TOP_LEVEL_DEF_KEY] ||= {})
|
|
1569
|
+
table[def_node.name] ||= def_node
|
|
1486
1570
|
end
|
|
1487
1571
|
|
|
1488
1572
|
# Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
|
|
@@ -1617,16 +1701,18 @@ module Rigor
|
|
|
1617
1701
|
# `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
|
|
1618
1702
|
# non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
|
|
1619
1703
|
# site against the subclass's lexical nesting — see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
1620
|
-
def build_discovered_superclasses(root)
|
|
1704
|
+
def build_discovered_superclasses(root, source_path = nil)
|
|
1621
1705
|
accumulator = {}
|
|
1622
|
-
walk_class_superclasses(root, [], accumulator)
|
|
1706
|
+
walk_class_superclasses(root, [], accumulator, source_path)
|
|
1623
1707
|
accumulator.freeze
|
|
1624
1708
|
end
|
|
1625
1709
|
|
|
1626
|
-
def walk_class_superclasses(node, qualified_prefix, accumulator)
|
|
1710
|
+
def walk_class_superclasses(node, qualified_prefix, accumulator, source_path = nil)
|
|
1627
1711
|
return unless node.is_a?(Prism::Node)
|
|
1628
1712
|
|
|
1629
1713
|
case node
|
|
1714
|
+
when Prism::CallNode
|
|
1715
|
+
record_anonymous_meta_superclass(node, accumulator, source_path)
|
|
1630
1716
|
when Prism::ClassNode
|
|
1631
1717
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1632
1718
|
if name
|
|
@@ -1645,10 +1731,27 @@ module Rigor
|
|
|
1645
1731
|
end
|
|
1646
1732
|
|
|
1647
1733
|
node.rigor_each_child do |child|
|
|
1648
|
-
walk_class_superclasses(child, qualified_prefix, accumulator)
|
|
1734
|
+
walk_class_superclasses(child, qualified_prefix, accumulator, source_path)
|
|
1649
1735
|
end
|
|
1650
1736
|
end
|
|
1651
1737
|
|
|
1738
|
+
# #319 — `Class.new(Parent) do ... end` names its superclass in the first positional. Recording it under the
|
|
1739
|
+
# call site's anonymous name keeps `Parent`'s surface reachable from the block body (whose `self_type` is now
|
|
1740
|
+
# `Singleton[<anonymous>]`) and from an instance of the resulting class, so giving the anonymous class an
|
|
1741
|
+
# identity does not cost the inheritance the old `Singleton[Parent]` answer carried for free.
|
|
1742
|
+
def record_anonymous_meta_superclass(call_node, accumulator, source_path)
|
|
1743
|
+
return unless AnonymousMetaClass.block_form_receiver(call_node) == :Class
|
|
1744
|
+
|
|
1745
|
+
arg = call_node.arguments&.arguments&.first
|
|
1746
|
+
return if arg.nil?
|
|
1747
|
+
|
|
1748
|
+
superclass = Source::ConstantPath.qualified_name(arg)
|
|
1749
|
+
return if superclass.nil?
|
|
1750
|
+
|
|
1751
|
+
name = AnonymousMetaClass.name_for(call_node, source_path)
|
|
1752
|
+
accumulator[name] = superclass if name
|
|
1753
|
+
end
|
|
1754
|
+
|
|
1652
1755
|
# ADR-48 — per qualified class name -> ordered `Data.define` member-name list, for both the named-subclass form
|
|
1653
1756
|
# (`class Point < Data.define(:x, :y)`) and the constant-assigned form (`Point = Data.define(:x, :y)`). Only
|
|
1654
1757
|
# `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
|
|
@@ -2511,7 +2614,7 @@ module Rigor
|
|
|
2511
2614
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
2512
2615
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
2513
2616
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
2514
|
-
file_methods, file_def_nodes = build_methods_and_def_nodes(root)
|
|
2617
|
+
file_methods, file_def_nodes = build_methods_and_def_nodes(root, path)
|
|
2515
2618
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
2516
2619
|
# ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
|
|
2517
2620
|
# the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
|
|
@@ -2519,7 +2622,7 @@ module Rigor
|
|
|
2519
2622
|
# silently degrading to the file's full ancestry closure.
|
|
2520
2623
|
merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
|
|
2521
2624
|
build_discovered_singleton_def_nodes(root))
|
|
2522
|
-
superclasses = build_discovered_superclasses(root)
|
|
2625
|
+
superclasses = build_discovered_superclasses(root, path)
|
|
2523
2626
|
includes = build_discovered_includes(root)
|
|
2524
2627
|
acc[:superclasses].merge!(superclasses)
|
|
2525
2628
|
includes.each do |class_name, mods|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../source/constant_path"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #320 — the *private-singleton-object* idiom: a constant holds a plain object whose singleton
|
|
8
|
+
# class carries the methods.
|
|
9
|
+
#
|
|
10
|
+
# class << Merger = Object.new
|
|
11
|
+
# def merge_attributes!(a, b) = a
|
|
12
|
+
# end
|
|
13
|
+
# Merger.merge_attributes!({}, {})
|
|
14
|
+
#
|
|
15
|
+
# {ScopeIndexer} already records such a body under the constant's own name with kind `:singleton` — that
|
|
16
|
+
# keying is correct for both spellings, since `Foo.bar` means "call `bar` on the value of constant `Foo`"
|
|
17
|
+
# whether the value is a class object or an ordinary object. The gap is on the *receiver* side: when the
|
|
18
|
+
# constant names a class or module the read types as `Singleton[Foo]` and dispatch recovers the name from
|
|
19
|
+
# the type, but when it holds an ordinary object the read types as `Nominal[Object]` and the name is
|
|
20
|
+
# gone — so every method in the body is invisible and `call.undefined-method` fires on working code.
|
|
21
|
+
#
|
|
22
|
+
# This module recovers the name from the receiver *syntax* instead, for exactly the receivers whose type
|
|
23
|
+
# has lost it. `Singleton` receivers are excluded so the established class/module path keeps its
|
|
24
|
+
# precedence; a receiver whose constant has no recorded singleton body yields nil, so an undefined method
|
|
25
|
+
# on such a constant (`Merger.nope`) still fires.
|
|
26
|
+
module SingletonObjectConstant
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# The constant name a call's receiver names, when that receiver is a constant reference whose type is
|
|
30
|
+
# not already a `Singleton` carrier. Nil for every other receiver shape.
|
|
31
|
+
def receiver_constant_name(call_node, receiver_type)
|
|
32
|
+
return nil if receiver_type.is_a?(Type::Singleton)
|
|
33
|
+
|
|
34
|
+
receiver = call_node.receiver
|
|
35
|
+
return nil unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
36
|
+
|
|
37
|
+
Source::ConstantPath.qualified_name(receiver)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# True when the project recorded `method_name` on the singleton side of the constant this call's
|
|
41
|
+
# receiver names. The suppression probe for `call.undefined-method`.
|
|
42
|
+
def recorded?(call_node, receiver_type, method_name, scope)
|
|
43
|
+
return false if scope.nil?
|
|
44
|
+
|
|
45
|
+
name = receiver_constant_name(call_node, receiver_type)
|
|
46
|
+
return false if name.nil?
|
|
47
|
+
|
|
48
|
+
scope.discovered_method?(name, method_name, :singleton)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The `Prism::DefNode` for `method_name` in the constant's singleton body, or nil. The inference tier's
|
|
52
|
+
# entry point.
|
|
53
|
+
def def_node_for(call_node, receiver_type, method_name, scope)
|
|
54
|
+
return nil if scope.nil?
|
|
55
|
+
|
|
56
|
+
name = receiver_constant_name(call_node, receiver_type)
|
|
57
|
+
return nil if name.nil?
|
|
58
|
+
|
|
59
|
+
scope.singleton_def_for(name, method_name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "../analysis/fact_store"
|
|
|
8
8
|
require_relative "../source/node_walker"
|
|
9
9
|
require_relative "../source/node_children"
|
|
10
10
|
require_relative "../source/constant_path"
|
|
11
|
+
require_relative "anonymous_meta_class"
|
|
11
12
|
require_relative "block_parameter_binder"
|
|
12
13
|
require_relative "body_fixpoint"
|
|
13
14
|
require_relative "dynamic_origin"
|
|
@@ -20,6 +21,7 @@ require_relative "method_parameter_binder"
|
|
|
20
21
|
require_relative "multi_target_binder"
|
|
21
22
|
require_relative "mutation_widening"
|
|
22
23
|
require_relative "narrowing"
|
|
24
|
+
require_relative "optimistic_origin"
|
|
23
25
|
|
|
24
26
|
module Rigor
|
|
25
27
|
module Inference
|
|
@@ -259,17 +261,11 @@ module Rigor
|
|
|
259
261
|
optimistic_origin_for(value_node, scope_after_rhs)
|
|
260
262
|
end
|
|
261
263
|
|
|
262
|
-
# The effective optimistic-nil-free cause of an expression
|
|
263
|
-
#
|
|
264
|
-
#
|
|
264
|
+
# The effective optimistic-nil-free cause of an expression. {Inference::OptimisticOrigin.resolve} owns
|
|
265
|
+
# the judgment — the mark on the node itself, the binding a bare local / ivar read resolves through, and
|
|
266
|
+
# the predicate-fold derivation of issue #313.
|
|
265
267
|
def optimistic_origin_for(node, scope)
|
|
266
|
-
|
|
267
|
-
return recorded if recorded
|
|
268
|
-
|
|
269
|
-
case node
|
|
270
|
-
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
271
|
-
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
272
|
-
end
|
|
268
|
+
Inference::OptimisticOrigin.resolve(node, scope)
|
|
273
269
|
end
|
|
274
270
|
|
|
275
271
|
# ADR-82 WD1 — the {Inference::DynamicOrigin} cause to propagate onto a local / ivar being bound to `rhs`.
|
|
@@ -1827,7 +1823,20 @@ module Rigor
|
|
|
1827
1823
|
return unless block.is_a?(Prism::BlockNode)
|
|
1828
1824
|
|
|
1829
1825
|
block_entry = build_block_entry_scope(node, block)
|
|
1830
|
-
|
|
1826
|
+
# #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
|
|
1827
|
+
# semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
|
|
1828
|
+
# and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
|
|
1829
|
+
# class-context a `class Foo ... end` body gets, keyed by the call site's anonymous name — the name
|
|
1830
|
+
# `ScopeIndexer` registered the body's methods under. Without it the body inherits the enclosing
|
|
1831
|
+
# scope, and at file top level that means `Scope#toplevel?` (a nil `self_type`) reports every macro
|
|
1832
|
+
# call in the body as `call.unresolved-toplevel`.
|
|
1833
|
+
#
|
|
1834
|
+
# Outer locals stay visible: unlike a `class` keyword body, the block is a closure.
|
|
1835
|
+
anonymous = AnonymousMetaClass.name_for(node, scope.source_path)
|
|
1836
|
+
return sub_eval(block, block_entry) if anonymous.nil?
|
|
1837
|
+
|
|
1838
|
+
sub_eval(block, block_entry.with_self_type(Type::Combinator.singleton_of(anonymous)),
|
|
1839
|
+
class_context: [ClassFrame.new(name: anonymous, singleton: false)])
|
|
1831
1840
|
end
|
|
1832
1841
|
|
|
1833
1842
|
# Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
|
|
@@ -2504,7 +2513,11 @@ module Rigor
|
|
|
2504
2513
|
def build_block_entry_scope(call_node, block_node)
|
|
2505
2514
|
expected = expected_block_param_types_for(call_node)
|
|
2506
2515
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
2507
|
-
|
|
2516
|
+
# Issue #316 — every block body enters with `self` unmodelled (`Scope#entering_opaque_block`); the
|
|
2517
|
+
# yielding method, not the lexical context, decides what `self` is, and Rigor does not track it.
|
|
2518
|
+
scope_with_params = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
|
|
2519
|
+
acc.with_local(name, type)
|
|
2520
|
+
end
|
|
2508
2521
|
block_local_names(block_node).reduce(scope_with_params) do |acc, name|
|
|
2509
2522
|
acc.with_local(name, Type::Combinator.constant_of(nil))
|
|
2510
2523
|
end
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -710,6 +710,30 @@ module Rigor
|
|
|
710
710
|
manifest.protocol_contracts
|
|
711
711
|
end
|
|
712
712
|
|
|
713
|
+
# ADR-103 WD6 / WD10 (#387) — the plugin's effect contributions. Each defaults to the manifest field
|
|
714
|
+
# and each MAY be overridden to fold in per-project facts, exactly as {#protocol_contracts} may.
|
|
715
|
+
#
|
|
716
|
+
# {#effect_attributions} is the one that needs it: a Rails app's ActiveJob transport is whatever
|
|
717
|
+
# `config.active_job.queue_adapter` names, so rigor-activejob answers `io.db.write` for Solid Queue
|
|
718
|
+
# and `io.net` for Sidekiq by reading the project's own configuration. That read is a plugin-side I/O
|
|
719
|
+
# boundary read and must not happen on a run with effects off — {Registry#effect_contributions} is
|
|
720
|
+
# lazy for exactly that reason, and nothing asks unless the project has an `effects:` block.
|
|
721
|
+
def effect_attributions
|
|
722
|
+
manifest.effect_attributions
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def effect_edges
|
|
726
|
+
manifest.effect_edges
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def effect_labels
|
|
730
|
+
manifest.effect_labels
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def effect_entry_points
|
|
734
|
+
manifest.effect_entry_points
|
|
735
|
+
end
|
|
736
|
+
|
|
713
737
|
# ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}. Memoised so the boundary's accumulated `FileEntry`
|
|
714
738
|
# rows persist across producer invocations within the same plugin instance and feed cache invalidation
|
|
715
739
|
# via `cache_for`.
|