rigortype 0.1.16 → 0.1.18
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 +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
- data/lib/rigor/analysis/check_rules.rb +180 -73
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +477 -1110
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +153 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +145 -14
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +15 -532
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +16 -3
- data/lib/rigor/environment/rbs_loader.rb +129 -71
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -63
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +185 -84
- data/lib/rigor/inference/narrowing.rb +262 -5
- data/lib/rigor/inference/scope_indexer.rb +208 -21
- data/lib/rigor/inference/statement_evaluator.rb +110 -48
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +302 -45
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +281 -15
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +150 -167
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +22 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +27 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +42 -25
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +36 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
|
@@ -4,6 +4,227 @@ require_relative "blueprint"
|
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Plugin
|
|
7
|
+
# ADR-52 WD1 — the compiled contribution table. Categorises a loaded
|
|
8
|
+
# plugin set by which per-call contribution paths each plugin
|
|
9
|
+
# actually implements, AND compiles the declarative gates (method
|
|
10
|
+
# names, `block_as_methods` verbs, `owns_receivers`) into frozen
|
|
11
|
+
# lookup structures, so the engine's hot sites discover "no plugin
|
|
12
|
+
# cares about this call" in O(1) instead of O(plugins × rules) — a
|
|
13
|
+
# top hotspot on plugin-heavy projects (GitLab's 11 plugins, of
|
|
14
|
+
# which only 2 implement any per-call path). Built once per
|
|
15
|
+
# {Registry}.
|
|
16
|
+
#
|
|
17
|
+
# Ordering contract: the gates only PRUNE consultations that could
|
|
18
|
+
# not fire (every pruned rule would have failed its own `methods:` /
|
|
19
|
+
# `verbs:` check); the engine still iterates the plugin subsets in
|
|
20
|
+
# registry order and each plugin's rules in declaration order, so
|
|
21
|
+
# the surviving contributions arrive in exactly the order the
|
|
22
|
+
# ungated walk produced — diagnostics stay byte-identical. The
|
|
23
|
+
# receiver-class ancestry match for `dynamic_return` still happens
|
|
24
|
+
# per-dispatch inside `Plugin::Base#dynamic_return_type`.
|
|
25
|
+
class ContributionIndex
|
|
26
|
+
# Ordered (registry-order) subsets relevant to each collector, and
|
|
27
|
+
# the membership sets used to gate the paths within a plugin.
|
|
28
|
+
# `for_file_diagnostics` is the subset the runner's per-file
|
|
29
|
+
# diagnostic loop visits: plugins overriding
|
|
30
|
+
# `#diagnostics_for_file` or declaring at least one `node_rule`.
|
|
31
|
+
attr_reader :for_method_dispatch, :for_statement, :for_file_diagnostics
|
|
32
|
+
|
|
33
|
+
EMPTY_BLOCK_ENTRIES = [].freeze
|
|
34
|
+
private_constant :EMPTY_BLOCK_ENTRIES
|
|
35
|
+
|
|
36
|
+
def initialize(plugins)
|
|
37
|
+
compile_memberships(plugins)
|
|
38
|
+
compile_gates
|
|
39
|
+
@block_entries_by_verb = build_block_entries(plugins)
|
|
40
|
+
@owns_receivers = plugins.flat_map { |p| manifest_for(p)&.owns_receivers || [] }.uniq.freeze
|
|
41
|
+
# Per-run ancestry verdict memo, keyed by environment identity
|
|
42
|
+
# then class name. Mutable inside the frozen index — sound
|
|
43
|
+
# because the class graph is fixed for the lifetime of a run.
|
|
44
|
+
@owns_receiver_memo = {}.compare_by_identity
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dynamic?(plugin) = @dynamic.include?(plugin)
|
|
49
|
+
def type_specifier?(plugin) = @type_specifier.include?(plugin)
|
|
50
|
+
|
|
51
|
+
# O(1) "could any plugin contribute a return type for a call named
|
|
52
|
+
# `method_name`?" — false only when every `dynamic_return` rule is
|
|
53
|
+
# `methods:`-gated on other names, in which case the ungated walk
|
|
54
|
+
# would have produced zero contributions too.
|
|
55
|
+
def dispatch_candidate?(method_name)
|
|
56
|
+
return true if @dynamic_global_gate.nil?
|
|
57
|
+
|
|
58
|
+
@dynamic_global_gate.include?(method_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# O(1) statement-path sibling of {#dispatch_candidate?} over the
|
|
62
|
+
# `type_specifier` rules (which are always `methods:`-gated).
|
|
63
|
+
def statement_candidate?(method_name)
|
|
64
|
+
return true if @type_specifier_global_gate.nil?
|
|
65
|
+
|
|
66
|
+
@type_specifier_global_gate.include?(method_name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Per-plugin gate: false when the plugin declares no
|
|
70
|
+
# `dynamic_return` rules at all, or when every rule is
|
|
71
|
+
# `methods:`-gated and none lists `method_name` — i.e. when
|
|
72
|
+
# `#dynamic_return_type` would return nil without ever entering a
|
|
73
|
+
# rule block. Subsumes the old `dynamic?(plugin)` membership
|
|
74
|
+
# check at the collector's call site.
|
|
75
|
+
def dynamic_candidate_for?(plugin, method_name)
|
|
76
|
+
return false unless @dynamic.include?(plugin)
|
|
77
|
+
|
|
78
|
+
gate = @dynamic_gates[plugin]
|
|
79
|
+
gate.nil? || gate.include?(method_name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Per-plugin gate over `type_specifier` rules; same contract as
|
|
83
|
+
# {#dynamic_candidate_for?}.
|
|
84
|
+
def type_specifier_candidate_for?(plugin, method_name)
|
|
85
|
+
return false unless @type_specifier.include?(plugin)
|
|
86
|
+
|
|
87
|
+
gate = @type_specifier_gates[plugin]
|
|
88
|
+
gate.nil? || gate.include?(method_name)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The `Macro::BlockAsMethod` entries whose `verbs` include `verb`,
|
|
92
|
+
# in (plugin registration, manifest declaration) order — the same
|
|
93
|
+
# first-match order the previous plugins × entries walk visited.
|
|
94
|
+
def block_entries_for(verb)
|
|
95
|
+
@block_entries_by_verb.fetch(verb, EMPTY_BLOCK_ENTRIES)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# True when `class_name` equals or inherits from any plugin's
|
|
99
|
+
# manifest-declared `owns_receivers:` entry. The union is compiled
|
|
100
|
+
# at build time (almost always empty → O(1) false) and per-class
|
|
101
|
+
# verdicts memoise per environment.
|
|
102
|
+
def owns_receiver?(class_name, environment)
|
|
103
|
+
return false if @owns_receivers.empty? || class_name.nil?
|
|
104
|
+
|
|
105
|
+
memo = (@owns_receiver_memo[environment] ||= {})
|
|
106
|
+
memo.fetch(class_name) do
|
|
107
|
+
memo[class_name] =
|
|
108
|
+
@owns_receivers.any? { |owner| class_matches_owner?(class_name, owner, environment) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# The categorisation sets + the ordered per-collector subsets.
|
|
115
|
+
def compile_memberships(plugins)
|
|
116
|
+
plugins.each { |p| reject_legacy_flow_hook!(p) }
|
|
117
|
+
@dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set
|
|
118
|
+
@type_specifier = plugins.reject { |p| p.class.type_specifiers.empty? }.to_set
|
|
119
|
+
compile_collector_subsets(plugins)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def compile_collector_subsets(plugins)
|
|
123
|
+
@for_method_dispatch = plugins.select { |p| @dynamic.include?(p) }.freeze
|
|
124
|
+
@for_statement = plugins.select { |p| @type_specifier.include?(p) }.freeze
|
|
125
|
+
@for_file_diagnostics =
|
|
126
|
+
plugins.select { |p| file_diagnostics_overridden?(p) || !p.class.node_rules.empty? }.freeze
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The per-plugin and registry-global method-name gates.
|
|
130
|
+
def compile_gates
|
|
131
|
+
@dynamic_gates = build_name_gates(@dynamic) { |p| p.class.dynamic_returns }
|
|
132
|
+
@type_specifier_gates = build_name_gates(@type_specifier) { |p| p.class.type_specifiers }
|
|
133
|
+
@dynamic_global_gate = union_gate(@dynamic_gates)
|
|
134
|
+
@type_specifier_global_gate = union_gate(@type_specifier_gates)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ADR-52 WD3 — the legacy ungated `flow_contribution_for` hook was
|
|
138
|
+
# deleted pre-1.0. A plugin still defining it would silently never
|
|
139
|
+
# be called, which is the worst failure mode for its author —
|
|
140
|
+
# surface a loud load-time error with the migration mapping
|
|
141
|
+
# instead. (The Base default is gone, so any definer wrote it
|
|
142
|
+
# themselves.)
|
|
143
|
+
def reject_legacy_flow_hook!(plugin)
|
|
144
|
+
return unless plugin.respond_to?(:flow_contribution_for)
|
|
145
|
+
|
|
146
|
+
raise ArgumentError,
|
|
147
|
+
"plugin #{(plugin.class.name || plugin.class).inspect} defines `flow_contribution_for`, " \
|
|
148
|
+
"which was removed (ADR-52). Declare the per-call return type via `dynamic_return` " \
|
|
149
|
+
"(receivers:/methods:/file_methods: gates, static or callable) and post-return narrowing " \
|
|
150
|
+
"facts via `type_specifier` — see the CHANGELOG migration note."
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Same `Method#owner` trick for the per-file diagnostics hook —
|
|
154
|
+
# the Base default returns `[]`, so a non-overriding plugin can be
|
|
155
|
+
# skipped without calling it.
|
|
156
|
+
def file_diagnostics_overridden?(plugin)
|
|
157
|
+
plugin.method(:diagnostics_for_file).owner != Rigor::Plugin::Base
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# `plugin → Set[Symbol] | nil`. A frozen Set when EVERY rule of
|
|
161
|
+
# the plugin is `methods:`-gated (the union of those names); nil
|
|
162
|
+
# when any rule is ungated (the plugin must be consulted for every
|
|
163
|
+
# method name, exactly as before).
|
|
164
|
+
def build_name_gates(members)
|
|
165
|
+
gates = {}
|
|
166
|
+
members.each do |plugin|
|
|
167
|
+
rules = yield(plugin)
|
|
168
|
+
# A static method-name Array can be compiled into the gate. A
|
|
169
|
+
# run-time callable `methods:` (ADR-52 slice 4) is unknown until
|
|
170
|
+
# after `#prepare`, and a receiver-only rule has no `methods:` —
|
|
171
|
+
# either makes the plugin ungated-by-name (nil), consulted on
|
|
172
|
+
# every dispatch and filtered in the instance path.
|
|
173
|
+
gates[plugin] =
|
|
174
|
+
if rules.all? { |rule| rule[:methods].is_a?(Array) }
|
|
175
|
+
rules.flat_map { |rule| rule[:methods] }.to_set.freeze
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
gates.freeze
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The registry-wide union of per-plugin gates, or nil when any
|
|
182
|
+
# plugin is ungated (no global pruning possible). An empty
|
|
183
|
+
# plugin set unions to an empty frozen Set — the collectors'
|
|
184
|
+
# `relevant.empty?` early return fires before it is consulted.
|
|
185
|
+
def union_gate(gates)
|
|
186
|
+
return nil if gates.value?(nil)
|
|
187
|
+
|
|
188
|
+
gates.values.reduce(Set.new) { |acc, names| acc.merge(names) }.freeze
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `verb Symbol → [BlockAsMethod entries]`, insertion-ordered by
|
|
192
|
+
# (plugin, declaration). Verbs are Symbol-normalised by
|
|
193
|
+
# `Macro::BlockAsMethod#initialize`.
|
|
194
|
+
def build_block_entries(plugins)
|
|
195
|
+
table = {}
|
|
196
|
+
plugins.each do |plugin|
|
|
197
|
+
entries = manifest_for(plugin)&.block_as_methods || []
|
|
198
|
+
entries.each do |entry|
|
|
199
|
+
entry.verbs.each { |verb| (table[verb] ||= []) << entry }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
table.each_value(&:freeze)
|
|
203
|
+
table.freeze
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Mirrors `MethodDispatcher`'s previous per-dispatch matching:
|
|
207
|
+
# exact-name fast path, then `Environment#class_ordering`,
|
|
208
|
+
# degrading to "no match" on any resolution failure.
|
|
209
|
+
def class_matches_owner?(class_name, owner, environment)
|
|
210
|
+
return true if class_name == owner
|
|
211
|
+
return false if environment.nil?
|
|
212
|
+
|
|
213
|
+
%i[equal subclass].include?(environment.class_ordering(class_name, owner))
|
|
214
|
+
rescue StandardError
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Manifest access tolerant of manifest-less plugin doubles in unit
|
|
219
|
+
# specs (a real loaded plugin always carries one — the loader
|
|
220
|
+
# validates it).
|
|
221
|
+
def manifest_for(plugin)
|
|
222
|
+
plugin.manifest
|
|
223
|
+
rescue StandardError
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
7
228
|
# Read-side query API over the plugins loaded for a single
|
|
8
229
|
# `Analysis::Runner.run`. Constructed by
|
|
9
230
|
# {Rigor::Plugin::Loader.load} and exposed downstream so the
|
|
@@ -24,7 +245,7 @@ module Rigor
|
|
|
24
245
|
# {.materialize} per-Ractor; the live `plugins` carriage on
|
|
25
246
|
# the coordinator registry stays unchanged.
|
|
26
247
|
class Registry
|
|
27
|
-
attr_reader :plugins, :load_errors, :blueprints
|
|
248
|
+
attr_reader :plugins, :load_errors, :blueprints, :contribution_index
|
|
28
249
|
|
|
29
250
|
# @param plugins [Array<Rigor::Plugin::Base>] instantiated
|
|
30
251
|
# plugin instances in deterministic order.
|
|
@@ -40,9 +261,30 @@ module Rigor
|
|
|
40
261
|
@plugins = plugins.dup.freeze
|
|
41
262
|
@load_errors = load_errors.dup.freeze
|
|
42
263
|
@blueprints = blueprints.dup.freeze
|
|
264
|
+
@contribution_index = ContributionIndex.new(@plugins)
|
|
265
|
+
# ADR-52 WD1 — aggregate queries the engine issues per def /
|
|
266
|
+
# per diagnostic candidate / per path are compiled once here
|
|
267
|
+
# (the registry is frozen, so the flat_map-on-every-call
|
|
268
|
+
# versions re-derived an invariant). `@contracts_by_path` is a
|
|
269
|
+
# mutable per-path memo inside the frozen registry — safe
|
|
270
|
+
# because the contract set and the glob semantics are fixed
|
|
271
|
+
# for the lifetime of the run.
|
|
272
|
+
@additional_initializers = @plugins.flat_map { |p| safe_manifest(p)&.additional_initializers || [] }.freeze
|
|
273
|
+
@open_receivers = @plugins.flat_map { |p| (safe_manifest(p)&.open_receivers || []).map(&:to_s) }.uniq.freeze
|
|
274
|
+
@open_receivers_set = @open_receivers.to_set.freeze
|
|
275
|
+
@protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
|
|
276
|
+
@contracts_by_path = {}
|
|
277
|
+
# ADR-52 WD4 — the single engine-owned node-rule walk, compiled
|
|
278
|
+
# once per run from the node-rule plugin subset (registry order).
|
|
279
|
+
# The runner reuses it for every file; it builds fresh per-file
|
|
280
|
+
# state internally, so it is safe to freeze and share.
|
|
281
|
+
@node_rule_walk = NodeRuleWalk.new(@plugins)
|
|
43
282
|
freeze
|
|
44
283
|
end
|
|
45
284
|
|
|
285
|
+
# ADR-52 WD4 — the per-run node-rule walk (see {NodeRuleWalk}).
|
|
286
|
+
attr_reader :node_rule_walk
|
|
287
|
+
|
|
46
288
|
# ADR-15 Phase 3 — build a fresh Registry from the supplied
|
|
47
289
|
# blueprint set by replaying {Blueprint#materialize} per
|
|
48
290
|
# entry against `services`. The returned registry carries
|
|
@@ -120,14 +362,15 @@ module Rigor
|
|
|
120
362
|
# beyond its RBS-declared method surface. `open_receiver?`
|
|
121
363
|
# is the membership predicate `Analysis::CheckRules` consults
|
|
122
364
|
# to skip the `call.undefined-method` rule for such a class.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
365
|
+
# Compiled at construction (ADR-52 WD1) — the predicate runs per
|
|
366
|
+
# undefined-method candidate, so the previous per-call flat_map
|
|
367
|
+
# re-derived a frozen invariant.
|
|
368
|
+
attr_reader :open_receivers
|
|
126
369
|
|
|
127
370
|
def open_receiver?(class_name)
|
|
128
371
|
return false if class_name.nil?
|
|
129
372
|
|
|
130
|
-
|
|
373
|
+
@open_receivers_set.include?(class_name.to_s)
|
|
131
374
|
end
|
|
132
375
|
|
|
133
376
|
# ADR-28 — flat, ordered list of every loaded plugin's
|
|
@@ -138,10 +381,10 @@ module Rigor
|
|
|
138
381
|
# Consumed by `Inference::MethodParameterBinder` (the
|
|
139
382
|
# parameter-type provision) and by contributing plugins'
|
|
140
383
|
# `#diagnostics_for_file` hooks (the presence + return-type
|
|
141
|
-
# check).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
384
|
+
# check). Compiled at construction (ADR-52 WD1); a plugin's
|
|
385
|
+
# config-folding `#protocol_contracts` override is stable for the
|
|
386
|
+
# run because config is injected at construction.
|
|
387
|
+
attr_reader :protocol_contracts
|
|
145
388
|
|
|
146
389
|
# ADR-38 — flat, ordered list of every loaded plugin's
|
|
147
390
|
# manifest-declared `Rigor::Plugin::AdditionalInitializer`
|
|
@@ -149,9 +392,9 @@ module Rigor
|
|
|
149
392
|
# read-before-write nil soundness gate: a `def` whose name an
|
|
150
393
|
# entry covers, on a class that equals or inherits from the
|
|
151
394
|
# entry's `receiver_constraint`, is treated like `initialize`.
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
395
|
+
# Compiled at construction (ADR-52 WD1) — consulted per `def`
|
|
396
|
+
# node, ×2 sites in `ScopeIndexer`.
|
|
397
|
+
attr_reader :additional_initializers
|
|
155
398
|
|
|
156
399
|
# ADR-28 — the subset of `protocol_contracts` whose
|
|
157
400
|
# `path_glob` matches `path`. Contract globs are authored
|
|
@@ -163,11 +406,15 @@ module Rigor
|
|
|
163
406
|
# suffix. `File::FNM_PATHNAME` keeps `*` from crossing `/`;
|
|
164
407
|
# `File::FNM_EXTGLOB` enables `{a,b}` groups. Returns `[]` for
|
|
165
408
|
# a nil path so the binder can call this unconditionally.
|
|
409
|
+
# Memoised per path (ADR-52 WD1) — consulted per `def` node by
|
|
410
|
+
# `MethodParameterBinder`, and the fnmatch sweep over every
|
|
411
|
+
# contract is pure in (contract set, path).
|
|
166
412
|
def contracts_for_path(path)
|
|
167
413
|
return [] if path.nil?
|
|
168
414
|
|
|
169
415
|
path_s = path.to_s
|
|
170
|
-
|
|
416
|
+
@contracts_by_path[path_s] ||=
|
|
417
|
+
protocol_contracts.select { |contract| path_matches_glob?(contract.path_glob, path_s) }.freeze
|
|
171
418
|
end
|
|
172
419
|
|
|
173
420
|
# ADR-32 WD4 + WD5 — flat ordered list of
|
|
@@ -194,14 +441,33 @@ module Rigor
|
|
|
194
441
|
FNMATCH_FLAGS = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
195
442
|
private_constant :FNMATCH_FLAGS
|
|
196
443
|
|
|
197
|
-
EMPTY = new.freeze
|
|
198
|
-
|
|
199
444
|
private
|
|
200
445
|
|
|
201
446
|
def path_matches_glob?(glob, path)
|
|
202
447
|
File.fnmatch?(glob, path, FNMATCH_FLAGS) ||
|
|
203
448
|
File.fnmatch?(File.join("**", glob), path, FNMATCH_FLAGS)
|
|
204
449
|
end
|
|
450
|
+
|
|
451
|
+
# Construction-time manifest access tolerant of manifest-less
|
|
452
|
+
# plugin doubles in unit specs (a real loaded plugin always
|
|
453
|
+
# carries one — the loader validates it). Mirrors
|
|
454
|
+
# `ContributionIndex#manifest_for`.
|
|
455
|
+
def safe_manifest(plugin)
|
|
456
|
+
plugin.manifest
|
|
457
|
+
rescue StandardError
|
|
458
|
+
nil
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def safe_protocol_contracts(plugin)
|
|
462
|
+
plugin.protocol_contracts || []
|
|
463
|
+
rescue StandardError
|
|
464
|
+
[]
|
|
465
|
+
end
|
|
205
466
|
end
|
|
467
|
+
|
|
468
|
+
# Assigned after the class body completes — `Registry.new` runs at
|
|
469
|
+
# assignment time and `#initialize` calls private helpers defined
|
|
470
|
+
# late in the body.
|
|
471
|
+
Registry::EMPTY = Registry.new.freeze
|
|
206
472
|
end
|
|
207
473
|
end
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "plugin/io_boundary"
|
|
|
9
9
|
require_relative "plugin/fact_store"
|
|
10
10
|
require_relative "plugin/services"
|
|
11
11
|
require_relative "plugin/base"
|
|
12
|
+
require_relative "plugin/node_rule_walk"
|
|
12
13
|
require_relative "plugin/registry"
|
|
13
14
|
require_relative "plugin/load_error"
|
|
14
15
|
require_relative "plugin/box"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rbs_extended"
|
|
4
|
+
require_relative "../inference/rbs_type_translator"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module RbsExtended
|
|
8
|
+
# Verifies every `rigor:v1:conforms-to <Interface>` class- /
|
|
9
|
+
# module-level directive in the loaded RBS environment (spec:
|
|
10
|
+
# `docs/type-specification/rbs-extended.md` § "Explicit
|
|
11
|
+
# conformance directive"). For each annotated class, the
|
|
12
|
+
# directive asserts the class satisfies the named structural
|
|
13
|
+
# interface as a *checked design assertion* — independent of
|
|
14
|
+
# whether any current call site exercises that requirement.
|
|
15
|
+
# Multiple directives on one class combine as an intersection:
|
|
16
|
+
# each interface is checked independently.
|
|
17
|
+
#
|
|
18
|
+
# ## Two checked tiers
|
|
19
|
+
#
|
|
20
|
+
# 1. **Presence** (FP-free): an interface method the class provably does
|
|
21
|
+
# NOT provide anywhere in its RBS-resolved method set (own, inherited,
|
|
22
|
+
# included) is a definitive non-conformance.
|
|
23
|
+
# 2. **Signature compatibility** (covariant return / contravariant
|
|
24
|
+
# params): for a method the class DOES provide, the provided RBS
|
|
25
|
+
# signature must be a behavioural subtype of the interface's required
|
|
26
|
+
# one. Both sides are *authored* RBS (the class in `signature_paths:`
|
|
27
|
+
# RBS, the interface in a loaded `sig`/library), so this is the same
|
|
28
|
+
# FP-safe both-sides-authored construction as the
|
|
29
|
+
# [ADR-35](../../../docs/adr/35-override-signature-compatibility.md)
|
|
30
|
+
# `def.override-*` rules — not the inferred-signature comparison whose
|
|
31
|
+
# FP risk justified deferring it. Conservative: single-method-type
|
|
32
|
+
# (non-overloaded) signatures only, `Dynamic[Top]` positions skipped,
|
|
33
|
+
# fires only on a proven `accepts(...).no?` violation.
|
|
34
|
+
#
|
|
35
|
+
# ## Output records (all drained by {Rigor::Analysis::Runner})
|
|
36
|
+
#
|
|
37
|
+
# - `Unsatisfied` — the class is missing one or more required interface
|
|
38
|
+
# methods. Surfaces as `rbs_extended.unsatisfied-conformance`.
|
|
39
|
+
# - `IncompatibleSignature` — a provided method's signature violates the
|
|
40
|
+
# interface contract (return widened, or a parameter narrowed). Same
|
|
41
|
+
# rule, signature-specific message.
|
|
42
|
+
# - `UnresolvedInterface` — the named interface is not loaded (a typo, or
|
|
43
|
+
# the defining library / `sig` set is not on the RBS load path).
|
|
44
|
+
# Surfaces as `dynamic.rbs-extended.unresolved` `:info`, the fail-soft
|
|
45
|
+
# channel the other directive parsers use, so a bad name never silently
|
|
46
|
+
# disables the author's assertion.
|
|
47
|
+
#
|
|
48
|
+
# Fail-soft throughout: a class whose own definition cannot be built (RBS
|
|
49
|
+
# error) is skipped rather than reported.
|
|
50
|
+
module ConformanceChecker
|
|
51
|
+
Unsatisfied = Data.define(:class_name, :interface_name, :missing_methods, :location)
|
|
52
|
+
IncompatibleSignature = Data.define(:class_name, :interface_name, :method_name, :detail, :location)
|
|
53
|
+
UnresolvedInterface = Data.define(:class_name, :interface_name, :location)
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Scans `rbs_loader` for `conforms-to` directives and returns the
|
|
58
|
+
# failure / unresolved records in source order. Returns an empty array
|
|
59
|
+
# when no directive is present, the loader is nil, or the env failed to
|
|
60
|
+
# build (the loader's iterators are themselves fail-soft).
|
|
61
|
+
def scan(rbs_loader)
|
|
62
|
+
return [] if rbs_loader.nil?
|
|
63
|
+
|
|
64
|
+
results = []
|
|
65
|
+
rbs_loader.each_class_decl_annotation_with_name do |class_name, string, location|
|
|
66
|
+
interface_name = RbsExtended.parse_conforms_to_annotation(string)
|
|
67
|
+
next if interface_name.nil?
|
|
68
|
+
|
|
69
|
+
results.concat(check_one(rbs_loader, class_name, interface_name, location))
|
|
70
|
+
end
|
|
71
|
+
results
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Array] zero or more records for one (class, interface) pair.
|
|
75
|
+
def check_one(rbs_loader, class_name, interface_name, location)
|
|
76
|
+
interface_def = resolve_interface(rbs_loader, class_name, interface_name)
|
|
77
|
+
if interface_def.nil?
|
|
78
|
+
return [UnresolvedInterface.new(
|
|
79
|
+
class_name: normalize(class_name), interface_name: interface_name, location: location
|
|
80
|
+
)]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class_def = rbs_loader.instance_definition(class_name)
|
|
84
|
+
return [] if class_def.nil? # fail-soft: cannot prove non-conformance
|
|
85
|
+
|
|
86
|
+
required = interface_def.methods
|
|
87
|
+
provided = class_def.methods
|
|
88
|
+
records = []
|
|
89
|
+
collect_missing(records, class_name, interface_name, required, provided, location)
|
|
90
|
+
collect_incompatible(records, class_name, interface_name, required, provided, location)
|
|
91
|
+
records
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def collect_missing(records, class_name, interface_name, required, provided, location)
|
|
95
|
+
missing = required.keys - provided.keys
|
|
96
|
+
return if missing.empty?
|
|
97
|
+
|
|
98
|
+
records << Unsatisfied.new(
|
|
99
|
+
class_name: normalize(class_name), interface_name: interface_name,
|
|
100
|
+
missing_methods: missing, location: location
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def collect_incompatible(records, class_name, interface_name, required, provided, location)
|
|
105
|
+
(required.keys & provided.keys).each do |method_name|
|
|
106
|
+
detail = signature_mismatch(required[method_name], provided[method_name])
|
|
107
|
+
next if detail.nil?
|
|
108
|
+
|
|
109
|
+
records << IncompatibleSignature.new(
|
|
110
|
+
class_name: normalize(class_name), interface_name: interface_name,
|
|
111
|
+
method_name: method_name, detail: detail, location: location
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns a human-readable mismatch detail when `provided` is NOT a
|
|
117
|
+
# behavioural subtype of the `required` (interface) signature, else
|
|
118
|
+
# nil. Mirrors the ADR-35 override checks: covariant return,
|
|
119
|
+
# contravariant params, single method type only, `Dynamic[Top]`
|
|
120
|
+
# positions skipped (fires only on a proven `accepts(...).no?`).
|
|
121
|
+
# Also checks arity divergence and keyword-requiredness divergence
|
|
122
|
+
# (positional-type comparison only was the initial scope; these
|
|
123
|
+
# extend it to the cases that cause runtime ArgumentError).
|
|
124
|
+
def signature_mismatch(required_method, provided_method)
|
|
125
|
+
return nil unless required_method.method_types.size == 1
|
|
126
|
+
return nil unless provided_method.method_types.size == 1
|
|
127
|
+
|
|
128
|
+
return_detail(required_method, provided_method) ||
|
|
129
|
+
param_detail(required_method, provided_method) ||
|
|
130
|
+
arity_detail(required_method, provided_method) ||
|
|
131
|
+
keyword_detail(required_method, provided_method)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def return_detail(required_method, provided_method)
|
|
135
|
+
req = translate(required_method.method_types.first.type.return_type)
|
|
136
|
+
prov = translate(provided_method.method_types.first.type.return_type)
|
|
137
|
+
return nil if req.nil? || prov.nil?
|
|
138
|
+
return nil if dynamic_top?(req) || dynamic_top?(prov)
|
|
139
|
+
return nil unless req.accepts(prov).no?
|
|
140
|
+
|
|
141
|
+
"return type #{prov.describe(:short)} is not a subtype of the required #{req.describe(:short)}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def param_detail(required_method, provided_method)
|
|
145
|
+
req = positional_param_types(required_method)
|
|
146
|
+
prov = positional_param_types(provided_method)
|
|
147
|
+
return nil if req.nil? || prov.nil?
|
|
148
|
+
|
|
149
|
+
[req.size, prov.size].min.times do |i|
|
|
150
|
+
rp = req[i]
|
|
151
|
+
pp = prov[i]
|
|
152
|
+
next if rp.nil? || pp.nil? || dynamic_top?(rp) || dynamic_top?(pp)
|
|
153
|
+
next unless pp.accepts(rp).no?
|
|
154
|
+
|
|
155
|
+
return "parameter #{i + 1} type #{pp.describe(:short)} does not accept the " \
|
|
156
|
+
"required #{rp.describe(:short)}"
|
|
157
|
+
end
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Checks positional-count divergence — cases that would cause a
|
|
162
|
+
# runtime `ArgumentError` even when every declared type matches.
|
|
163
|
+
# Skipped when either side has a rest parameter (`*args`) since
|
|
164
|
+
# that makes the arity range unbounded. Two violation shapes:
|
|
165
|
+
# (a) provided requires MORE positionals than the interface allows
|
|
166
|
+
# (caller passes ≤ interface total → provided raises);
|
|
167
|
+
# (b) provided accepts FEWER positionals than the interface requires
|
|
168
|
+
# (caller passes ≥ interface required → provided raises).
|
|
169
|
+
def arity_detail(required_method, provided_method)
|
|
170
|
+
req_func = required_method.method_types.first.type
|
|
171
|
+
prov_func = provided_method.method_types.first.type
|
|
172
|
+
return nil unless req_func.respond_to?(:required_positionals)
|
|
173
|
+
return nil unless prov_func.respond_to?(:required_positionals)
|
|
174
|
+
|
|
175
|
+
req_req = req_func.required_positionals.size
|
|
176
|
+
req_opt = req_func.optional_positionals.size
|
|
177
|
+
prov_req = prov_func.required_positionals.size
|
|
178
|
+
prov_opt = prov_func.required_positionals.size + prov_func.optional_positionals.size
|
|
179
|
+
|
|
180
|
+
# (a) provided requires too many — callers may not pass enough
|
|
181
|
+
if req_func.rest_positionals.nil? && prov_req > req_req + req_opt
|
|
182
|
+
return "requires #{prov_req} positional argument#{'s' if prov_req != 1} " \
|
|
183
|
+
"but the interface allows at most #{req_req + req_opt}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# (b) provided accepts too few — callers will pass more than provided can handle
|
|
187
|
+
if prov_func.rest_positionals.nil? && req_req > prov_opt
|
|
188
|
+
return "accepts at most #{prov_opt} positional argument#{'s' if prov_opt != 1} " \
|
|
189
|
+
"but the interface requires at least #{req_req}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Checks keyword-requiredness divergence.
|
|
196
|
+
# (a) A keyword the interface requires that the provided method does
|
|
197
|
+
# not accept at all → callers will pass it, provided will raise.
|
|
198
|
+
# (b) A keyword the provided method requires that the interface does
|
|
199
|
+
# not mention → callers following the interface won't pass it,
|
|
200
|
+
# provided will raise.
|
|
201
|
+
# Skipped when either side has a keyword rest (`**kwargs`).
|
|
202
|
+
def keyword_detail(required_method, provided_method)
|
|
203
|
+
req_func, prov_func = keyword_funcs(required_method, provided_method)
|
|
204
|
+
return nil unless req_func
|
|
205
|
+
|
|
206
|
+
prov_accepted = accepted_keywords(prov_func)
|
|
207
|
+
req_accepted = accepted_keywords(req_func)
|
|
208
|
+
|
|
209
|
+
not_accepted = req_func.required_keywords.keys - prov_accepted
|
|
210
|
+
return keyword_mismatch_message("does not accept required", not_accepted) if not_accepted.any?
|
|
211
|
+
|
|
212
|
+
extra_required = prov_func.required_keywords.keys - req_accepted
|
|
213
|
+
if extra_required.any?
|
|
214
|
+
return keyword_mismatch_message("requires", extra_required,
|
|
215
|
+
suffix: " not declared by the interface")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def keyword_funcs(required_method, provided_method)
|
|
222
|
+
req_func = required_method.method_types.first.type
|
|
223
|
+
prov_func = provided_method.method_types.first.type
|
|
224
|
+
return [nil, nil] unless req_func.respond_to?(:required_keywords)
|
|
225
|
+
return [nil, nil] unless prov_func.respond_to?(:required_keywords)
|
|
226
|
+
return [nil, nil] unless req_func.rest_keywords.nil? && prov_func.rest_keywords.nil?
|
|
227
|
+
|
|
228
|
+
[req_func, prov_func]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def accepted_keywords(func)
|
|
232
|
+
func.required_keywords.keys + func.optional_keywords.keys
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def keyword_mismatch_message(prefix, kw_keys, suffix: "")
|
|
236
|
+
listed = kw_keys.sort.map { |k| "`#{k}:`" }.join(", ")
|
|
237
|
+
noun = kw_keys.size == 1 ? "keyword" : "keywords"
|
|
238
|
+
"#{prefix} #{noun} #{listed}#{suffix}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def positional_param_types(method_def)
|
|
242
|
+
func = method_def.method_types.first.type
|
|
243
|
+
return nil unless func.respond_to?(:required_positionals)
|
|
244
|
+
|
|
245
|
+
(func.required_positionals + func.optional_positionals).map { |param| translate(param.type) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Resolves the (possibly namespace-relative) `interface_name` against
|
|
249
|
+
# the declaring class's namespace chain, mirroring Ruby constant
|
|
250
|
+
# lookup: `conforms-to _Foo` inside `Bar::Baz` tries `Bar::Baz::_Foo`,
|
|
251
|
+
# `Bar::_Foo`, then top-level `_Foo` (longest prefix first). Returns
|
|
252
|
+
# the first interface definition that resolves, or nil. (A leading
|
|
253
|
+
# `::` is already stripped by the parser, so an intended-absolute name
|
|
254
|
+
# still resolves via the trailing top-level candidate.)
|
|
255
|
+
def resolve_interface(rbs_loader, class_name, interface_name)
|
|
256
|
+
candidate_interface_names(class_name, interface_name).each do |candidate|
|
|
257
|
+
defn = rbs_loader.interface_definition(candidate)
|
|
258
|
+
return defn if defn
|
|
259
|
+
end
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def candidate_interface_names(class_name, interface_name)
|
|
264
|
+
prefixes = namespace_prefixes(class_name)
|
|
265
|
+
prefixes.map { |prefix| "#{prefix}::#{interface_name}" } + [interface_name]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Namespace prefixes of a qualified name, longest first:
|
|
269
|
+
# `"Bar::Baz"` → `["Bar::Baz", "Bar"]`. Covers both the
|
|
270
|
+
# `class Bar::Baz` and the `module Bar; class Baz` nesting shapes
|
|
271
|
+
# (a superset of `Module.nesting`, which the directive's resolved
|
|
272
|
+
# class name does not record).
|
|
273
|
+
def namespace_prefixes(class_name)
|
|
274
|
+
parts = normalize(class_name).split("::")
|
|
275
|
+
(1..parts.size).to_a.reverse.map { |count| parts.first(count).join("::") }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def translate(rbs_type)
|
|
279
|
+
Inference::RbsTypeTranslator.translate(rbs_type, self_type: nil, instance_type: nil, type_vars: {})
|
|
280
|
+
rescue StandardError
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def dynamic_top?(type)
|
|
285
|
+
type.is_a?(Type::Dynamic) || (type.respond_to?(:top?) && type.top?.yes?)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def normalize(class_name)
|
|
289
|
+
class_name.to_s.sub(/\A::/, "")
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|