rigortype 0.3.3 → 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 +6 -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/rule_ids.rb +28 -2
- 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 +158 -0
- 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/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/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- 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/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- 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/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.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +79 -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
|
|
@@ -13,6 +13,7 @@ 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"
|
|
@@ -1134,6 +1135,11 @@ module Rigor
|
|
|
1134
1135
|
arg_types = call_arg_types(node)
|
|
1135
1136
|
block_type = block_return_type_for(node, receiver, arg_types)
|
|
1136
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
|
+
|
|
1137
1143
|
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1138
1144
|
return local_def_result if local_def_result
|
|
1139
1145
|
|
|
@@ -1181,11 +1187,19 @@ module Rigor
|
|
|
1181
1187
|
# semantic outcome, not a fail-soft compromise, so it MUST NOT record a tracer event.
|
|
1182
1188
|
return inherit_receiver_origin(node) if receiver.is_a?(Type::Dynamic)
|
|
1183
1189
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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)
|
|
1188
1201
|
record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
|
|
1202
|
+
Effects::Collector.record_unresolved(node, scope.source_path) if Effects::Collector.active?
|
|
1189
1203
|
|
|
1190
1204
|
fallback_for(node, family: :prism)
|
|
1191
1205
|
end
|
|
@@ -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
|
|
@@ -88,10 +88,13 @@ module Rigor
|
|
|
88
88
|
# program and types its rvalue under a scope that carries the surrounding qualified prefix as `self_type`, so
|
|
89
89
|
# the rvalue typer sees in-class references resolve correctly. Multiple writes to the same qualified name union
|
|
90
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.
|
|
91
95
|
in_source_constants = build_in_source_constants(root, seeded_scope)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
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))
|
|
95
98
|
|
|
96
99
|
# Slice 7 phase 12. In-source method discovery. Walks every class/module body for `Prism::DefNode` and
|
|
97
100
|
# recognised `define_method` calls and records the introduced method names. `rigor check` consults the table to
|
|
@@ -1206,6 +1209,15 @@ module Rigor
|
|
|
1206
1209
|
accumulator.freeze
|
|
1207
1210
|
end
|
|
1208
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
|
+
|
|
1209
1221
|
def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
|
|
1210
1222
|
return unless node.is_a?(Prism::Node)
|
|
1211
1223
|
|
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`.
|