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,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# One entry of a plugin's `effect_edges:` — a call graph edge the *syntax* does not contain but the
|
|
6
|
+
# framework does (ADR-103 WD10; design note § 11.2 "Framework edges").
|
|
7
|
+
#
|
|
8
|
+
# `save` runs the class body's `before_save :normalize` and its validators; `Job.perform_now` runs
|
|
9
|
+
# `Job#perform`; `UserMailer.welcome(u)` runs `UserMailer#welcome`. None of those is a call the
|
|
10
|
+
# analyzer can see, and all three are ordinary synchronous in-process control flow — so a plugin that
|
|
11
|
+
# models the framework contributes **edges**, not only labels.
|
|
12
|
+
#
|
|
13
|
+
# What a plugin must NOT contribute is the deferred half: `perform_later` never edges to `perform`,
|
|
14
|
+
# because the body runs in another process on another stack and the caller's code does not contain it
|
|
15
|
+
# (ADR-103 WD4, "attribution follows the code, not the clock"). The enum below has no spelling for it,
|
|
16
|
+
# which is the enforcement.
|
|
17
|
+
#
|
|
18
|
+
# ## Why a fixed enum rather than a callback
|
|
19
|
+
#
|
|
20
|
+
# A plugin supplies **parameters**; the engine owns the strategy. A block would have to run inside the
|
|
21
|
+
# per-file effect scan — the one place ADR-103 WD13 forbids anything that resolves, walks or types —
|
|
22
|
+
# and would not survive the fork-pool / Ractor boundary the collection window crosses. The enum keeps
|
|
23
|
+
# the plugin's contribution declarative, Marshal-clean and reviewable, and keeps every walk on the
|
|
24
|
+
# engine's side of the contract.
|
|
25
|
+
#
|
|
26
|
+
# ## The strategies
|
|
27
|
+
#
|
|
28
|
+
# - `:activerecord_callbacks` — `receiver:` is the ActiveRecord base class. On every project class
|
|
29
|
+
# whose ancestry reaches it, the engine reads the class body's callback and validation macros
|
|
30
|
+
# (`before_save :sym`, `validate :sym`, `after_commit :sym`, …) and synthesises the persistence
|
|
31
|
+
# selectors (`save`, `create!`, `destroy`, `valid?`, …) as effect units edged to those methods.
|
|
32
|
+
# `validates … uniqueness: true` additionally contributes an `io.db.read` origin, because the
|
|
33
|
+
# uniqueness check IS a query.
|
|
34
|
+
# - `:perform_now` — `receiver:` is the job base class. `Job.perform_now(…)` on a project subclass
|
|
35
|
+
# reaches `Job#perform`.
|
|
36
|
+
# - `:mailer_body` — `receiver:` is the mailer base class. `UserMailer.welcome(u)` on a project
|
|
37
|
+
# subclass reaches `UserMailer#welcome`, the class-method-to-instance mapping ActionMailer performs
|
|
38
|
+
# at run time.
|
|
39
|
+
#
|
|
40
|
+
# `method:` and `singleton:` are carried for a future strategy that keys on one selector; the three
|
|
41
|
+
# above read only `receiver:`.
|
|
42
|
+
class EffectEdge
|
|
43
|
+
# Every strategy the engine implements. A `target:` outside this set is a manifest error, so a
|
|
44
|
+
# plugin cannot silently declare an edge nothing honours.
|
|
45
|
+
TARGETS = %i[activerecord_callbacks perform_now mailer_body].freeze
|
|
46
|
+
|
|
47
|
+
attr_reader :receiver, :method, :singleton, :target, :why
|
|
48
|
+
|
|
49
|
+
def initialize(receiver:, target:, why:, method: nil, singleton: false)
|
|
50
|
+
@receiver = validate_receiver!(receiver)
|
|
51
|
+
@target = validate_target!(target)
|
|
52
|
+
@method = method&.to_sym
|
|
53
|
+
@singleton = singleton ? true : false
|
|
54
|
+
@why = validate_why!(why)
|
|
55
|
+
freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
{
|
|
60
|
+
"receiver" => @receiver, "method" => @method&.to_s, "singleton" => @singleton,
|
|
61
|
+
"target" => @target.to_s
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ==(other)
|
|
66
|
+
other.is_a?(EffectEdge) && to_h == other.to_h
|
|
67
|
+
end
|
|
68
|
+
alias eql? ==
|
|
69
|
+
|
|
70
|
+
def hash
|
|
71
|
+
to_h.hash
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def validate_receiver!(receiver)
|
|
77
|
+
value = receiver.to_s
|
|
78
|
+
unless EffectAttribution::CLASS_NAME.match?(value)
|
|
79
|
+
raise ArgumentError, "effect edge receiver must be a class name, got #{receiver.inspect}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
value.dup.freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_target!(target)
|
|
86
|
+
value = target.to_sym
|
|
87
|
+
return value if TARGETS.include?(value)
|
|
88
|
+
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"effect edge target must be one of #{TARGETS.inspect}, got #{target.inspect}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_why!(why)
|
|
94
|
+
value = why.to_s
|
|
95
|
+
raise ArgumentError, "effect edge #{@receiver} -> #{@target} needs a `why:` justification" if value.empty?
|
|
96
|
+
|
|
97
|
+
value.dup.freeze
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# One entry of a plugin's `effect_entry_points:` — a **named** set of globs that
|
|
6
|
+
# `effects.snapshot.reach:` may adopt by name (ADR-103 WD14).
|
|
7
|
+
#
|
|
8
|
+
# `reach:` is "whose transitive footprint does the snapshot record", and the honest answer for a Rails
|
|
9
|
+
# app — controller actions, `perform`, mailer methods, channel methods — is a fact about the framework
|
|
10
|
+
# rather than about the project. So the framework's plugin names it and the project adopts it:
|
|
11
|
+
#
|
|
12
|
+
# effects:
|
|
13
|
+
# snapshot:
|
|
14
|
+
# reach: [rails]
|
|
15
|
+
#
|
|
16
|
+
# A preset is a **glob** set, matched against the project-relative file a method is defined in, with
|
|
17
|
+
# the `rigor unused --entry-point` semantics {Rigor::Effects::EntryPoints} already implements. Globs
|
|
18
|
+
# rather than a class-ancestry filter, deliberately: `reach:` is resolved from the snapshot's own
|
|
19
|
+
# method table, which carries a defining path per key and no ancestry — and a Rails app's layout IS
|
|
20
|
+
# the ancestry, which is why the convention exists at all.
|
|
21
|
+
class EffectEntryPoints
|
|
22
|
+
attr_reader :name, :globs, :why
|
|
23
|
+
|
|
24
|
+
# @param name [String, Symbol] the token `reach:` adopts. Must satisfy
|
|
25
|
+
# {Rigor::Effects::EntryPoints::NAME_PATTERN}, checked at registration.
|
|
26
|
+
# @param globs [Array<String>] project-relative path globs
|
|
27
|
+
# @param why [String] what the preset stands for, for the plugin's README and `rigor effects`' help
|
|
28
|
+
def initialize(name:, globs:, why: "")
|
|
29
|
+
@name = name.to_s.dup.freeze
|
|
30
|
+
@globs = Array(globs).map { |glob| glob.to_s.dup.freeze }.uniq.sort.freeze
|
|
31
|
+
raise ArgumentError, "effect entry-point preset #{@name.inspect} declares no globs" if @globs.empty?
|
|
32
|
+
|
|
33
|
+
@why = why.to_s.dup.freeze
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{ "name" => @name, "globs" => @globs }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
other.is_a?(EffectEntryPoints) && to_h == other.to_h
|
|
43
|
+
end
|
|
44
|
+
alias eql? ==
|
|
45
|
+
|
|
46
|
+
def hash
|
|
47
|
+
to_h.hash
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
# Which plugin ids the engine itself ships (ADR-103 WD6).
|
|
6
|
+
#
|
|
7
|
+
# WD6's trust ladder grants two things to a **first-party bundled** plugin and to nothing else: it may
|
|
8
|
+
# open the effect-label root of the framework it models rather than one named after itself
|
|
9
|
+
# (`rails.*`, not `activerecord.*`), and its `effect_attributions:` may carry `discharge: true`. Both
|
|
10
|
+
# rest on the same fact — a bundled plugin's framework knowledge is versioned with the engine, reviewed
|
|
11
|
+
# in this repository, and gated by `make check-plugins`, which is exactly the standing a gem's shipped
|
|
12
|
+
# RBS has.
|
|
13
|
+
#
|
|
14
|
+
# The answer is **derived, never listed**: a plugin is first-party when the engine bundles the gem that
|
|
15
|
+
# would register it, which is the same question {Loader.bundled_plugin_path} already answers when it
|
|
16
|
+
# decides whether to require a plugin by its engine-anchored path or by gem name. Every bundled plugin
|
|
17
|
+
# id equals its gem name minus the `rigor-` prefix (pinned by spec), so the id is enough to ask. A list
|
|
18
|
+
# would be a second source of truth to keep in sync with `plugins/`, and the first drift would silently
|
|
19
|
+
# demote a plugin's rows to the tainted lane.
|
|
20
|
+
#
|
|
21
|
+
# This is a trust *ladder*, not a sandbox. ADR-2 settles that plugins are trusted gems the user
|
|
22
|
+
# selected and chooses documentation over forced isolation; a plugin determined to spoof a bundled id
|
|
23
|
+
# is out of scope there and stays out of scope here. What the predicate buys is that an ordinary
|
|
24
|
+
# third-party plugin cannot accidentally claim authority it was never granted.
|
|
25
|
+
module FirstParty
|
|
26
|
+
# The prefix a bundled plugin's gem name carries over its manifest id.
|
|
27
|
+
GEM_PREFIX = "rigor-"
|
|
28
|
+
|
|
29
|
+
@memo = {}
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Whether `id` names a plugin the engine bundles.
|
|
33
|
+
#
|
|
34
|
+
# Memoised: the answer is a `File.file?` on a path fixed for the process, and the effect surfaces
|
|
35
|
+
# ask it once per plugin per run — but a plugin's own specs ask it far more often than that.
|
|
36
|
+
def bundled?(id)
|
|
37
|
+
key = id.to_s
|
|
38
|
+
return false if key.empty?
|
|
39
|
+
|
|
40
|
+
return @memo[key] if @memo.key?(key)
|
|
41
|
+
|
|
42
|
+
# Required here rather than at the top of the file: {Loader} pulls in the plugin {Registry},
|
|
43
|
+
# which pulls in {Manifest}, which asks this module the question — a `require` cycle that would
|
|
44
|
+
# leave `Loader` undefined half-way through load. The require is idempotent and this method is
|
|
45
|
+
# memoised, so it costs one `$LOADED_FEATURES` probe per process.
|
|
46
|
+
require_relative "loader"
|
|
47
|
+
@memo[key] = !Loader.bundled_plugin_path("#{GEM_PREFIX}#{key}").nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Drops the memo. For specs that stub the engine root only.
|
|
51
|
+
def reset!
|
|
52
|
+
@memo = {}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -43,8 +43,11 @@ module Rigor
|
|
|
43
43
|
@target_bundle_root
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Frozen so the value stays `Ractor.shareable?`: a Ractor-pool worker READS this (its Environment
|
|
47
|
+
# build resolves the same root and skips the write — #414), and reading an unshareable value out of
|
|
48
|
+
# a module ivar from a non-main Ractor is the same `Ractor::IsolationError` as writing one.
|
|
46
49
|
def target_bundle_root=(root)
|
|
47
|
-
@target_bundle_root = root.nil? ? nil : File.expand_path(root.to_s)
|
|
50
|
+
@target_bundle_root = root.nil? ? nil : File.expand_path(root.to_s).freeze
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# Requires `feature`, falling back to the analyzed project's bundler install tree. The fallback
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require_relative "../inference/hkt_registry"
|
|
4
4
|
require_relative "protocol_contract"
|
|
5
5
|
require_relative "additional_initializer"
|
|
6
|
+
require_relative "effect_attribution"
|
|
7
|
+
require_relative "effect_edge"
|
|
8
|
+
require_relative "effect_ancestry"
|
|
9
|
+
require_relative "effect_entry_points"
|
|
10
|
+
require_relative "first_party"
|
|
6
11
|
|
|
7
12
|
module Rigor
|
|
8
13
|
module Plugin
|
|
@@ -33,11 +38,17 @@ module Rigor
|
|
|
33
38
|
end
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
# ADR-103 WD2 — what a plugin's `effect_root:` may be spelled as: one label segment, the shape
|
|
42
|
+
# {Rigor::Effects::Label} accepts as a root.
|
|
43
|
+
VALID_EFFECT_ROOT = /\A[a-z][a-z0-9_]*\z/
|
|
44
|
+
|
|
36
45
|
attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
|
|
37
46
|
:owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
|
|
38
47
|
:heredoc_templates, :nested_class_templates, :trait_registries,
|
|
39
48
|
:hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
|
|
40
|
-
:source_rbs_synthesizer, :additional_initializers
|
|
49
|
+
:source_rbs_synthesizer, :additional_initializers,
|
|
50
|
+
:effect_root, :effect_labels, :effect_attributions, :effect_edges, :effect_entry_points,
|
|
51
|
+
:effect_ancestry
|
|
41
52
|
|
|
42
53
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
43
54
|
id:, version:,
|
|
@@ -46,7 +57,9 @@ module Rigor
|
|
|
46
57
|
block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
|
|
47
58
|
trait_registries: [],
|
|
48
59
|
hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
|
|
49
|
-
source_rbs_synthesizer: nil, additional_initializers: []
|
|
60
|
+
source_rbs_synthesizer: nil, additional_initializers: [],
|
|
61
|
+
effect_root: nil, effect_labels: [], effect_attributions: [], effect_edges: [],
|
|
62
|
+
effect_entry_points: [], effect_ancestry: []
|
|
50
63
|
)
|
|
51
64
|
validate_id!(id)
|
|
52
65
|
validate_version!(version)
|
|
@@ -65,6 +78,8 @@ module Rigor
|
|
|
65
78
|
validate_protocol_contracts!(protocol_contracts)
|
|
66
79
|
validate_source_rbs_synthesizer!(source_rbs_synthesizer)
|
|
67
80
|
validate_additional_initializers!(additional_initializers)
|
|
81
|
+
validate_effect_fields!(effect_root, effect_labels, effect_attributions, effect_edges,
|
|
82
|
+
effect_entry_points, effect_ancestry)
|
|
68
83
|
|
|
69
84
|
assign_fields(id, version, description, config_schema, produces, consumes, owns_receivers,
|
|
70
85
|
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
@@ -72,6 +87,8 @@ module Rigor
|
|
|
72
87
|
source_rbs_synthesizer)
|
|
73
88
|
assign_nested_class_templates(nested_class_templates)
|
|
74
89
|
assign_additional_initializers(additional_initializers)
|
|
90
|
+
assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
|
|
91
|
+
effect_entry_points, effect_ancestry)
|
|
75
92
|
freeze
|
|
76
93
|
end
|
|
77
94
|
|
|
@@ -115,6 +132,31 @@ module Rigor
|
|
|
115
132
|
@additional_initializers = additional_initializers.dup.freeze
|
|
116
133
|
end
|
|
117
134
|
private :assign_additional_initializers
|
|
135
|
+
|
|
136
|
+
# ADR-103 WD2 / WD6 / WD10 / WD14 — the effect contract's five fields, assigned together and outside
|
|
137
|
+
# `assign_fields` (which already carries the maximum positional arity).
|
|
138
|
+
# The six `effect_*` fields validate together, so {#initialize} carries one call rather than six.
|
|
139
|
+
def validate_effect_fields!(effect_root, effect_labels, effect_attributions, effect_edges,
|
|
140
|
+
effect_entry_points, effect_ancestry)
|
|
141
|
+
validate_effect_root!(effect_root)
|
|
142
|
+
validate_effect_labels!(effect_labels)
|
|
143
|
+
validate_effect_attributions!(effect_attributions)
|
|
144
|
+
validate_effect_edges!(effect_edges)
|
|
145
|
+
validate_effect_entry_points!(effect_entry_points)
|
|
146
|
+
validate_effect_ancestry!(effect_ancestry)
|
|
147
|
+
end
|
|
148
|
+
private :validate_effect_fields!
|
|
149
|
+
|
|
150
|
+
def assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
|
|
151
|
+
effect_entry_points, effect_ancestry)
|
|
152
|
+
@effect_root = effect_root.nil? ? nil : effect_root.to_s.dup.freeze
|
|
153
|
+
@effect_labels = effect_labels.map { |label| label.to_s.dup.freeze }.uniq.sort.freeze
|
|
154
|
+
@effect_attributions = effect_attributions.dup.freeze
|
|
155
|
+
@effect_edges = effect_edges.dup.freeze
|
|
156
|
+
@effect_entry_points = effect_entry_points.dup.freeze
|
|
157
|
+
@effect_ancestry = effect_ancestry.dup.freeze
|
|
158
|
+
end
|
|
159
|
+
private :assign_effect_fields
|
|
118
160
|
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
119
161
|
|
|
120
162
|
public
|
|
@@ -161,10 +203,39 @@ module Rigor
|
|
|
161
203
|
"signature_paths" => signature_paths,
|
|
162
204
|
"protocol_contracts" => protocol_contracts.map(&:to_h),
|
|
163
205
|
"source_rbs_synthesizer" => source_rbs_synthesizer&.class&.name,
|
|
164
|
-
"additional_initializers" => additional_initializers.map(&:to_h)
|
|
206
|
+
"additional_initializers" => additional_initializers.map(&:to_h),
|
|
207
|
+
"effect_root" => effect_root,
|
|
208
|
+
"effect_labels" => effect_labels,
|
|
209
|
+
"effect_attributions" => effect_attributions.map(&:to_h),
|
|
210
|
+
"effect_edges" => effect_edges.map(&:to_h),
|
|
211
|
+
"effect_entry_points" => effect_entry_points.map(&:to_h),
|
|
212
|
+
"effect_ancestry" => effect_ancestry.map(&:to_h)
|
|
165
213
|
}
|
|
166
214
|
end
|
|
167
215
|
|
|
216
|
+
# ADR-103 WD2 — the identity opening a root when this plugin's labels join the run's vocabulary: the
|
|
217
|
+
# framework root it models for a first-party bundled plugin that declares one, and the plugin id
|
|
218
|
+
# otherwise. A third-party plugin's `effect_root:` is ignored here (and warned about by the registry),
|
|
219
|
+
# so it opens only the root named after itself.
|
|
220
|
+
def effect_owner
|
|
221
|
+
return id if effect_root.nil?
|
|
222
|
+
return id unless FirstParty.bundled?(id)
|
|
223
|
+
|
|
224
|
+
effect_root
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Whether this plugin's `effect_attributions:` may carry `discharge: true` (ADR-103 WD6).
|
|
228
|
+
def effect_discharge_allowed?
|
|
229
|
+
FirstParty.bundled?(id)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Whether the plugin contributes anything to the effect surfaces at all. The compiled per-run tables
|
|
233
|
+
# skip a plugin that answers false, so a project whose plugins predate this contract pays nothing.
|
|
234
|
+
def effects?
|
|
235
|
+
!effect_labels.empty? || !effect_attributions.empty? || !effect_edges.empty? ||
|
|
236
|
+
!effect_entry_points.empty?
|
|
237
|
+
end
|
|
238
|
+
|
|
168
239
|
def ==(other)
|
|
169
240
|
other.is_a?(Manifest) && to_h == other.to_h
|
|
170
241
|
end
|
|
@@ -414,6 +485,59 @@ module Rigor
|
|
|
414
485
|
"got #{synthesizer.inspect}"
|
|
415
486
|
end
|
|
416
487
|
|
|
488
|
+
# ADR-103 WD2 — `effect_root:` names the effect-label root this plugin opens when it is one the
|
|
489
|
+
# engine bundles: rigor-activejob models ActiveJob and therefore opens `rails.*`, not `activejob.*`.
|
|
490
|
+
# Declaring one is only ever a *request*; {#effect_owner} grants it to a first-party plugin and
|
|
491
|
+
# ignores it otherwise, so the field is safe to accept from anyone.
|
|
492
|
+
def validate_effect_root!(root)
|
|
493
|
+
return if root.nil?
|
|
494
|
+
return if root.is_a?(String) && root.match?(VALID_EFFECT_ROOT)
|
|
495
|
+
|
|
496
|
+
raise ArgumentError,
|
|
497
|
+
"plugin manifest effect_root must match #{VALID_EFFECT_ROOT.inspect}, got #{root.inspect}"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# ADR-103 WD2 — `effect_labels:` lists the vocabulary this plugin registers. The engine folds them
|
|
501
|
+
# into the run's {Rigor::Effects::Registry} through `#with(labels:, owner: manifest.effect_owner)`,
|
|
502
|
+
# which is where root ownership is enforced; a label outside the plugin's root is a load-time plugin
|
|
503
|
+
# error, reported like any other. Only the spelling is checked here.
|
|
504
|
+
def validate_effect_labels!(labels)
|
|
505
|
+
validate_array_of!("effect_labels", labels, "non-empty String") { |l| l.is_a?(String) && !l.empty? }
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# ADR-103 WD6 / WD10 — `effect_attributions:` colours calls into the framework this plugin models.
|
|
509
|
+
# Each entry MUST be a {Rigor::Plugin::EffectAttribution}.
|
|
510
|
+
def validate_effect_attributions!(entries)
|
|
511
|
+
validate_array_of!("effect_attributions", entries, "Rigor::Plugin::EffectAttribution instances") do |e|
|
|
512
|
+
e.is_a?(EffectAttribution)
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# ADR-103 WD10 — `effect_edges:` declares the framework edges the syntax lacks. Each entry MUST be a
|
|
517
|
+
# {Rigor::Plugin::EffectEdge}, whose `target:` is one of a fixed engine-side strategy enum.
|
|
518
|
+
def validate_effect_edges!(entries)
|
|
519
|
+
validate_array_of!("effect_edges", entries, "Rigor::Plugin::EffectEdge instances") do |e|
|
|
520
|
+
e.is_a?(EffectEdge)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# ADR-103 WD17 (#465) — `effect_ancestry:` states an ancestry edge the plugin's own gem introduces
|
|
525
|
+
# and the project's source never writes. Each entry MUST be a {Rigor::Plugin::EffectAncestry}; only
|
|
526
|
+
# a bundled plugin's survives, which `Effects::PluginFacts` enforces.
|
|
527
|
+
def validate_effect_ancestry!(entries)
|
|
528
|
+
validate_array_of!("effect_ancestry", entries, "Rigor::Plugin::EffectAncestry instances") do |e|
|
|
529
|
+
e.is_a?(EffectAncestry)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# ADR-103 WD14 — `effect_entry_points:` names the `effects.snapshot.reach:` presets this plugin
|
|
534
|
+
# supplies. Each entry MUST be a {Rigor::Plugin::EffectEntryPoints}.
|
|
535
|
+
def validate_effect_entry_points!(entries)
|
|
536
|
+
validate_array_of!("effect_entry_points", entries, "Rigor::Plugin::EffectEntryPoints instances") do |e|
|
|
537
|
+
e.is_a?(EffectEntryPoints)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
417
541
|
def coerce_consumes(consumes)
|
|
418
542
|
unless consumes.is_a?(Array)
|
|
419
543
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
@@ -244,6 +244,36 @@ module Rigor
|
|
|
244
244
|
# ADR-52 WD4 — the per-run node-rule walk (see {NodeRuleWalk}).
|
|
245
245
|
attr_reader :node_rule_walk
|
|
246
246
|
|
|
247
|
+
# ADR-103 WD2 / WD6 / WD10 / WD14 (#387) — every loaded plugin's effect contribution, in
|
|
248
|
+
# registration order, as {Contribution} rows.
|
|
249
|
+
#
|
|
250
|
+
# **Lazy, unlike the other aggregates.** A plugin MAY compute its `effect_attributions:` from project
|
|
251
|
+
# facts — rigor-activejob reads `config.active_job.queue_adapter` to decide whether an enqueue is a
|
|
252
|
+
# database write or a Redis one — and that is an I/O-boundary read a `rigor check` with no `effects:`
|
|
253
|
+
# block must never pay. Nothing asks for this until {Rigor::Effects::PluginFacts.build} does, and
|
|
254
|
+
# nothing calls that unless collection is on. Memoised into the frozen registry the same way
|
|
255
|
+
# `contracts_for_path` memoises, and for the same reason: the answer is fixed for the run.
|
|
256
|
+
def effect_contributions
|
|
257
|
+
@effect_memo[:contributions] ||= compile_effect_contributions
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# One plugin's effect contribution, flattened off whichever surface answered it (the manifest, or the
|
|
261
|
+
# plugin's own override). Carries the two authority answers with it — `owner`, the label root this
|
|
262
|
+
# plugin may open, and `discharge_allowed`, whether its attributions may discharge — so nothing
|
|
263
|
+
# downstream has to re-derive who is first-party.
|
|
264
|
+
Contribution = Data.define(:id, :owner, :requested_root, :discharge_allowed, :labels, :attributions,
|
|
265
|
+
:edges, :entry_points, :ancestry) do
|
|
266
|
+
# `ancestry:` defaults so a caller written before #465 — and every spec that constructs one by
|
|
267
|
+
# hand — keeps working; a plugin that declares none contributes none.
|
|
268
|
+
def initialize(ancestry: [], **rest)
|
|
269
|
+
super
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def empty?
|
|
273
|
+
labels.empty? && attributions.empty? && edges.empty? && entry_points.empty? && ancestry.empty?
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
247
277
|
# ADR-15 Phase 3 — build a fresh Registry from the supplied blueprint set by replaying
|
|
248
278
|
# {Blueprint#materialize} per entry against `services`. The returned registry carries NEW plugin
|
|
249
279
|
# instances (mutable per-Ractor accumulators included) and the same blueprint set, so a worker can hand
|
|
@@ -379,6 +409,31 @@ module Rigor
|
|
|
379
409
|
@open_receivers_set = @open_receivers.to_set.freeze
|
|
380
410
|
@protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
|
|
381
411
|
@contracts_by_path = {}
|
|
412
|
+
@effect_memo = {}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Fail-soft per plugin: a plugin whose `effect_attributions:` override raises (a missing
|
|
416
|
+
# `config/application.rb`, an unreadable path) contributes nothing rather than taking the run down.
|
|
417
|
+
# An effect contribution is an enrichment; ADR-2 isolates plugin failures at the analyzer boundary
|
|
418
|
+
# and this is that boundary.
|
|
419
|
+
def compile_effect_contributions
|
|
420
|
+
@plugins.filter_map { |plugin| effect_contribution_for(plugin) }.freeze
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def effect_contribution_for(plugin)
|
|
424
|
+
manifest = safe_manifest(plugin)
|
|
425
|
+
return nil if manifest.nil?
|
|
426
|
+
|
|
427
|
+
contribution = Contribution.new(
|
|
428
|
+
id: manifest.id, owner: manifest.effect_owner, requested_root: manifest.effect_root,
|
|
429
|
+
discharge_allowed: manifest.effect_discharge_allowed?,
|
|
430
|
+
labels: plugin.effect_labels, attributions: plugin.effect_attributions,
|
|
431
|
+
edges: plugin.effect_edges, entry_points: plugin.effect_entry_points,
|
|
432
|
+
ancestry: plugin.effect_ancestry
|
|
433
|
+
)
|
|
434
|
+
contribution.empty? ? nil : contribution
|
|
435
|
+
rescue StandardError
|
|
436
|
+
nil
|
|
382
437
|
end
|
|
383
438
|
|
|
384
439
|
def path_matches_glob?(glob, path)
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
require_relative "../rbs_extended"
|
|
6
|
+
require_relative "reporter"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module RbsExtended
|
|
10
|
+
# Collects every effect envelope the project's own RBS declares (ADR-103 WD5 / WD14; #383).
|
|
11
|
+
#
|
|
12
|
+
# It produces values, never diagnostics — the check that compares a bound against the proven
|
|
13
|
+
# summaries is {Rigor::Effects::EnvelopeCheck}. Two tables come out, because they are consumed
|
|
14
|
+
# differently:
|
|
15
|
+
#
|
|
16
|
+
# - {Result#method_envelopes} — keyed by method key (`Class#m` / `Class.m`), applied as written.
|
|
17
|
+
# - {Result#class_envelopes} — keyed by class / module name, DISTRIBUTED by the check to every
|
|
18
|
+
# method of that Ruby class discovery knows (reopenings, other files, synthesized `attr_*` and
|
|
19
|
+
# `define_method` members), never to subclasses. A per-method envelope wins over a distributed
|
|
20
|
+
# one; nearest wins, and no `-except` syntax is needed.
|
|
21
|
+
#
|
|
22
|
+
# ## Why it parses rather than reading the built environment
|
|
23
|
+
#
|
|
24
|
+
# {ConformanceChecker}, the obvious model, walks `RbsLoader`'s built env. An envelope cannot: the
|
|
25
|
+
# diagnostic has to name **where the bound was written** (`sig/foo.rbs:12`), and the ADR-54 env
|
|
26
|
+
# cache dumps every `RBS::Location` to a zero-range `<cached>` sentinel, so a warm run would lose
|
|
27
|
+
# both the position and the only evidence of which file a declaration came from. Parsing the
|
|
28
|
+
# project's own signature sources is a few milliseconds over a tree Rigor already globs, it is
|
|
29
|
+
# identical warm and cold, and it enforces ADR-103 WD6's trust rule structurally: a `%a{pure}` in
|
|
30
|
+
# rbs core or in a gem's shipped RBS is never read, so it cannot bound a project method that
|
|
31
|
+
# happens to share its key.
|
|
32
|
+
#
|
|
33
|
+
# Malformed payloads and `pure`-versus-`effect` contradictions are recorded on a {Reporter} the
|
|
34
|
+
# scanner owns and ride out on {Result#unresolved}. They surface no diagnostic in this slice:
|
|
35
|
+
# `effect.unknown-label` and the annotation-in-a-project-without-`effects:` residual are #384's.
|
|
36
|
+
module EnvelopeScanner
|
|
37
|
+
Result = Data.define(:method_envelopes, :class_envelopes, :unresolved) do
|
|
38
|
+
def empty? = method_envelopes.empty? && class_envelopes.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
EMPTY = Result.new(method_envelopes: {}.freeze, class_envelopes: {}.freeze, unresolved: [].freeze)
|
|
42
|
+
|
|
43
|
+
# RBS's kind for a `def self?.x` member — it declares BOTH sides, so the envelope binds both.
|
|
44
|
+
SINGLETON_INSTANCE = :singleton_instance
|
|
45
|
+
private_constant :SINGLETON_INSTANCE
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# @param sources [Array<Array(String, String)>] `[buffer name, RBS source]` pairs — the project's
|
|
50
|
+
# `.rbs` files, plus the virtual entries rbs-inline and plugin `source_rbs` synthesis contribute.
|
|
51
|
+
# @param registry [Rigor::Effects::Registry] the vocabulary an unknown label is judged against.
|
|
52
|
+
# @return [Result]
|
|
53
|
+
def scan(sources:, registry:)
|
|
54
|
+
reporter = Reporter.new
|
|
55
|
+
methods = {}
|
|
56
|
+
classes = {}
|
|
57
|
+
sources.each do |name, content|
|
|
58
|
+
declarations(name, content).each do |decl|
|
|
59
|
+
walk(decl, [], methods, classes, registry, reporter)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
Result.new(
|
|
63
|
+
method_envelopes: methods.freeze, class_envelopes: classes.freeze,
|
|
64
|
+
unresolved: reporter.unresolved_payloads
|
|
65
|
+
)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
EMPTY
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The **accepted-signature** reader: every per-method envelope declared anywhere in a BUILT
|
|
71
|
+
# `RBS::Environment` — a gem's shipped signatures, Rigor's bundled overlays, core RBS
|
|
72
|
+
# (ADR-103 WD6; #386).
|
|
73
|
+
#
|
|
74
|
+
# It exists for one purpose and is usable for exactly one: importing a `≤` bound at a call site
|
|
75
|
+
# that resolves into un-analysed code ({Rigor::Effects::EnvelopeIndex}). It MUST NOT feed a
|
|
76
|
+
# contract check. Two facts make that a structural guarantee rather than a convention —
|
|
77
|
+
# {.scan}, which the check reads, never opens this door, and the values here carry no
|
|
78
|
+
# `location` at all (the ADR-54 env cache dumps every `RBS::Location` to a `<cached>` sentinel,
|
|
79
|
+
# so a warm run could not name where the bound was written even if a diagnostic wanted to).
|
|
80
|
+
# There is also no body to check: a gem method is un-analysed by definition, which is why WD6
|
|
81
|
+
# trusts the declaration instead, exactly as it already trusts the gem's *types*.
|
|
82
|
+
#
|
|
83
|
+
# Class-level annotations are deliberately not read. Distribution needs "every method of the
|
|
84
|
+
# class discovery knows", which is a project fact; a gem's class-level tag would have to
|
|
85
|
+
# distribute over a definition set this reader does not have.
|
|
86
|
+
#
|
|
87
|
+
# @param loader [Rigor::Environment::RbsLoader] the run's loader; it owns the env walk
|
|
88
|
+
# ({Rigor::Environment::RbsLoader#each_annotated_method_member}), so the environment itself never
|
|
89
|
+
# leaves it.
|
|
90
|
+
# @param registry [Rigor::Effects::Registry]
|
|
91
|
+
# @return [Hash{String => Rigor::Effects::Envelope}] keyed `Class#m` / `Class.m`
|
|
92
|
+
def from_loader(loader:, registry:)
|
|
93
|
+
out = {}
|
|
94
|
+
loader.each_annotated_method_member do |class_name, member|
|
|
95
|
+
keys_for(class_name, member).each do |key|
|
|
96
|
+
next if out.key?(key)
|
|
97
|
+
|
|
98
|
+
envelope = RbsExtended.read_effect_envelope(
|
|
99
|
+
member.annotations, owner_key: key, source: :effect_annotation, registry: registry
|
|
100
|
+
)
|
|
101
|
+
out[key] = envelope if envelope
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
out.freeze
|
|
105
|
+
rescue StandardError
|
|
106
|
+
{}.freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Fail-soft per file: an unparseable `.rbs` is already quarantined by the loader and reported as
|
|
110
|
+
# `rbs.coverage.quarantined-signature`; contributing no envelope is the consistent answer here.
|
|
111
|
+
def declarations(name, content)
|
|
112
|
+
_, _, decls = ::RBS::Parser.parse_signature(::RBS::Buffer.new(name: name, content: content))
|
|
113
|
+
decls || []
|
|
114
|
+
rescue StandardError
|
|
115
|
+
[]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Descends the declaration tree, carrying the lexical prefix so a nested `class Bar` inside
|
|
119
|
+
# `module Foo` keys as `Foo::Bar` — the same spelling the effects scanner gives its units.
|
|
120
|
+
def walk(decl, prefix, methods, classes, registry, reporter)
|
|
121
|
+
return unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
|
|
122
|
+
|
|
123
|
+
nested = prefix + [decl.name.to_s.sub(/\A::/, "")]
|
|
124
|
+
class_name = nested.join("::")
|
|
125
|
+
envelope = RbsExtended.read_effect_envelope(
|
|
126
|
+
decl.annotations, owner_key: class_name, source: :class_annotation,
|
|
127
|
+
registry: registry, reporter: reporter
|
|
128
|
+
)
|
|
129
|
+
classes[class_name] = envelope if envelope
|
|
130
|
+
decl.members.each { |member| absorb_member(member, nested, methods, classes, registry, reporter) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def absorb_member(member, prefix, methods, classes, registry, reporter)
|
|
134
|
+
return walk(member, prefix, methods, classes, registry, reporter) unless
|
|
135
|
+
member.is_a?(::RBS::AST::Members::MethodDefinition)
|
|
136
|
+
return if member.annotations.nil? || member.annotations.empty?
|
|
137
|
+
|
|
138
|
+
keys_for(prefix.join("::"), member).each do |key|
|
|
139
|
+
envelope = RbsExtended.read_effect_envelope(
|
|
140
|
+
member.annotations, owner_key: key, source: :effect_annotation,
|
|
141
|
+
registry: registry, reporter: reporter
|
|
142
|
+
)
|
|
143
|
+
methods[key] = envelope if envelope
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The effect-unit keys one RBS member declares. `def self?.x` declares both sides, so its
|
|
148
|
+
# envelope binds both; every other kind declares one.
|
|
149
|
+
def keys_for(class_name, member)
|
|
150
|
+
case member.kind
|
|
151
|
+
when :singleton then ["#{class_name}.#{member.name}"]
|
|
152
|
+
when SINGLETON_INSTANCE then ["#{class_name}##{member.name}", "#{class_name}.#{member.name}"]
|
|
153
|
+
else ["#{class_name}##{member.name}"]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private_class_method :declarations, :walk, :absorb_member, :keys_for
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|