rigortype 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +93 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -1
data/lib/rigor/plugin.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "tempfile"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../analysis/buffer_binding"
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
|
+
require_relative "../inference/fork_map"
|
|
7
8
|
require_relative "diagnostic_oracle"
|
|
8
9
|
require_relative "discovery_seed"
|
|
9
10
|
require_relative "kill_signature"
|
|
@@ -111,6 +112,18 @@ module Rigor
|
|
|
111
112
|
[path, *(@dependents[path] || [])]
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Release the process-private mutant file now, instead of waiting for `Tempfile`'s finalizer at process
|
|
116
|
+
# exit. A one-shot `rigor coverage` run does not need this — the process ends and the file goes with it —
|
|
117
|
+
# but a caller that outlives the oracle does: the spec suite's `after(:suite)` residue check (issue #330)
|
|
118
|
+
# runs while the process is still alive, so an un-released file reads as a leak there even though it
|
|
119
|
+
# would have been reclaimed a moment later. Idempotent, and safe on an oracle that never mutated
|
|
120
|
+
# anything (the file is created lazily).
|
|
121
|
+
def release!
|
|
122
|
+
@mutant_file&.close!
|
|
123
|
+
@mutant_file = nil
|
|
124
|
+
@mutant_pid = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
private
|
|
115
128
|
|
|
116
129
|
# The diagnostic signatures the dependents of `path` report while `source` stands in for it. Empty (and
|
|
@@ -136,11 +149,17 @@ module Rigor
|
|
|
136
149
|
|
|
137
150
|
# The process-private mutant file. Created lazily, and re-created after a fork ({CLI::MutationForkScan}
|
|
138
151
|
# workers must not share one path), which the pid guard detects.
|
|
152
|
+
#
|
|
153
|
+
# A worker's copy goes in {Inference::ForkMap.child_scratch_dir} rather than the system temp dir: the
|
|
154
|
+
# worker ends at `exit!`, which runs neither `at_exit` nor `Tempfile`'s finalizer, so a file placed
|
|
155
|
+
# anywhere else survives the run with nobody left who knows its name (issue #330). On the parent — and
|
|
156
|
+
# on the sequential path — that reader answers `nil`, which is `Tempfile.new`'s own default, and the
|
|
157
|
+
# finalizer reclaims the file at normal exit as before.
|
|
139
158
|
def mutant_file
|
|
140
159
|
return @mutant_file if @mutant_file && @mutant_pid == Process.pid
|
|
141
160
|
|
|
142
161
|
@mutant_pid = Process.pid
|
|
143
|
-
@mutant_file = Tempfile.new(["rigor-mutant-", ".rb"])
|
|
162
|
+
@mutant_file = Tempfile.new(["rigor-mutant-", ".rb"], Inference::ForkMap.child_scratch_dir)
|
|
144
163
|
end
|
|
145
164
|
|
|
146
165
|
def seed_for(buffer)
|
|
@@ -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
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "builtins/imported_refinements"
|
|
5
5
|
require_relative "flow_contribution"
|
|
6
|
+
require_relative "effects/envelope"
|
|
7
|
+
require_relative "effects/label"
|
|
8
|
+
require_relative "effects/label_set"
|
|
9
|
+
require_relative "effects/signature_sources"
|
|
6
10
|
require_relative "rbs_extended/reporter"
|
|
7
11
|
require_relative "rbs_extended/hkt_directives"
|
|
8
12
|
|
|
@@ -621,6 +625,170 @@ module Rigor
|
|
|
621
625
|
facts.empty? ? nil : facts
|
|
622
626
|
end
|
|
623
627
|
|
|
628
|
+
# ── Effect envelopes (ADR-103 WD1 / WD5 / WD14; #383) ──────────────────────────────────────────
|
|
629
|
+
#
|
|
630
|
+
# Two spellings reach the same value. `%a{pure}` is the ecosystem's existing purity annotation
|
|
631
|
+
# (rbs core and Steep both carry it) and reads as the EMPTY envelope; `%a{rigor:v1:effect …}` is
|
|
632
|
+
# Rigor's labelled bound. `rigor:v1:pure` is deliberately NOT implemented — WD14 fixed `%a{pure}`
|
|
633
|
+
# as the only purity spelling.
|
|
634
|
+
|
|
635
|
+
# The bare rbs-native purity annotation. Matched whole, with surrounding whitespace tolerated, so
|
|
636
|
+
# `%a{purely}` and `%a{pure io}` are not it.
|
|
637
|
+
PURE_ANNOTATION_PATTERN = /\A\s*pure\s*\z/
|
|
638
|
+
private_constant :PURE_ANNOTATION_PATTERN
|
|
639
|
+
|
|
640
|
+
# `%a{rigor:v1:effect io.db, nondet.time}` — a space-separated head (the `assert` / `conforms-to`
|
|
641
|
+
# family), then a comma-separated list of BARE label tokens. There is no parenthesised comment
|
|
642
|
+
# form: RBS has real comments. The payload group is optional so that `%a{rigor:v1:effect}` still
|
|
643
|
+
# MATCHES the directive and is reported as malformed rather than read as "not a directive".
|
|
644
|
+
EFFECT_DIRECTIVE_PATTERN = /\Arigor:v1:effect(?:\s+(?<labels>.*?))?\s*\z/
|
|
645
|
+
private_constant :EFFECT_DIRECTIVE_PATTERN
|
|
646
|
+
|
|
647
|
+
# The outcome of reading one `%a{rigor:v1:effect …}` payload.
|
|
648
|
+
#
|
|
649
|
+
# `bound` is the declared {Rigor::Effects::LabelSet}, or {Effects::LabelSet::TOP} when the tag
|
|
650
|
+
# could not be given a meaning. `malformed` says the grammar was violated (an empty list, a token
|
|
651
|
+
# that is not a label); `unknown_labels` says the grammar held but the registry does not
|
|
652
|
+
# recognise a spelling — a different condition with different handling, and what #384's
|
|
653
|
+
# `effect.unknown-label` reads. `labels` is the list exactly as written, recognised or not, which
|
|
654
|
+
# is what lets the diagnostic ask whether some OTHER member of the list was known.
|
|
655
|
+
EffectAnnotation = Data.define(:bound, :labels, :unknown_labels, :malformed) do
|
|
656
|
+
def malformed? = malformed
|
|
657
|
+
def top? = bound.top?
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
NO_EFFECT_LABELS = [].freeze
|
|
661
|
+
private_constant :NO_EFFECT_LABELS
|
|
662
|
+
|
|
663
|
+
# Whether `string` is the bare `%a{pure}` annotation.
|
|
664
|
+
def pure_annotation?(string)
|
|
665
|
+
!string.nil? && PURE_ANNOTATION_PATTERN.match?(string)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Reads one annotation string as an effect-envelope payload.
|
|
669
|
+
#
|
|
670
|
+
# Returns `nil` when the string is not a `rigor:v1:effect` directive at all (so a caller can walk
|
|
671
|
+
# an annotation list without pre-filtering). Otherwise an {EffectAnnotation}:
|
|
672
|
+
#
|
|
673
|
+
# - well-formed and fully recognised → the declared bound;
|
|
674
|
+
# - malformed (empty list, a token outside the label grammar) → {Effects::LabelSet::TOP}, and a
|
|
675
|
+
# `record_unresolved` event on `reporter`;
|
|
676
|
+
# - well-formed but carrying a spelling `registry` does not know → {Effects::LabelSet::TOP} and
|
|
677
|
+
# the unrecognised spellings, because an unknown label makes the WHOLE tag ⊤ rather than the
|
|
678
|
+
# subset that happened to parse. Narrowing to the recognised subset would turn a typo into
|
|
679
|
+
# findings on correct code; widening suppresses them, which is the direction the false-positive
|
|
680
|
+
# budget runs (ADR-5).
|
|
681
|
+
def parse_effect_annotation(string, registry: nil, reporter: nil, source_location: nil)
|
|
682
|
+
match = EFFECT_DIRECTIVE_PATTERN.match(string.to_s)
|
|
683
|
+
return nil if match.nil?
|
|
684
|
+
|
|
685
|
+
tokens = match[:labels].to_s.split(",", -1).map(&:strip)
|
|
686
|
+
if tokens.empty? || tokens.any? { |token| !Effects::Label.valid?(token) }
|
|
687
|
+
record_unresolved(reporter, string, source_location)
|
|
688
|
+
return EffectAnnotation.new(
|
|
689
|
+
bound: Effects::LabelSet::TOP, labels: NO_EFFECT_LABELS, unknown_labels: NO_EFFECT_LABELS, malformed: true
|
|
690
|
+
)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
tokens = tokens.freeze
|
|
694
|
+
unknown = registry.nil? ? [] : tokens.reject { |token| registry.known?(token) }
|
|
695
|
+
EffectAnnotation.new(
|
|
696
|
+
bound: unknown.empty? ? Effects::LabelSet.new(tokens) : Effects::LabelSet::TOP,
|
|
697
|
+
labels: tokens, unknown_labels: unknown.uniq.sort.freeze, malformed: false
|
|
698
|
+
)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Reads the effect envelope off an annotation list — `RBS::Definition::Method#annotations`, or the
|
|
702
|
+
# `#annotations` of an `RBS::AST::Members::MethodDefinition` / class declaration, which carry the
|
|
703
|
+
# same `(string, location)` shape.
|
|
704
|
+
#
|
|
705
|
+
# Precedence, per the spec: `%a{pure}` and `%a{rigor:v1:effect …}` on ONE declaration are
|
|
706
|
+
# contradictory and `pure` wins; the contradiction is recorded on `reporter` (the existing
|
|
707
|
+
# `RBS::Extended` conflict channel) rather than silently resolved. Returns `nil` when the list
|
|
708
|
+
# carries neither spelling.
|
|
709
|
+
#
|
|
710
|
+
# @param annotations [Array<#string>] the node's annotations, in source order.
|
|
711
|
+
# @param owner_key [String] the method key (`Class#m` / `Class.m`) or class name the bound binds.
|
|
712
|
+
# @param source [Symbol] {Effects::Envelope::SOURCES} member to stamp when the labelled spelling
|
|
713
|
+
# matched; `%a{pure}` always stamps `:pure_annotation`, and a class-level read overrides both.
|
|
714
|
+
def read_effect_envelope(annotations, owner_key:, source: :effect_annotation, registry: nil, reporter: nil)
|
|
715
|
+
return nil if annotations.nil? || annotations.empty?
|
|
716
|
+
|
|
717
|
+
pure = annotations.find { |annotation| pure_annotation?(annotation.string) }
|
|
718
|
+
labelled = nil
|
|
719
|
+
parsed = nil
|
|
720
|
+
annotations.each do |annotation|
|
|
721
|
+
result = parse_effect_annotation(
|
|
722
|
+
annotation.string, registry: registry, reporter: reporter, source_location: annotation_location(annotation)
|
|
723
|
+
)
|
|
724
|
+
next if result.nil?
|
|
725
|
+
|
|
726
|
+
labelled = annotation
|
|
727
|
+
parsed = result
|
|
728
|
+
break
|
|
729
|
+
end
|
|
730
|
+
return nil if pure.nil? && labelled.nil?
|
|
731
|
+
|
|
732
|
+
if pure && labelled
|
|
733
|
+
record_unresolved(
|
|
734
|
+
reporter,
|
|
735
|
+
"`%a{pure}` and `%a{#{labelled.string}}` on one declaration are contradictory; `pure` wins",
|
|
736
|
+
annotation_location(pure)
|
|
737
|
+
)
|
|
738
|
+
end
|
|
739
|
+
return build_pure_envelope(pure, owner_key: owner_key, source: source) if pure
|
|
740
|
+
|
|
741
|
+
build_effect_envelope(labelled, parsed, owner_key: owner_key, source: source)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def build_pure_envelope(annotation, owner_key:, source:)
|
|
745
|
+
Effects::Envelope.build(
|
|
746
|
+
owner_key: owner_key,
|
|
747
|
+
bound: Effects::LabelSet::EMPTY,
|
|
748
|
+
source: source == :class_annotation ? :class_annotation : :pure_annotation,
|
|
749
|
+
location: render_annotation_location(annotation),
|
|
750
|
+
spelling: "%a{#{annotation.string}}"
|
|
751
|
+
)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def build_effect_envelope(annotation, parsed, owner_key:, source:)
|
|
755
|
+
Effects::Envelope.build(
|
|
756
|
+
owner_key: owner_key,
|
|
757
|
+
bound: parsed.bound,
|
|
758
|
+
source: source,
|
|
759
|
+
location: render_annotation_location(annotation),
|
|
760
|
+
spelling: "%a{#{annotation.string}}",
|
|
761
|
+
unknown_labels: parsed.unknown_labels,
|
|
762
|
+
declared_labels: parsed.labels
|
|
763
|
+
)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def annotation_location(annotation)
|
|
767
|
+
annotation.respond_to?(:location) ? annotation.location : nil
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# `path:line` for the annotation, project-relative when it sits under the working directory, so a
|
|
771
|
+
# diagnostic message names `sig/foo.rbs:12` rather than an absolute path.
|
|
772
|
+
def render_annotation_location(annotation)
|
|
773
|
+
location = annotation_location(annotation)
|
|
774
|
+
return nil if location.nil?
|
|
775
|
+
|
|
776
|
+
buffer = location.respond_to?(:buffer) ? location.buffer : nil
|
|
777
|
+
name = buffer.respond_to?(:name) ? buffer.name.to_s : nil
|
|
778
|
+
return nil if name.nil? || name.empty?
|
|
779
|
+
|
|
780
|
+
line = location.respond_to?(:start_line) ? location.start_line : 1
|
|
781
|
+
"#{relative_annotation_path(name)}:#{line}"
|
|
782
|
+
rescue StandardError
|
|
783
|
+
nil
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# The buffer-name → readable-path rule lives with the walk that produces the buffers
|
|
787
|
+
# ({Effects::SignatureSources}), so a `virtual:` name is stripped identically wherever it surfaces.
|
|
788
|
+
def relative_annotation_path(name)
|
|
789
|
+
Effects::SignatureSources.source_path(name)
|
|
790
|
+
end
|
|
791
|
+
|
|
624
792
|
# ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events
|
|
625
793
|
# uniformly without nil-checking each time. When the reporter is nil (the v0.1.0 → v0.1.3 default for call
|
|
626
794
|
# sites that do not yet thread `environment:`), the call is a no-op and the parser stays fail-soft.
|
data/lib/rigor/reflection.rb
CHANGED
|
@@ -44,6 +44,10 @@ module Rigor
|
|
|
44
44
|
# out of scope for the v0.0.7 first pass; v0.1.0's plugin API added it as a separate
|
|
45
45
|
# concern.
|
|
46
46
|
module Reflection
|
|
47
|
+
# #354 — thread-local slot for the per-run ancestor-scope memo. See {.ancestor_constant_scopes}.
|
|
48
|
+
ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
|
|
49
|
+
private_constant :ANCESTOR_SCOPES_KEY
|
|
50
|
+
|
|
47
51
|
module_function
|
|
48
52
|
|
|
49
53
|
# @param class_name [String, Symbol]
|
|
@@ -118,42 +122,139 @@ module Rigor
|
|
|
118
122
|
# reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
|
|
119
123
|
# value-pinned `Constant[Regexp]` match-predicate operand.
|
|
120
124
|
def resolve_constant_type(name, scope: Scope.empty)
|
|
125
|
+
prefix = enclosing_class_path(scope)
|
|
126
|
+
|
|
127
|
+
# Step 1 — `Module.nesting`, innermost first. Each entry contributes only its OWN constants.
|
|
128
|
+
walker = prefix
|
|
129
|
+
while walker && !walker.empty?
|
|
130
|
+
hit = constant_type_at("#{walker}::#{name}", scope)
|
|
131
|
+
return hit if hit
|
|
132
|
+
|
|
133
|
+
idx = walker.rindex("::")
|
|
134
|
+
walker = idx ? walker[0, idx] : nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Step 2 (#354) — the ancestors of the innermost cresting scope, which Ruby consults BEFORE
|
|
138
|
+
# falling back to the top level. Skipping this step did not merely lose a resolution: when the
|
|
139
|
+
# same name also exists at top level, step 3 answered a lookup Ruby gives to the ancestor, so
|
|
140
|
+
# `KEY` inside `class Sub < Base` typed as the top-level constant rather than `Base::KEY` — a
|
|
141
|
+
# wrong type on correct code, which outranks any worst-case reading (AGENTS.md
|
|
142
|
+
# § "Implementation Guidelines"). Only project ancestors are walked; an RBS-known superclass
|
|
143
|
+
# contributes no name here (see {.ancestor_constant_scopes}).
|
|
144
|
+
if prefix
|
|
145
|
+
ancestor_constant_scopes(prefix, scope).each do |ancestor|
|
|
146
|
+
hit = constant_type_at("#{ancestor}::#{name}", scope)
|
|
147
|
+
return hit if hit
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Step 3 — the bare name (top level).
|
|
152
|
+
constant_type_at(name, scope)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# One candidate name, consulted in source-precedence order: the class registry (yielding a
|
|
156
|
+
# `Singleton[C]`), source-discovered classes, in-source value constants, then RBS-side
|
|
157
|
+
# constants. In-source values win over RBS constant decls because the user's source is
|
|
158
|
+
# authoritative for its own constants. Returns nil when no source knows `candidate`.
|
|
159
|
+
def constant_type_at(candidate, scope)
|
|
121
160
|
env = scope.environment
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
|
|
162
|
+
singleton = env.singleton_for_name(candidate)
|
|
163
|
+
return singleton if singleton
|
|
164
|
+
|
|
165
|
+
in_source_class = scope.discovered_classes[candidate]
|
|
166
|
+
return in_source_class if in_source_class
|
|
167
|
+
|
|
168
|
+
in_source_value = scope.in_source_constants[candidate]
|
|
169
|
+
return in_source_value if in_source_value
|
|
170
|
+
|
|
171
|
+
env.constant_for_name(candidate)
|
|
172
|
+
end
|
|
173
|
+
private_class_method :constant_type_at
|
|
174
|
+
|
|
175
|
+
# #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
|
|
176
|
+
# ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
|
|
177
|
+
# transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
|
|
178
|
+
#
|
|
179
|
+
# Only PROJECT ancestors appear. `Scope#superclass_of` / `#includes_of` carry as-written names
|
|
180
|
+
# from the discovery pre-pass, and an as-written name that resolves to no discovered class or
|
|
181
|
+
# module is dropped — so a `class Foo < ActiveRecord::Base` contributes nothing and a constant
|
|
182
|
+
# owned by an RBS-known ancestor still resolves only if the bare name reaches it at step 3. That
|
|
183
|
+
# gap is deliberate for this slice: widening to the RBS ancestor graph is a separate question
|
|
184
|
+
# with its own FP surface.
|
|
185
|
+
#
|
|
186
|
+
# Memoised per run because step 2 runs on every constant reference whose lexical candidates all
|
|
187
|
+
# miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
|
|
188
|
+
# bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
|
|
189
|
+
# back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
|
|
190
|
+
# ADR-62 warm loop) cannot hit stale entries.
|
|
191
|
+
def ancestor_constant_scopes(class_name, scope)
|
|
192
|
+
# ADR-46: `superclass_of` / `includes_of` record a cross-file class dependency per consumer
|
|
193
|
+
# file, and the memo is run-scoped rather than file-scoped — a hit would skip the recording and
|
|
194
|
+
# under-record the edge for every later file. Recording runs are rare (incremental only), so
|
|
195
|
+
# they simply bypass the memo rather than complicate its key.
|
|
196
|
+
return compute_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
|
|
197
|
+
|
|
198
|
+
generation = scope.run_generation || scope.discovered_superclasses
|
|
199
|
+
slot = Thread.current[ANCESTOR_SCOPES_KEY]
|
|
200
|
+
unless slot && slot[0].equal?(generation)
|
|
201
|
+
slot = [generation, {}]
|
|
202
|
+
Thread.current[ANCESTOR_SCOPES_KEY] = slot
|
|
138
203
|
end
|
|
139
|
-
|
|
204
|
+
bucket = slot[1]
|
|
205
|
+
bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
|
|
140
206
|
end
|
|
207
|
+
private_class_method :ancestor_constant_scopes
|
|
141
208
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
209
|
+
def compute_ancestor_constant_scopes(class_name, scope)
|
|
210
|
+
queue = [class_name]
|
|
211
|
+
seen = { class_name => true }
|
|
212
|
+
out = []
|
|
213
|
+
until queue.empty?
|
|
214
|
+
current = queue.shift
|
|
215
|
+
# Mixins first, then the superclass — Ruby's ancestor order.
|
|
216
|
+
scope.includes_of(current).each do |raw|
|
|
217
|
+
resolved = resolve_ancestor_name(current, raw, scope)
|
|
218
|
+
next if resolved.nil? || seen[resolved]
|
|
219
|
+
|
|
220
|
+
seen[resolved] = true
|
|
221
|
+
out << resolved
|
|
222
|
+
queue << resolved
|
|
223
|
+
end
|
|
224
|
+
raw_super = scope.superclass_of(current)
|
|
225
|
+
next if raw_super.nil?
|
|
226
|
+
|
|
227
|
+
resolved_super = resolve_ancestor_name(current, raw_super, scope)
|
|
228
|
+
next if resolved_super.nil? || seen[resolved_super]
|
|
229
|
+
|
|
230
|
+
seen[resolved_super] = true
|
|
231
|
+
out << resolved_super
|
|
232
|
+
queue << resolved_super
|
|
233
|
+
end
|
|
234
|
+
out.freeze
|
|
235
|
+
end
|
|
236
|
+
private_class_method :compute_ancestor_constant_scopes
|
|
237
|
+
|
|
238
|
+
# Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the
|
|
239
|
+
# subclass's lexical nesting, innermost first — the same walk
|
|
240
|
+
# `ExpressionTyper#compute_ancestor_class_name` performs for method lookup. Returns nil when no
|
|
241
|
+
# candidate names a discovered project class or module.
|
|
242
|
+
def resolve_ancestor_name(subclass_qualified, raw, scope)
|
|
243
|
+
segments = subclass_qualified.split("::")
|
|
244
|
+
(segments.length - 1).downto(0) do |i|
|
|
245
|
+
candidate = (segments[0, i] + [raw]).join("::")
|
|
246
|
+
return candidate if known_project_namespace?(candidate, scope)
|
|
152
247
|
end
|
|
153
|
-
|
|
154
|
-
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
private_class_method :resolve_ancestor_name
|
|
251
|
+
|
|
252
|
+
def known_project_namespace?(name, scope)
|
|
253
|
+
scope.discovered_superclasses.key?(name) ||
|
|
254
|
+
scope.discovered_includes.key?(name) ||
|
|
255
|
+
scope.discovered_classes.key?(name)
|
|
155
256
|
end
|
|
156
|
-
private_class_method :
|
|
257
|
+
private_class_method :known_project_namespace?
|
|
157
258
|
|
|
158
259
|
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set.
|
|
159
260
|
# `Nominal[T]` and `Singleton[T]` both expose `class_name`. Returns nil at the top level.
|