rigortype 0.1.19 → 0.2.0
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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +49 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/constant_path"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# ADR-48 Struct follow-up, slice 3 — the fold-safe-local scan. Determines,
|
|
10
|
+
# for a single local-variable scope (a method body or the program
|
|
11
|
+
# top-level), which `Struct`-materialised locals are **provably never
|
|
12
|
+
# mutated, aliased, or escaped** and so may have their member reads folded
|
|
13
|
+
# off a *stored* binding (relaxing the slice-2 fresh-receiver gate).
|
|
14
|
+
#
|
|
15
|
+
# The analysis is a conservative ALLOW-LIST, not a deny-list: a local is
|
|
16
|
+
# fold-safe only when *every* read of it is the receiver of a known-pure
|
|
17
|
+
# read call. Anything the scan does not recognise as a pure read — a
|
|
18
|
+
# setter, an `[]=` / operator-write, an argument / alias / container store
|
|
19
|
+
# / return (escape), or an unknown method call (which could mutate `self`
|
|
20
|
+
# internally) — disqualifies the local. A missed case therefore makes the
|
|
21
|
+
# scan over-conservative (no fold), **never unsound** (folding a mutated
|
|
22
|
+
# value) — the false-positive-safe direction.
|
|
23
|
+
#
|
|
24
|
+
# Soundness rests on a counting identity: a local `n` is fold-safe iff
|
|
25
|
+
# *every* `LocalVariableReadNode(n)` is the receiver of a pure-read call.
|
|
26
|
+
# Equivalently `total_reads(n) == pure_receiver_reads(n)`. Any other
|
|
27
|
+
# occurrence — `n` as a setter receiver (`n.x = v` is a `:x=` call, not a
|
|
28
|
+
# pure read), an `[]=`/operator-write receiver (the receiver read is not
|
|
29
|
+
# under a pure call), a call argument, an assignment RHS (alias), a
|
|
30
|
+
# container element, a bare value (return/escape) — leaves a read that is
|
|
31
|
+
# not a pure-receiver read, so the counts diverge.
|
|
32
|
+
#
|
|
33
|
+
# See docs/notes/20260615-struct-folding-slice3-design.md and
|
|
34
|
+
# docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
|
|
35
|
+
module StructFoldSafety
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
EMPTY = Set.new.freeze
|
|
39
|
+
|
|
40
|
+
# The fixed `Struct` read methods that never mutate. A member-reader name
|
|
41
|
+
# (`:x`) is added per-local from the local's recorded layout. A setter
|
|
42
|
+
# (`:x=`), `:[]=`, `store`, `push`, etc. are deliberately absent.
|
|
43
|
+
FIXED_READS = %i[
|
|
44
|
+
[] dig to_h to_hash to_a values members deconstruct deconstruct_keys
|
|
45
|
+
== != eql? equal? hash inspect to_s size length frozen? each each_pair
|
|
46
|
+
values_at with
|
|
47
|
+
].to_set.freeze
|
|
48
|
+
|
|
49
|
+
# Nested `def` / `class` / `module` bodies open a *new* local-variable
|
|
50
|
+
# scope, so the scan does not descend into them — a local of the same
|
|
51
|
+
# name there is a different binding. Blocks share the enclosing locals
|
|
52
|
+
# (closures), so the scan does descend into them.
|
|
53
|
+
def scope_boundary?(node)
|
|
54
|
+
node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) ||
|
|
55
|
+
node.is_a?(Prism::ModuleNode) || node.is_a?(Prism::SingletonClassNode)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param root [Prism::Node, nil] the local-variable scope to scan.
|
|
59
|
+
# @param layout_lookup [#call] a `String -> Array[Symbol] | nil` resolver
|
|
60
|
+
# mapping a constant receiver name to its struct member list.
|
|
61
|
+
# @return [Set<Symbol>] the fold-safe local names.
|
|
62
|
+
def fold_safe_locals(root, layout_lookup)
|
|
63
|
+
return EMPTY if root.nil?
|
|
64
|
+
|
|
65
|
+
members = {}
|
|
66
|
+
writes = Hash.new(0)
|
|
67
|
+
collect_struct_locals(root, layout_lookup, members, writes)
|
|
68
|
+
return EMPTY if members.empty?
|
|
69
|
+
|
|
70
|
+
total = Hash.new(0)
|
|
71
|
+
pure = Hash.new(0)
|
|
72
|
+
count_uses(root, members, total, pure)
|
|
73
|
+
|
|
74
|
+
safe = members.each_key.select do |name|
|
|
75
|
+
writes[name] == 1 && total[name].positive? && total[name] == pure[name]
|
|
76
|
+
end
|
|
77
|
+
safe.empty? ? EMPTY : safe.to_set
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Pass 1 — record each local's single struct materialisation (its member
|
|
81
|
+
# set) and count its assignments. A local assigned more than once is
|
|
82
|
+
# later excluded (the static fold-safe set cannot track a rebinding).
|
|
83
|
+
def collect_struct_locals(node, layout_lookup, members, writes)
|
|
84
|
+
return if node.nil?
|
|
85
|
+
|
|
86
|
+
if node.is_a?(Prism::LocalVariableWriteNode)
|
|
87
|
+
writes[node.name] += 1
|
|
88
|
+
found = struct_materialization_members(node.value, layout_lookup)
|
|
89
|
+
members[node.name] = found if found
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
each_local_scope_child(node) do |child|
|
|
93
|
+
collect_struct_locals(child, layout_lookup, members, writes)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Pass 2 — count, per recorded struct local, total reads vs. reads that
|
|
98
|
+
# are the receiver of a pure-read call.
|
|
99
|
+
def count_uses(node, members, total, pure)
|
|
100
|
+
return if node.nil?
|
|
101
|
+
|
|
102
|
+
total[node.name] += 1 if node.is_a?(Prism::LocalVariableReadNode) && members.key?(node.name)
|
|
103
|
+
|
|
104
|
+
if node.is_a?(Prism::CallNode)
|
|
105
|
+
receiver = node.receiver
|
|
106
|
+
if receiver.is_a?(Prism::LocalVariableReadNode) && members.key?(receiver.name) &&
|
|
107
|
+
pure_read_call?(node, members[receiver.name])
|
|
108
|
+
pure[receiver.name] += 1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
each_local_scope_child(node) do |child|
|
|
113
|
+
count_uses(child, members, total, pure)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A call is a pure read of the receiver when its name is a fixed Struct
|
|
118
|
+
# read or one of the receiver's member readers. Setters (`:x=`), `:[]=`,
|
|
119
|
+
# and any unknown method are excluded.
|
|
120
|
+
def pure_read_call?(call_node, member_set)
|
|
121
|
+
name = call_node.name
|
|
122
|
+
FIXED_READS.include?(name) || member_set.include?(name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The member set of a `<Struct chain>.new(...)` / `.[]` materialisation,
|
|
126
|
+
# or nil. Handles the inline `Struct.new(:a, :b).new(...)` form and the
|
|
127
|
+
# `Const.new(...)` form (resolved through the layout side-table).
|
|
128
|
+
def struct_materialization_members(value_node, layout_lookup)
|
|
129
|
+
return nil unless value_node.is_a?(Prism::CallNode)
|
|
130
|
+
return nil unless %i[new []].include?(value_node.name)
|
|
131
|
+
|
|
132
|
+
receiver = value_node.receiver
|
|
133
|
+
case receiver
|
|
134
|
+
when Prism::CallNode
|
|
135
|
+
struct_new_member_set(receiver)
|
|
136
|
+
|
|
137
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
138
|
+
name = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
139
|
+
name && member_set_of(layout_lookup.call(name))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The Symbol member set of a literal `Struct.new(:a, :b [, keyword_init:])`
|
|
144
|
+
# call, or nil. (A leading String name and the trailing options hash are
|
|
145
|
+
# ignored — only the literal-Symbol positionals contribute.)
|
|
146
|
+
def struct_new_member_set(call_node)
|
|
147
|
+
return nil unless call_node.is_a?(Prism::CallNode) && call_node.name == :new
|
|
148
|
+
return nil unless meta_constant?(call_node.receiver, :Struct)
|
|
149
|
+
|
|
150
|
+
args = call_node.arguments&.arguments || []
|
|
151
|
+
positional = args.last.is_a?(Prism::KeywordHashNode) ? args[0..-2] : args
|
|
152
|
+
positional = positional[1..] if positional.first.is_a?(Prism::StringNode)
|
|
153
|
+
return nil if positional.nil? || positional.empty?
|
|
154
|
+
return nil unless positional.all?(Prism::SymbolNode)
|
|
155
|
+
|
|
156
|
+
positional.to_set { |sym| sym.unescaped.to_sym }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def member_set_of(members)
|
|
160
|
+
members && !members.empty? ? members.to_set : nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def meta_constant?(node, name)
|
|
164
|
+
case node
|
|
165
|
+
when Prism::ConstantReadNode then node.name == name
|
|
166
|
+
when Prism::ConstantPathNode then node.parent.nil? && node.name == name
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Yields each child to recurse into, skipping the subtree of a nested
|
|
171
|
+
# local-variable-scope boundary (a `def` / `class` / `module`).
|
|
172
|
+
def each_local_scope_child(node)
|
|
173
|
+
node.compact_child_nodes.each do |child|
|
|
174
|
+
next if scope_boundary?(child)
|
|
175
|
+
|
|
176
|
+
yield child
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -8,13 +8,13 @@ module Rigor
|
|
|
8
8
|
# the template name. Stored in {SyntheticMethodIndex} and
|
|
9
9
|
# consulted by {MethodDispatcher} below the RBS dispatch tier.
|
|
10
10
|
#
|
|
11
|
-
# Per ADR-16 § WD13 (cost-bounded best-effort):
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# string
|
|
16
|
-
#
|
|
17
|
-
#
|
|
11
|
+
# Per ADR-16 § WD13 (cost-bounded best-effort): method names
|
|
12
|
+
# emit with return types promoted by the dispatcher's Slice 6
|
|
13
|
+
# tiers — Slice 6a promotes via `origin_module` (TierB) and
|
|
14
|
+
# Slice 6b via `return_type` nominal lookup (TierC). The
|
|
15
|
+
# `return_type` string is preserved so promotion can resolve
|
|
16
|
+
# it without re-walking; `"untyped"` / `"void"` placeholders
|
|
17
|
+
# fall back to `Dynamic[Top]`.
|
|
18
18
|
#
|
|
19
19
|
# The `provenance` Hash carries debug / `--explain` metadata:
|
|
20
20
|
# plugin id, the template's call shape, and the source
|
|
@@ -21,20 +21,14 @@ require_relative "../type/hash_shape"
|
|
|
21
21
|
|
|
22
22
|
module Rigor
|
|
23
23
|
module LanguageServer
|
|
24
|
-
# Answers `textDocument/completion` requests.
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# Constant-path completion (slice 6), Union / Intersection /
|
|
31
|
-
# Refined / Shape receiver handling (slice 7), and parse-recovery
|
|
32
|
-
# fallback for malformed buffers (slice 8) extend this v1 floor.
|
|
24
|
+
# Answers `textDocument/completion` requests. Provides method
|
|
25
|
+
# completion for `obj.|` (known-type receiver → RBS-known methods),
|
|
26
|
+
# constant-path completion, hash-key completion, Union / Intersection /
|
|
27
|
+
# Refined / Shape receiver handling, and parse-recovery fallback for
|
|
28
|
+
# malformed buffers (sentinel injection).
|
|
33
29
|
#
|
|
34
30
|
# LSP `CompletionItemKind` values used:
|
|
35
|
-
# - 2 = Method
|
|
36
|
-
#
|
|
37
|
-
# Slice 6 will add 7 (Class), 9 (Module), 21 (Constant).
|
|
31
|
+
# - 2 = Method, 5 = Field, 7 = Class, 9 = Module, 21 = Constant.
|
|
38
32
|
class CompletionProvider # rubocop:disable Metrics/ClassLength
|
|
39
33
|
include BufferResolution
|
|
40
34
|
|
|
@@ -15,10 +15,10 @@ module Rigor
|
|
|
15
15
|
# a `BufferBinding` from the BufferTable entry, runs the Runner,
|
|
16
16
|
# and pushes the resulting LSP `Diagnostic[]` through the writer.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# Debouncing is wired via an optional `Debouncer` injected at
|
|
19
|
+
# construction (delay defaults to 200ms quiet-time); without a
|
|
20
|
+
# debouncer each call blocks synchronously (primarily for specs).
|
|
21
|
+
# Ractor-pool dispatch is queued.
|
|
22
22
|
class DiagnosticPublisher
|
|
23
23
|
# Maps Rigor severity symbols to LSP DiagnosticSeverity
|
|
24
24
|
# integers per spec § "Diagnostic":
|
|
@@ -128,9 +128,9 @@ module Rigor
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
# LSP `Range` is 0-based start + end with `character` in
|
|
131
|
-
# UTF-16 code units.
|
|
132
|
-
# ASCII source); UTF-16 conversion stays queued
|
|
133
|
-
# doc § "Open questions".
|
|
131
|
+
# UTF-16 code units. This implementation emits byte columns
|
|
132
|
+
# (correct for ASCII source); UTF-16 conversion stays queued
|
|
133
|
+
# per design doc § "Open questions".
|
|
134
134
|
def range_from(location)
|
|
135
135
|
{
|
|
136
136
|
start: { line: location.start_line - 1, character: location.start_column },
|
|
@@ -64,9 +64,8 @@ module Rigor
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def base_scope(_path)
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# every cursor stop.
|
|
67
|
+
# Pulls the Environment from the cached ProjectContext so
|
|
68
|
+
# each hover avoids repeating the RBS-load build.
|
|
70
69
|
Scope.empty(environment: @project_context.environment)
|
|
71
70
|
end
|
|
72
71
|
end
|
|
@@ -11,17 +11,8 @@ module Rigor
|
|
|
11
11
|
module LanguageServer
|
|
12
12
|
# Builds the LSP `Hover.contents` markdown body. Dispatches on
|
|
13
13
|
# the hovered Prism node class so each shape (method call,
|
|
14
|
-
# constant, local,
|
|
15
|
-
# type-aware presentation
|
|
16
|
-
#
|
|
17
|
-
# Slice A1 (this commit) ships:
|
|
18
|
-
# - default body bit-for-bit matching the LSP v1 slice 5
|
|
19
|
-
# output (`type:` / `erased:` / `node:`),
|
|
20
|
-
# - `Prism::CallNode` specialisation surfacing the receiver
|
|
21
|
-
# type + RBS-erased method signature.
|
|
22
|
-
#
|
|
23
|
-
# Slices A2-A4 extend the dispatch with constant /
|
|
24
|
-
# local / ivar / literal renderers per
|
|
14
|
+
# constant, local, ivar, literal) gets the most relevant
|
|
15
|
+
# type-aware presentation per
|
|
25
16
|
# `docs/design/20260517-lsp-hover-completion.md`.
|
|
26
17
|
class HoverRenderer
|
|
27
18
|
# @param node_scope_lookup [#[]] node-to-scope table built
|
|
@@ -6,21 +6,13 @@ require_relative "buffer_table"
|
|
|
6
6
|
module Rigor
|
|
7
7
|
module LanguageServer
|
|
8
8
|
# LSP server lifecycle state machine + JSON-RPC method dispatcher.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# -
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# notifications). Out-of-state requests return the
|
|
17
|
-
# spec-defined `InvalidRequest` (-32002) / `MethodNotFound`
|
|
18
|
-
# (-32601) error shapes.
|
|
19
|
-
#
|
|
20
|
-
# Slice 2 wraps this dispatcher in a stdio JSON-RPC reader /
|
|
21
|
-
# writer so the CLI subcommand can serve real LSP clients.
|
|
22
|
-
# Slice 3+ adds document sync; slice 4+ adds publishDiagnostics;
|
|
23
|
-
# slice 5-8 add the rest of the v1 capability surface.
|
|
9
|
+
# State machine: `:uninitialized` → `:initialized` → `:shutdown`
|
|
10
|
+
# → `:exited`. {#dispatch} routes (method, params) to the matching
|
|
11
|
+
# handler and returns the response payload (`nil` for notifications);
|
|
12
|
+
# out-of-state requests return `InvalidRequest` (-32002) /
|
|
13
|
+
# `MethodNotFound` (-32601). Full v1 capability surface (document
|
|
14
|
+
# sync, publishDiagnostics, hover, completion, sig-help, folding,
|
|
15
|
+
# selection, and watched-file invalidation) is implemented.
|
|
24
16
|
class Server # rubocop:disable Metrics/ClassLength
|
|
25
17
|
# JSON-RPC error codes per LSP spec § "Response Message".
|
|
26
18
|
ERROR_PARSE_ERROR = -32_700
|
|
@@ -58,8 +50,8 @@ module Rigor
|
|
|
58
50
|
# the providers read on every request. When present,
|
|
59
51
|
# `workspace/didChangeWatchedFiles` and
|
|
60
52
|
# `workspace/didChangeConfiguration` invalidate the cache;
|
|
61
|
-
# nil means "no project context"
|
|
62
|
-
#
|
|
53
|
+
# nil means "no project context": each request rebuilds env
|
|
54
|
+
# from scratch (mainly for specs and backward compatibility).
|
|
63
55
|
def initialize(buffer_table: BufferTable.new, publisher: nil, # rubocop:disable Metrics/ParameterLists
|
|
64
56
|
hover_provider: nil, document_symbol_provider: nil,
|
|
65
57
|
completion_provider: nil, signature_help_provider: nil,
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
module Rigor
|
|
4
4
|
# The Language Server subsystem. See
|
|
5
5
|
# `docs/design/20260517-language-server.md` for the design.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# surface.
|
|
6
|
+
# The full v1 capability surface (document sync, publishDiagnostics,
|
|
7
|
+
# hover, completion, sig-help, folding, selection, and watched-file
|
|
8
|
+
# invalidation) is implemented. This module is the namespace and
|
|
9
|
+
# require entry point for the subsystem.
|
|
11
10
|
module LanguageServer
|
|
12
11
|
end
|
|
13
12
|
end
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -18,10 +18,11 @@ module Rigor
|
|
|
18
18
|
# overrides {#init} to wire up any state it needs from the
|
|
19
19
|
# injected service container.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# contributions
|
|
24
|
-
#
|
|
21
|
+
# This class implements all plugin protocol hooks: per-call
|
|
22
|
+
# return-type contributions (`dynamic_return`), narrowing-fact
|
|
23
|
+
# contributions (`type_specifier`), AST node rules (`node_rule`),
|
|
24
|
+
# and producer/cache hooks. Cumulative implementation per the
|
|
25
|
+
# ADR-37 / ADR-52 slice chain.
|
|
25
26
|
#
|
|
26
27
|
# Example plugin:
|
|
27
28
|
#
|
|
@@ -543,10 +544,11 @@ module Rigor
|
|
|
543
544
|
#
|
|
544
545
|
# `path` is the analysed file path; `scope` is the entry
|
|
545
546
|
# `Rigor::Scope` after `ScopeIndexer` ran; `root` is the
|
|
546
|
-
# parsed `Prism::Node` root. Plugin authors traverse
|
|
547
|
-
# themselves
|
|
548
|
-
#
|
|
549
|
-
#
|
|
547
|
+
# parsed `Prism::Node` root. Plugin authors can traverse
|
|
548
|
+
# `root` themselves or declare rules via the `.node_rule` DSL
|
|
549
|
+
# (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base
|
|
550
|
+
# class mentioned in ADR-2 was superseded by the block-based
|
|
551
|
+
# `.node_rule` DSL.
|
|
550
552
|
#
|
|
551
553
|
# Default returns `[]` so plugins that contribute through
|
|
552
554
|
# other channels (e.g. slice-4 narrowing contributions,
|
|
@@ -29,10 +29,9 @@ module Rigor
|
|
|
29
29
|
# class-level methods whose block argument runs as if it were
|
|
30
30
|
# an instance method of the receiver.
|
|
31
31
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `Scope#self_type` for
|
|
35
|
-
# arrives in slice 1b.
|
|
32
|
+
# Engine wiring: `Inference::MacroBlockSelfType.narrow_self_type_for`
|
|
33
|
+
# (called from expression_typer.rb) consults registered entries
|
|
34
|
+
# and narrows `Scope#self_type` for matching block call sites.
|
|
36
35
|
#
|
|
37
36
|
# ## Fields
|
|
38
37
|
#
|
|
@@ -68,13 +68,10 @@ module Rigor
|
|
|
68
68
|
# to a later slice — the `returns:` declarations cost
|
|
69
69
|
# nothing to write today and unlock precision then.
|
|
70
70
|
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# `SyntheticMethodIndex` the dispatcher consults; slice 2c
|
|
76
|
-
# authors `plugins/rigor-dry-struct/` and
|
|
77
|
-
# `plugins/rigor-dry-types/` as the worked consumers.
|
|
71
|
+
# Engine wiring: `Inference::SyntheticMethodScanner` (slice 2b,
|
|
72
|
+
# `synthetic_method_scanner.rb`) consumes `manifest.heredoc_templates`.
|
|
73
|
+
# Worked consumers: `plugins/rigor-dry-struct/` and
|
|
74
|
+
# `plugins/rigor-dry-types/` (slice 2c).
|
|
78
75
|
class HeredocTemplate
|
|
79
76
|
NAME_PLACEHOLDER = "\#{name}"
|
|
80
77
|
|
|
@@ -78,12 +78,9 @@ module Rigor
|
|
|
78
78
|
# facts (attr_reader / after_save / etc.); slice 3 emits
|
|
79
79
|
# only the module's plain instance methods.
|
|
80
80
|
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# scanner that walks Tier B call sites + the per-method
|
|
85
|
-
# explosion via `SyntheticMethodIndex`; slice 3c authors
|
|
86
|
-
# `plugins/rigor-devise/` model side as the worked consumer.
|
|
81
|
+
# Engine wiring: `Inference::SyntheticMethodScanner#collect_trait_registries`
|
|
82
|
+
# (slice 3b) walks Tier B call sites and explodes per-method
|
|
83
|
+
# entries. Worked consumer: `plugins/rigor-devise/` (slice 3c).
|
|
87
84
|
class TraitRegistry
|
|
88
85
|
REST_POSITION = :rest
|
|
89
86
|
|
data/lib/rigor/plugin/macro.rb
CHANGED
|
@@ -14,11 +14,10 @@ module Rigor
|
|
|
14
14
|
# `nested_class_templates:`) and the substrate consumes them
|
|
15
15
|
# to recognise the call shapes a library exposes to its users.
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# add files alongside `block_as_method.rb` without churn.
|
|
17
|
+
# Tier A (`BlockAsMethod`), Tier B (`TraitRegistry`), Tier C
|
|
18
|
+
# (`HeredocTemplate`), and ADR-36 nested-class emission
|
|
19
|
+
# (`NestedClassTemplate`) value classes are all shipped here.
|
|
20
|
+
# Engine wiring lives in `lib/rigor/inference/`.
|
|
22
21
|
#
|
|
23
22
|
# Per ADR-16 § WD13, substrate-produced output ships at a
|
|
24
23
|
# **floor** in v0.1.x ("substrate-affected code parses cleanly
|