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,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/constant_path"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "attribution"
|
|
8
|
+
require_relative "envelope_index"
|
|
9
|
+
require_relative "file_collection"
|
|
10
|
+
require_relative "local_ownership"
|
|
11
|
+
require_relative "origin"
|
|
12
|
+
require_relative "framework_units"
|
|
13
|
+
require_relative "plugin_facts"
|
|
14
|
+
require_relative "summary"
|
|
15
|
+
require_relative "unit_scan"
|
|
16
|
+
|
|
17
|
+
module Rigor
|
|
18
|
+
module Effects
|
|
19
|
+
# Turns one file's AST plus the call decisions the typer recorded for it into a {FileCollection}.
|
|
20
|
+
#
|
|
21
|
+
# The scanner owns **identity** — which effect units the file defines and what each is keyed as — and
|
|
22
|
+
# delegates each unit's body to {UnitScan}. Keys follow the existing symbol tables (ADR-103 WD14):
|
|
23
|
+
# `Class#m` for an instance method, `Class.m` for a singleton one, and `<toplevel>#m` for a `def`
|
|
24
|
+
# outside any class body. Reopenings in one file join here; reopenings across files join when the
|
|
25
|
+
# runner merges the collections.
|
|
26
|
+
#
|
|
27
|
+
# Three kinds of unit exist beyond a plain `def`:
|
|
28
|
+
#
|
|
29
|
+
# - `define_method(:literal) { … }` — the block is the method's body. The def-node discovery tables
|
|
30
|
+
# skip it, so this is the minimal extension WD14 calls for, made here rather than in `ScopeIndexer`
|
|
31
|
+
# because nothing outside effects needs it yet.
|
|
32
|
+
# - `attr_reader` / `attr_writer` / `attr_accessor` — synthesised: a reader is ∅, a writer is
|
|
33
|
+
# `mutate.self`. Without them a caller's edge into an accessor would read as unresolved.
|
|
34
|
+
# - a nested `def` — its own unit under the same class, never contained in the enclosing method.
|
|
35
|
+
#
|
|
36
|
+
# **This walk exists only when collection is on.** ADR-103 WD13 prefers riding `ScopeIndexer`'s
|
|
37
|
+
# existing `def` descent; a separate walk is taken here because the scanner must also attribute each
|
|
38
|
+
# recorded call *node* to its enclosing unit, and doing that inside the indexer would put an
|
|
39
|
+
# effects-shaped concern on the hot path for every run. Off, this file is never loaded past `require`.
|
|
40
|
+
class Scanner
|
|
41
|
+
# Mirrors `Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY`. Spelled again rather than required so the
|
|
42
|
+
# effects namespace does not pull the indexer in; the two are pinned together by spec.
|
|
43
|
+
TOP_LEVEL_KEY = "<toplevel>"
|
|
44
|
+
|
|
45
|
+
# Receiver-less calls in a class / module body that {#record_declaration} interprets. Everything
|
|
46
|
+
# else in a class body is out of scope in v1 — its statements run at load time, which is a unit of
|
|
47
|
+
# its own that no slice models yet.
|
|
48
|
+
DECLARATION_MACROS = %i[include prepend attr_reader attr_writer attr_accessor define_method].to_set.freeze
|
|
49
|
+
|
|
50
|
+
MUTATE_SELF = LabelSet.new(["mutate.self"])
|
|
51
|
+
private_constant :MUTATE_SELF
|
|
52
|
+
|
|
53
|
+
# A synthesised writer's summary is the same value at every `attr_accessor` in the project, so it is
|
|
54
|
+
# built once rather than per accessor.
|
|
55
|
+
WRITER_SUMMARY = Summary.new(bundles: { Origin.construct("attr-writer") => MUTATE_SELF })
|
|
56
|
+
private_constant :WRITER_SUMMARY
|
|
57
|
+
|
|
58
|
+
def self.scan(root:, path:, calls:, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
|
|
59
|
+
plugin_facts: PluginFacts.empty)
|
|
60
|
+
new(path: path, calls: calls, attribution: attribution, envelopes: envelopes,
|
|
61
|
+
plugin_facts: plugin_facts).scan(root)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(path:, calls:, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
|
|
65
|
+
plugin_facts: PluginFacts.empty)
|
|
66
|
+
@path = path
|
|
67
|
+
@calls = calls
|
|
68
|
+
@attribution = attribution
|
|
69
|
+
@envelopes = envelopes
|
|
70
|
+
@plugin_facts = plugin_facts
|
|
71
|
+
@summaries = {}
|
|
72
|
+
@edges = {}
|
|
73
|
+
@superclasses = {}
|
|
74
|
+
@includes = {}
|
|
75
|
+
# ADR-103 WD10 / #387 — the class-body facts the framework-edge strategies read, harvested only
|
|
76
|
+
# when a loaded plugin declared one. A run with no `effect_edges:` never allocates them.
|
|
77
|
+
@harvest = plugin_facts.edges? ? {} : nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def scan(root)
|
|
81
|
+
walk(root, [], false)
|
|
82
|
+
synthesize_framework_units
|
|
83
|
+
FileCollection.new(
|
|
84
|
+
path: @path, summaries: @summaries, edges: @edges,
|
|
85
|
+
superclasses: @superclasses, includes: @includes
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def walk(node, prefix, singleton)
|
|
92
|
+
return unless node.is_a?(Prism::Node)
|
|
93
|
+
|
|
94
|
+
case node
|
|
95
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
96
|
+
return walk_namespace(node, prefix)
|
|
97
|
+
when Prism::SingletonClassNode
|
|
98
|
+
return walk(node.body, prefix, true) if node.body
|
|
99
|
+
when Prism::DefNode
|
|
100
|
+
return enter_def(node, prefix, singleton)
|
|
101
|
+
when Prism::CallNode
|
|
102
|
+
harvest_class_body_macro(node, prefix)
|
|
103
|
+
return record_declaration(node, prefix) if declaration?(node)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
node.rigor_each_child { |child| walk(child, prefix, singleton) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def walk_namespace(node, prefix)
|
|
110
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
111
|
+
return node.rigor_each_child { |child| walk(child, prefix, false) } if name.nil?
|
|
112
|
+
|
|
113
|
+
nested = prefix + [name]
|
|
114
|
+
record_superclass(nested.join("::"), node, prefix) if node.is_a?(Prism::ClassNode)
|
|
115
|
+
walk(node.body, nested, false) if node.body
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def enter_def(node, prefix, singleton)
|
|
119
|
+
own_singleton = singleton || !node.receiver.nil?
|
|
120
|
+
scan = add_unit(class_name_for(prefix), node.name.to_s, own_singleton, node.body, node.parameters)
|
|
121
|
+
harvest_def(prefix, node.name.to_s, own_singleton, scan) if @harvest && !prefix.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# What the framework strategies need to know about a `def` the class body spelled out itself: that it
|
|
125
|
+
# exists, and whether it reaches `super`. `:defs` stays instance-only, because a mailer action is an
|
|
126
|
+
# instance method; `:units` is keyed by the suffix a synthetic unit key carries, so both sides of the
|
|
127
|
+
# `#` / `.` split are answerable (#440). A body the collector could not finish reads as delegating,
|
|
128
|
+
# which keeps the framework's claim — the fail-soft direction for an upper bound.
|
|
129
|
+
def harvest_def(prefix, name, singleton, scan)
|
|
130
|
+
entry = harvest_for(prefix)
|
|
131
|
+
entry[:defs] << name unless singleton
|
|
132
|
+
entry[:units]["#{singleton ? '.' : '#'}#{name}"] = scan.nil? || scan.delegates_upward?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ADR-103 WD10 — a receiver-less call in a class body, recorded as `macro => [literal symbol
|
|
136
|
+
# arguments]`. That is all the framework-edge strategies need: `before_save :normalize` names a
|
|
137
|
+
# method on the same class, and a computed callback (`before_save -> { … }`, a method object) names
|
|
138
|
+
# none the strategy could resolve, so it contributes nothing rather than a guess. The block form is
|
|
139
|
+
# already contained in the class body, which v1 does not model as a unit at all.
|
|
140
|
+
def harvest_class_body_macro(node, prefix)
|
|
141
|
+
return if @harvest.nil? || prefix.empty? || !node.receiver.nil?
|
|
142
|
+
|
|
143
|
+
entry = harvest_for(prefix)
|
|
144
|
+
name = node.name.to_s
|
|
145
|
+
if FrameworkUnits::CALLBACK_MACROS.include?(name)
|
|
146
|
+
(entry[:macros][name] ||= []).concat(symbol_arguments(node))
|
|
147
|
+
elsif uniqueness_validator?(node, name)
|
|
148
|
+
entry[:uniqueness] = true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# `validates :email, uniqueness: true` and `validates_uniqueness_of :email` — the validation whose
|
|
153
|
+
# implementation is a `SELECT`.
|
|
154
|
+
def uniqueness_validator?(node, name)
|
|
155
|
+
return true if name == FrameworkUnits::UNIQUENESS_MACRO
|
|
156
|
+
return false unless name == FrameworkUnits::VALIDATES_MACRO
|
|
157
|
+
|
|
158
|
+
node.arguments&.arguments&.any? do |argument|
|
|
159
|
+
argument.is_a?(Prism::KeywordHashNode) && argument.elements.any? do |element|
|
|
160
|
+
element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
|
|
161
|
+
element.key.unescaped == FrameworkUnits::UNIQUENESS_OPTION
|
|
162
|
+
end
|
|
163
|
+
end || false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def harvest_for(prefix)
|
|
167
|
+
@harvest[class_name_for(prefix)] ||= { defs: [], units: {}, macros: {}, uniqueness: false }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Files the units the framework contributes for each class this file declares. Runs after the walk,
|
|
171
|
+
# because a callback macro may be written below the `def` it names and a mailer action may be
|
|
172
|
+
# declared anywhere in the body.
|
|
173
|
+
def synthesize_framework_units
|
|
174
|
+
return if @harvest.nil?
|
|
175
|
+
|
|
176
|
+
@harvest.each do |class_name, entry|
|
|
177
|
+
FrameworkUnits.synthesize(
|
|
178
|
+
class_name: class_name, instance_methods: entry[:defs], macros: entry[:macros],
|
|
179
|
+
uniqueness: entry[:uniqueness], plugin_facts: @plugin_facts, own_units: entry[:units]
|
|
180
|
+
).each { |key, summary, edges| merge_unit(key, summary, edges) }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Scans one unit and files its summary, then recurses into the units its body declared. Fail-soft
|
|
185
|
+
# per unit (ADR-103 WD13): a unit the scanner cannot finish is recorded as non-exhaustive with
|
|
186
|
+
# `collector-error` and its siblings are unaffected.
|
|
187
|
+
#
|
|
188
|
+
# @return [UnitScan, nil] the finished scan, or nil when the unit failed soft
|
|
189
|
+
def add_unit(class_name, method_name, singleton, body, parameters)
|
|
190
|
+
key = "#{class_name}#{singleton ? '.' : '#'}#{method_name}"
|
|
191
|
+
names = parameter_names(parameters)
|
|
192
|
+
scan = UnitScan.new(
|
|
193
|
+
singleton: singleton, parameters: names,
|
|
194
|
+
block_parameter: block_parameter_name(parameters),
|
|
195
|
+
owned_locals: LocalOwnership.owned(body, names), calls: @calls,
|
|
196
|
+
attribution: @attribution, envelopes: @envelopes, plugin_facts: @plugin_facts,
|
|
197
|
+
owner_class: class_name, method_name: method_name
|
|
198
|
+
)
|
|
199
|
+
summary, edges = scan.run(body)
|
|
200
|
+
merge_unit(key, summary, edges)
|
|
201
|
+
scan.nested.each do |name, nested_singleton, nested_body, nested_parameters|
|
|
202
|
+
add_unit(class_name, name, singleton || nested_singleton, nested_body, nested_parameters)
|
|
203
|
+
end
|
|
204
|
+
scan
|
|
205
|
+
rescue StandardError
|
|
206
|
+
merge_unit(key, Summary.tainted("collector-error", method_name), [])
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# A receiver-less call in a class / module body that declares units or ancestry. Class bodies are
|
|
211
|
+
# not themselves effect units in v1 (their statements run at load time), so nothing else in one
|
|
212
|
+
# contributes labels — but `include` and the accessor macros decide what the *methods* are.
|
|
213
|
+
def declaration?(node)
|
|
214
|
+
node.receiver.nil? && DECLARATION_MACROS.include?(node.name)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def record_declaration(node, prefix)
|
|
218
|
+
class_name = class_name_for(prefix)
|
|
219
|
+
case node.name
|
|
220
|
+
when :include, :prepend then record_includes(class_name, node, prefix)
|
|
221
|
+
when :define_method then declare_define_method(class_name, node)
|
|
222
|
+
else synthesize_accessors(class_name, node)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def declare_define_method(class_name, node)
|
|
227
|
+
unit = UnitScan.define_method_unit(node)
|
|
228
|
+
return if unit.nil?
|
|
229
|
+
|
|
230
|
+
name, singleton, body, parameters = unit
|
|
231
|
+
add_unit(class_name, name, singleton, body, parameters)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def synthesize_accessors(class_name, node)
|
|
235
|
+
symbol_arguments(node).each do |name|
|
|
236
|
+
merge_unit("#{class_name}##{name}", Summary.empty, []) unless node.name == :attr_writer
|
|
237
|
+
next if node.name == :attr_reader
|
|
238
|
+
|
|
239
|
+
merge_unit("#{class_name}##{name}=", WRITER_SUMMARY, [])
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def record_includes(class_name, node, prefix)
|
|
244
|
+
names = constant_arguments(node).flat_map { |name| lexical_candidates(name, prefix) }
|
|
245
|
+
(@includes[class_name] ||= []).concat(names) unless names.empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def record_superclass(full_name, node, prefix)
|
|
249
|
+
superclass = node.superclass && Source::ConstantPath.qualified_name(node.superclass)
|
|
250
|
+
@superclasses[full_name] = lexical_candidates(superclass, prefix) if superclass
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# An ancestry name is recorded AS WRITTEN — `class Loud < Base` inside `module Tracer` names
|
|
254
|
+
# `Base`, not `Tracer::Base` — and a single file cannot say which constant that resolves to. So the
|
|
255
|
+
# scanner records the candidates Ruby's own lexical lookup would try, most-qualified first, and the
|
|
256
|
+
# propagator picks the one the merged project actually defines. Same shape as `ScopeIndexer`'s
|
|
257
|
+
# as-written superclass table, resolved at the same point: when the whole project is in view.
|
|
258
|
+
def lexical_candidates(name, prefix)
|
|
259
|
+
return [name] if prefix.empty? || name.start_with?("#{prefix.join('::')}::")
|
|
260
|
+
|
|
261
|
+
prefix.length.downto(1).map { |depth| "#{prefix.first(depth).join('::')}::#{name}" } + [name]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def merge_unit(key, summary, edges)
|
|
265
|
+
@summaries[key] = @summaries.key?(key) ? @summaries[key].join(summary) : summary
|
|
266
|
+
(@edges[key] ||= []).concat(edges) unless edges.empty?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def class_name_for(prefix)
|
|
270
|
+
prefix.empty? ? TOP_LEVEL_KEY : prefix.join("::")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def symbol_arguments(node)
|
|
274
|
+
node.arguments&.arguments&.filter_map { |argument| argument.unescaped if argument.is_a?(Prism::SymbolNode) } ||
|
|
275
|
+
[]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def constant_arguments(node)
|
|
279
|
+
node.arguments&.arguments&.filter_map { |argument| Source::ConstantPath.qualified_name(argument) } || []
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def parameter_names(parameters)
|
|
283
|
+
return Set.new unless parameters.is_a?(Prism::ParametersNode)
|
|
284
|
+
|
|
285
|
+
names = Set.new
|
|
286
|
+
[parameters.requireds, parameters.optionals, parameters.posts, parameters.keywords].each do |group|
|
|
287
|
+
group.each { |parameter| names << parameter.name.to_s if parameter.respond_to?(:name) && parameter.name }
|
|
288
|
+
end
|
|
289
|
+
[parameters.rest, parameters.keyword_rest].each do |parameter|
|
|
290
|
+
names << parameter.name.to_s if parameter.respond_to?(:name) && parameter&.name
|
|
291
|
+
end
|
|
292
|
+
names
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def block_parameter_name(parameters)
|
|
296
|
+
return nil unless parameters.is_a?(Prism::ParametersNode)
|
|
297
|
+
|
|
298
|
+
parameters.block&.name&.to_s
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Effects
|
|
5
|
+
# Everything an effect envelope may be written in, as `[buffer name, RBS source]` pairs
|
|
6
|
+
# (ADR-103 WD5 / WD6; #383 / #384).
|
|
7
|
+
#
|
|
8
|
+
# Two strata, and the exclusion is the point: the project's own `signature_paths:` tree (or the
|
|
9
|
+
# `sig/` default `Environment.for_project` falls back to) plus the loader's **virtual** entries —
|
|
10
|
+
# which is how rbs-inline's `# @rbs %a{…}` comments and a plugin's `source_rbs` synthesis arrive.
|
|
11
|
+
# Gem-shipped and bundled RBS is deliberately absent: WD6 makes project-authored envelopes the
|
|
12
|
+
# checked stratum, so a `%a{pure}` in rbs core cannot bound a project method that shares its key.
|
|
13
|
+
#
|
|
14
|
+
# Shared by the two passes that read declarations — the envelope check and the
|
|
15
|
+
# annotations-unchecked residual — so the residual can never disagree with the check about what
|
|
16
|
+
# counts as "the project's own signatures".
|
|
17
|
+
module SignatureSources
|
|
18
|
+
# Where `Environment.for_project` looks when no `signature_paths:` is configured. Spelled again
|
|
19
|
+
# here rather than required, because the constant is private to {Rigor::Environment} and a walk
|
|
20
|
+
# over the project's own files has no business reaching into its builder.
|
|
21
|
+
DEFAULT_ROOTS = ["sig"].freeze
|
|
22
|
+
|
|
23
|
+
# A cheap text pre-filter for "does this source carry an effect annotation at all". It matches
|
|
24
|
+
# the two spellings the envelope reader honours and nothing else, so a signature tree with no
|
|
25
|
+
# effect annotation is answered by one regex per file and never parsed. It is a ROUTING test,
|
|
26
|
+
# not the grammar — `RbsExtended.parse_effect_annotation` is still what decides meaning.
|
|
27
|
+
ANNOTATION_HINT = /%a\{\s*(?:pure\s*\}|rigor:v1:effect\b)/
|
|
28
|
+
|
|
29
|
+
# A `virtual:<plugin-id>:<source path>` buffer is rbs-inline's (or a plugin's) synthesized RBS for
|
|
30
|
+
# a Ruby file the author actually wrote in. Naming that file is what a reader can act on, so the
|
|
31
|
+
# synthetic prefix comes off before the name is made project-relative.
|
|
32
|
+
VIRTUAL_BUFFER_PATTERN = /\Avirtual:[^:]*:/
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# The path a reader can open, for a buffer name from {.collect}: the `.rbs` file as configured,
|
|
37
|
+
# or the `.rb` file behind a `virtual:` buffer, project-relative either way.
|
|
38
|
+
def source_path(name)
|
|
39
|
+
stripped = name.to_s.sub(VIRTUAL_BUFFER_PATTERN, "")
|
|
40
|
+
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
41
|
+
stripped.start_with?(root) ? stripped[root.length..] : stripped
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param signature_paths [Array<String>, nil] the configured roots; empty / nil takes the default.
|
|
45
|
+
# @param virtual_rbs [Array<Array(String, String)>, nil] the loader's virtual entries.
|
|
46
|
+
# @return [Array<Array(String, String)>]
|
|
47
|
+
def collect(signature_paths:, virtual_rbs: nil)
|
|
48
|
+
files = roots(signature_paths).flat_map { |root| Dir.glob(File.join(root.to_s, "**", "*.rbs")).sort }
|
|
49
|
+
sources = files.filter_map do |path|
|
|
50
|
+
[path, File.read(path)]
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
sources + Array(virtual_rbs).map { |name, content| [name.to_s, content.to_s] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def roots(signature_paths)
|
|
58
|
+
signature_paths.nil? || signature_paths.empty? ? DEFAULT_ROOTS : signature_paths
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The FIRST annotated virtual entry, as a `collect`-shaped one-element array (or an empty one).
|
|
62
|
+
#
|
|
63
|
+
# #441 — the analysis paths use this to hand the annotations-unchecked residual the inline stratum
|
|
64
|
+
# of a run that resolved an environment but never stored it. Reduced to one entry on purpose: the
|
|
65
|
+
# residual reports one annotation per run, so the carrier a run holds past the environment's own
|
|
66
|
+
# lifetime is a single synthesized buffer rather than the project's whole virtual tree.
|
|
67
|
+
#
|
|
68
|
+
# @param virtual_rbs [Array<Array(String, String)>, nil]
|
|
69
|
+
# @return [Array<Array(String, String)>] zero or one pair.
|
|
70
|
+
def annotated_carrier(virtual_rbs)
|
|
71
|
+
Array(virtual_rbs).each do |name, content|
|
|
72
|
+
return [[name.to_s, content.to_s]] if ANNOTATION_HINT.match?(content)
|
|
73
|
+
end
|
|
74
|
+
[].freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The first `[name, content, line]` carrying an effect annotation, or nil. Line numbers are
|
|
78
|
+
# 1-based and count into `content`, which for a `virtual:` buffer is the SYNTHESIZED text — the
|
|
79
|
+
# caller re-anchors it against the Ruby file when that matters.
|
|
80
|
+
def first_annotated(sources)
|
|
81
|
+
sources.each do |name, content|
|
|
82
|
+
content.each_line.with_index(1) do |line, number|
|
|
83
|
+
return [name, content, number] if ANNOTATION_HINT.match?(line)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|