rigortype 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +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 +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -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 +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -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 +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -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 +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -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 +139 -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 +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -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 +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -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 +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -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 +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -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 +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -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 +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- 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-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -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 +84 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "unknown_label_report"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Effects
|
|
7
|
+
# Turns the declarations a run carries into `effect.unknown-label` findings (ADR-103 WD1 / WD14;
|
|
8
|
+
# #384). Pure: it reads envelopes and config values and answers value objects, never diagnostics
|
|
9
|
+
# and never the filesystem — positioning the finding is the caller's job, because only the caller
|
|
10
|
+
# knows whether a `virtual:` buffer has to be mapped back to the `.rb` the author wrote in.
|
|
11
|
+
#
|
|
12
|
+
# Two producers today, one shape:
|
|
13
|
+
#
|
|
14
|
+
# - {.for_envelopes} — every `%a{pure}` / `%a{rigor:v1:effect …}` the envelope scanner read, in
|
|
15
|
+
# `.rbs` or through rbs-inline. One finding per (declaration, unrecognised token); a
|
|
16
|
+
# `def self?.x` member declares two keys off ONE annotation, so findings are deduplicated by
|
|
17
|
+
# where they were written rather than by which method they bind.
|
|
18
|
+
# - {.for_config} — a `.rigor.yml` label list: `effects.tolerated:`, `effects.labels:`, each
|
|
19
|
+
# `envelopes[].effect` and each `attribution:` value. The key path is a parameter rather than a
|
|
20
|
+
# literal precisely because there are four of them and each names a different place to edit.
|
|
21
|
+
module UnknownLabelCheck
|
|
22
|
+
# `location` is the `path:line` the declaration was written at, or nil for a config value —
|
|
23
|
+
# the caller resolves both. `spelling` is the author's own annotation text, which is also how a
|
|
24
|
+
# finding read out of a synthesized buffer is found again in the Ruby source.
|
|
25
|
+
Finding = Data.define(:token, :subject, :consequence, :location, :spelling, :report) do
|
|
26
|
+
def message = report.message(subject: subject, consequence: consequence)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
ENVELOPE_CONSEQUENCE = "the annotation now bounds nothing"
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param method_envelopes [Hash{String => Envelope}]
|
|
34
|
+
# @param class_envelopes [Hash{String => Envelope}]
|
|
35
|
+
# @param registry [Rigor::Effects::Registry]
|
|
36
|
+
# @return [Array<Finding>]
|
|
37
|
+
def for_envelopes(method_envelopes:, class_envelopes:, registry:)
|
|
38
|
+
seen = {}
|
|
39
|
+
declarations(method_envelopes, class_envelopes).each do |envelope|
|
|
40
|
+
envelope.unknown_labels.each do |token|
|
|
41
|
+
report = UnknownLabelReport.for(
|
|
42
|
+
token: token, registry: registry, siblings: envelope.declared_labels
|
|
43
|
+
)
|
|
44
|
+
next if report.nil?
|
|
45
|
+
|
|
46
|
+
seen[[envelope.location, envelope.spelling, token]] ||= envelope_finding(envelope, token, report)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
seen.values
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param labels [Array<String>] the list as written, in source order.
|
|
53
|
+
# @param key_path [String] the `.rigor.yml` key, for the message (`effects.tolerated`).
|
|
54
|
+
# @param consequence [String] what the degradation cost, for the message.
|
|
55
|
+
# @return [Array<Finding>]
|
|
56
|
+
def for_config(labels:, key_path:, consequence:, registry:)
|
|
57
|
+
tokens = Array(labels).map(&:to_s)
|
|
58
|
+
tokens.filter_map do |token|
|
|
59
|
+
report = UnknownLabelReport.for(token: token, registry: registry, siblings: tokens)
|
|
60
|
+
next if report.nil?
|
|
61
|
+
|
|
62
|
+
Finding.new(
|
|
63
|
+
token: token, subject: "`#{key_path}:` in .rigor.yml", consequence: consequence,
|
|
64
|
+
location: nil, spelling: nil, report: report
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Method-level declarations first, so a method that also carries a class-level bound reports
|
|
70
|
+
# against its own annotation rather than against the distributed one.
|
|
71
|
+
def declarations(method_envelopes, class_envelopes)
|
|
72
|
+
method_envelopes.values + class_envelopes.values
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def envelope_finding(envelope, token, report)
|
|
76
|
+
Finding.new(
|
|
77
|
+
token: token, subject: "Effect envelope on #{envelope.owner_key}",
|
|
78
|
+
consequence: ENVELOPE_CONSEQUENCE, location: envelope.location,
|
|
79
|
+
spelling: envelope.spelling, report: report
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method :declarations, :envelope_finding
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -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
|