rigortype 0.1.4 → 0.1.6
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 +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +11 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +37 -2
- metadata +92 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "project_patched_methods"
|
|
6
|
+
require_relative "../analysis/dependency_source_inference/return_type_heuristic"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# ADR-17 slice 2 — pre-pass scanner. Walks every file the user
|
|
11
|
+
# listed under `pre_eval:` and harvests every `def` /
|
|
12
|
+
# `def self.` declaration inside a class / module body into a
|
|
13
|
+
# {ProjectPatchedMethods} registry the dispatcher consults
|
|
14
|
+
# below the plugin tier.
|
|
15
|
+
#
|
|
16
|
+
# The walker is intentionally a strict subset of
|
|
17
|
+
# {Rigor::Inference::ScopeIndexer}'s machinery: it only needs
|
|
18
|
+
# `class C; def m; end; end` shape recognition, not full
|
|
19
|
+
# inference. Parse errors degrade to a fail-soft `:warning`
|
|
20
|
+
# `pre-eval.parse-error` diagnostic accumulated alongside
|
|
21
|
+
# the registry; per ADR-17 § "Failure modes" a parse failure
|
|
22
|
+
# in a pre-eval file MUST NOT abort the rest of the run.
|
|
23
|
+
module ProjectPatchedScanner
|
|
24
|
+
# Frozen scan outcome carrying the populated registry and
|
|
25
|
+
# the per-file warnings the runner emits at run start.
|
|
26
|
+
class Result < Data.define(:registry, :diagnostics)
|
|
27
|
+
def initialize(registry:, diagnostics: [])
|
|
28
|
+
super(
|
|
29
|
+
registry: registry,
|
|
30
|
+
diagnostics: diagnostics.freeze
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# @param paths [Array<String>] absolute paths to the
|
|
38
|
+
# pre-eval files. The runner has already validated that
|
|
39
|
+
# each path exists (slice-1 `pre-eval.file-not-found`
|
|
40
|
+
# `:error` covers missing entries); the scanner does NOT
|
|
41
|
+
# re-check existence.
|
|
42
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
43
|
+
# editor-mode buffer binding. When set, the scanner reads
|
|
44
|
+
# the buffer's physical bytes if a pre-eval entry matches
|
|
45
|
+
# the logical path, so users editing a monkey-patch file
|
|
46
|
+
# see the in-flight version in their analysis.
|
|
47
|
+
# @return [Result] the populated registry plus any
|
|
48
|
+
# per-file warnings.
|
|
49
|
+
def scan(paths, buffer: nil)
|
|
50
|
+
entries = []
|
|
51
|
+
diagnostics = []
|
|
52
|
+
paths.each { |path| scan_file(path, entries, diagnostics, buffer) }
|
|
53
|
+
diagnostics.concat(duplicate_declaration_diagnostics(entries))
|
|
54
|
+
Result.new(
|
|
55
|
+
registry: ProjectPatchedMethods.new(entries: entries),
|
|
56
|
+
diagnostics: diagnostics
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ADR-17 § "Failure modes" — when two pre-eval entries
|
|
61
|
+
# declare the same `(class_name, method_name, kind)` triple,
|
|
62
|
+
# emit one `:info` `pre-eval.duplicate-declaration`
|
|
63
|
+
# diagnostic per collision. The registry's first-write-wins
|
|
64
|
+
# behaviour is unchanged; the diagnostic just makes the
|
|
65
|
+
# shadowing visible so users notice when a later patch
|
|
66
|
+
# is silently masked.
|
|
67
|
+
def duplicate_declaration_diagnostics(entries)
|
|
68
|
+
seen = {}
|
|
69
|
+
entries.each_with_object([]) do |entry, acc|
|
|
70
|
+
key = [entry.class_name, entry.method_name, entry.kind]
|
|
71
|
+
if (first = seen[key])
|
|
72
|
+
acc << build_diagnostic(
|
|
73
|
+
path: entry.source_path,
|
|
74
|
+
line: entry.source_line,
|
|
75
|
+
column: 1,
|
|
76
|
+
severity: :info,
|
|
77
|
+
rule: "pre-eval.duplicate-declaration",
|
|
78
|
+
message: "pre-eval duplicate declaration: " \
|
|
79
|
+
"#{entry.class_name}##{entry.method_name} " \
|
|
80
|
+
"(#{entry.kind}) is already declared at " \
|
|
81
|
+
"#{first.source_path}:#{first.source_line}. " \
|
|
82
|
+
"The first declaration wins; this entry is shadowed."
|
|
83
|
+
)
|
|
84
|
+
else
|
|
85
|
+
seen[key] = entry
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
private_class_method :duplicate_declaration_diagnostics
|
|
90
|
+
|
|
91
|
+
def scan_file(path, entries, diagnostics, buffer = nil)
|
|
92
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
93
|
+
parse_result =
|
|
94
|
+
if physical == path
|
|
95
|
+
Prism.parse_file(path)
|
|
96
|
+
else
|
|
97
|
+
Prism.parse(File.read(physical), filepath: path)
|
|
98
|
+
end
|
|
99
|
+
unless parse_result.errors.empty?
|
|
100
|
+
diagnostics << parse_error_diagnostic(path, parse_result.errors)
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
walk_node(parse_result.value, [], false, path, entries)
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
diagnostics << build_diagnostic(
|
|
107
|
+
path: path, line: 1, column: 1,
|
|
108
|
+
severity: :warning,
|
|
109
|
+
rule: "pre-eval.parse-error",
|
|
110
|
+
message: "rigor: failed to read pre_eval entry #{path.inspect}: " \
|
|
111
|
+
"#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
|
|
112
|
+
"the rest of the run proceeds."
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
private_class_method :scan_file
|
|
116
|
+
|
|
117
|
+
def parse_error_diagnostic(path, errors)
|
|
118
|
+
first = errors.first
|
|
119
|
+
line = first.respond_to?(:location) ? first.location&.start_line || 1 : 1
|
|
120
|
+
build_diagnostic(
|
|
121
|
+
path: path, line: line, column: 1,
|
|
122
|
+
severity: :warning,
|
|
123
|
+
rule: "pre-eval.parse-error",
|
|
124
|
+
message: "rigor: pre_eval entry #{path.inspect} has a parse error " \
|
|
125
|
+
"(#{first&.message}). Pre-evaluation skipped for this file; " \
|
|
126
|
+
"the rest of the run proceeds."
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
private_class_method :parse_error_diagnostic
|
|
130
|
+
|
|
131
|
+
# Builds a diagnostic Hash-shape the runner translates to a
|
|
132
|
+
# `Rigor::Analysis::Diagnostic`. The scanner intentionally
|
|
133
|
+
# does NOT depend on the analysis layer (it's a pre-pass);
|
|
134
|
+
# the runner adapts at the call site.
|
|
135
|
+
def build_diagnostic(path:, line:, column:, severity:, rule:, message:)
|
|
136
|
+
{ path: path, line: line, column: column, severity: severity, rule: rule, message: message }
|
|
137
|
+
end
|
|
138
|
+
private_class_method :build_diagnostic
|
|
139
|
+
|
|
140
|
+
def walk_node(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
141
|
+
return unless node.is_a?(Prism::Node)
|
|
142
|
+
|
|
143
|
+
case node
|
|
144
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
145
|
+
descend_class_or_module(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
146
|
+
when Prism::SingletonClassNode
|
|
147
|
+
descend_singleton_class(node, qualified_prefix, source_path, entries)
|
|
148
|
+
when Prism::DefNode
|
|
149
|
+
record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
150
|
+
else
|
|
151
|
+
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
private_class_method :walk_node
|
|
155
|
+
|
|
156
|
+
def walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
157
|
+
node.compact_child_nodes.each do |child|
|
|
158
|
+
walk_node(child, qualified_prefix, in_singleton_class, source_path, entries)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
private_class_method :walk_children
|
|
162
|
+
|
|
163
|
+
def descend_class_or_module(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
164
|
+
name = qualified_name_for(node.constant_path)
|
|
165
|
+
if name && node.body
|
|
166
|
+
walk_node(node.body, qualified_prefix + [name], in_singleton_class, source_path, entries)
|
|
167
|
+
else
|
|
168
|
+
walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
private_class_method :descend_class_or_module
|
|
172
|
+
|
|
173
|
+
def descend_singleton_class(node, qualified_prefix, source_path, entries)
|
|
174
|
+
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
175
|
+
walk_node(node.body, qualified_prefix, true, source_path, entries)
|
|
176
|
+
else
|
|
177
|
+
walk_children(node, qualified_prefix, false, source_path, entries)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
private_class_method :descend_singleton_class
|
|
181
|
+
|
|
182
|
+
def record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
|
|
183
|
+
return if qualified_prefix.empty?
|
|
184
|
+
|
|
185
|
+
class_name = qualified_prefix.join("::")
|
|
186
|
+
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
187
|
+
line = node.location&.start_line || 1
|
|
188
|
+
return_type = Analysis::DependencySourceInference::ReturnTypeHeuristic.extract(node)
|
|
189
|
+
entries << ProjectPatchedMethods::Entry.new(
|
|
190
|
+
class_name: class_name, method_name: node.name, kind: kind,
|
|
191
|
+
source_path: source_path, source_line: line,
|
|
192
|
+
return_type: return_type
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
private_class_method :record_def_node
|
|
196
|
+
|
|
197
|
+
def qualified_name_for(node)
|
|
198
|
+
case node
|
|
199
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
200
|
+
when Prism::ConstantPathNode
|
|
201
|
+
parent = node.parent.nil? ? nil : qualified_name_for(node.parent)
|
|
202
|
+
return nil if !node.parent.nil? && parent.nil?
|
|
203
|
+
|
|
204
|
+
parent.nil? ? node.name.to_s : "#{parent}::#{node.name}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
private_class_method :qualified_name_for
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -177,8 +177,10 @@ module Rigor
|
|
|
177
177
|
return if def_node.body.nil? || qualified_prefix.empty?
|
|
178
178
|
|
|
179
179
|
class_name = qualified_prefix.join("::")
|
|
180
|
+
singleton = def_node.receiver.is_a?(Prism::SelfNode) ||
|
|
181
|
+
def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
|
|
180
182
|
self_type =
|
|
181
|
-
if
|
|
183
|
+
if singleton
|
|
182
184
|
Type::Combinator.singleton_of(class_name)
|
|
183
185
|
else
|
|
184
186
|
Type::Combinator.nominal_of(class_name)
|
|
@@ -371,9 +373,12 @@ module Rigor
|
|
|
371
373
|
return
|
|
372
374
|
end
|
|
373
375
|
when Prism::SingletonClassNode
|
|
374
|
-
if node.
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
if node.body
|
|
377
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
378
|
+
if singleton_prefix
|
|
379
|
+
walk_methods(node.body, singleton_prefix, true, accumulator)
|
|
380
|
+
return
|
|
381
|
+
end
|
|
377
382
|
end
|
|
378
383
|
when Prism::ConstantWriteNode
|
|
379
384
|
if meta_new_block_body(node)
|
|
@@ -395,6 +400,30 @@ module Rigor
|
|
|
395
400
|
walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
|
|
396
401
|
end
|
|
397
402
|
end
|
|
403
|
+
|
|
404
|
+
# Resolves a `class << X` body's qualified prefix.
|
|
405
|
+
# - `class << self` keeps `qualified_prefix` (the enclosing class).
|
|
406
|
+
# - `class << Foo` inside `class Foo` collapses to the same prefix
|
|
407
|
+
# (semantically `class << self`).
|
|
408
|
+
# - `class << Foo` not nested in `class Foo` returns `[Foo]`
|
|
409
|
+
# so methods defined inside register on Foo's singleton.
|
|
410
|
+
# - Any other expression (variable, method call) returns nil
|
|
411
|
+
# so the walker falls through and skips the body.
|
|
412
|
+
def singleton_class_prefix(node, qualified_prefix)
|
|
413
|
+
case node.expression
|
|
414
|
+
when Prism::SelfNode
|
|
415
|
+
qualified_prefix
|
|
416
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
417
|
+
rendered = qualified_name_for(node.expression)
|
|
418
|
+
return nil unless rendered
|
|
419
|
+
|
|
420
|
+
if !qualified_prefix.empty? && qualified_prefix.last == rendered
|
|
421
|
+
qualified_prefix
|
|
422
|
+
else
|
|
423
|
+
rendered.split("::")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
398
427
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
399
428
|
|
|
400
429
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
@@ -418,11 +447,50 @@ module Rigor
|
|
|
418
447
|
return if qualified_prefix.empty?
|
|
419
448
|
|
|
420
449
|
class_name = qualified_prefix.join("::")
|
|
421
|
-
|
|
450
|
+
singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
451
|
+
kind = singleton ? :singleton : :instance
|
|
422
452
|
accumulator[class_name] ||= {}
|
|
423
453
|
accumulator[class_name][def_node.name] = kind
|
|
424
454
|
end
|
|
425
455
|
|
|
456
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
457
|
+
# `module Meta`) is semantically equivalent to `def self.bar`:
|
|
458
|
+
# at the def-site, the runtime value of the constant `Foo` is
|
|
459
|
+
# the module itself (== `self`). Recognise the form so the
|
|
460
|
+
# method registers as singleton on the enclosing class.
|
|
461
|
+
#
|
|
462
|
+
# The cross-class form `def Bar.baz` inside `module Foo` —
|
|
463
|
+
# where the receiver names a constant other than the
|
|
464
|
+
# enclosing class — is not supported at this slice; falls
|
|
465
|
+
# through to `:instance` (current behaviour) rather than
|
|
466
|
+
# silently re-routing the registration.
|
|
467
|
+
def def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
468
|
+
return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
469
|
+
|
|
470
|
+
def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Only `Prism::ConstantReadNode` is observed in real Ruby —
|
|
474
|
+
# Prism mis-parses `def C::P.method` as `def C.P` (Ruby itself
|
|
475
|
+
# rejects the form as a SyntaxError). The ConstantPathNode
|
|
476
|
+
# branch stays defensive in case Prism's grammar widens.
|
|
477
|
+
def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
|
|
478
|
+
return false if qualified_prefix.empty?
|
|
479
|
+
|
|
480
|
+
case receiver
|
|
481
|
+
when Prism::ConstantReadNode
|
|
482
|
+
receiver.name.to_s == qualified_prefix.last
|
|
483
|
+
when Prism::ConstantPathNode
|
|
484
|
+
rendered = render_constant_path(receiver)
|
|
485
|
+
return false unless rendered
|
|
486
|
+
|
|
487
|
+
path = rendered.split("::")
|
|
488
|
+
qualified_prefix.last(path.length) == path
|
|
489
|
+
else
|
|
490
|
+
false
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
426
494
|
# v0.0.2 #5 — instance-side def-node recording. Walks
|
|
427
495
|
# class bodies the same way as `build_discovered_methods`
|
|
428
496
|
# but records the actual `Prism::DefNode` for each
|
|
@@ -452,9 +520,12 @@ module Rigor
|
|
|
452
520
|
return
|
|
453
521
|
end
|
|
454
522
|
when Prism::SingletonClassNode
|
|
455
|
-
if node.
|
|
456
|
-
|
|
457
|
-
|
|
523
|
+
if node.body
|
|
524
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
525
|
+
if singleton_prefix
|
|
526
|
+
walk_def_nodes(node.body, singleton_prefix, true, accumulator)
|
|
527
|
+
return
|
|
528
|
+
end
|
|
458
529
|
end
|
|
459
530
|
when Prism::ConstantWriteNode
|
|
460
531
|
if meta_new_block_body(node)
|
|
@@ -481,7 +552,7 @@ module Rigor
|
|
|
481
552
|
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
482
553
|
|
|
483
554
|
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
484
|
-
return if
|
|
555
|
+
return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
485
556
|
|
|
486
557
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
487
558
|
accumulator[class_name] ||= {}
|
|
@@ -530,9 +601,12 @@ module Rigor
|
|
|
530
601
|
return current_visibility
|
|
531
602
|
end
|
|
532
603
|
when Prism::SingletonClassNode
|
|
533
|
-
if node.
|
|
534
|
-
|
|
535
|
-
|
|
604
|
+
if node.body
|
|
605
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
606
|
+
if singleton_prefix
|
|
607
|
+
walk_method_visibilities(node.body, singleton_prefix, true, :public, accumulator)
|
|
608
|
+
return current_visibility
|
|
609
|
+
end
|
|
536
610
|
end
|
|
537
611
|
when Prism::ConstantWriteNode
|
|
538
612
|
if meta_new_block_body(node)
|
|
@@ -703,6 +777,76 @@ module Rigor
|
|
|
703
777
|
node.unescaped&.to_sym
|
|
704
778
|
end
|
|
705
779
|
|
|
780
|
+
# Walks every file in `paths` (each path is parsed once with
|
|
781
|
+
# `Prism.parse_file`) and returns the unioned project-wide
|
|
782
|
+
# `discovered_classes` Hash: `{qualified_name => Singleton[…]}`.
|
|
783
|
+
# Used by `Analysis::Runner` to seed each file's
|
|
784
|
+
# `default_scope.discovered_classes` so that lexical
|
|
785
|
+
# constant lookup in one file resolves a `class Foo`
|
|
786
|
+
# declared in a sibling file. Per-file collisions are
|
|
787
|
+
# last-write-wins (matches the existing in-file merge
|
|
788
|
+
# semantics). Parse failures fail-soft to an empty
|
|
789
|
+
# contribution. The `buffer` argument, when present,
|
|
790
|
+
# redirects reads for the bound logical path to the
|
|
791
|
+
# buffer's physical path so editor-mode pre-passes see
|
|
792
|
+
# the in-flight bytes.
|
|
793
|
+
#
|
|
794
|
+
# **Modules are intentionally excluded** from the
|
|
795
|
+
# project-wide seed: a `module M; module_function; def x; end; end`
|
|
796
|
+
# body, when surfaced as `singleton(M)` to the dispatcher,
|
|
797
|
+
# falls through to `Kernel#x` (or any Module ancestor
|
|
798
|
+
# method) when the project's per-file
|
|
799
|
+
# `discovered_methods` doesn't know `M.x` — leading to
|
|
800
|
+
# surprising types like `Kernel.select → Array[String]`.
|
|
801
|
+
# Until cross-file `discovered_methods` follows the same
|
|
802
|
+
# project-wide seed, registering modules here would
|
|
803
|
+
# introduce regressions in modules-with-module_function
|
|
804
|
+
# idioms that previously resolved to `Dynamic[Top]`.
|
|
805
|
+
# Class declarations are safe because per-file
|
|
806
|
+
# `discovered_methods` already tracks `def self.x` /
|
|
807
|
+
# `def x` instance and singleton methods consistently.
|
|
808
|
+
#
|
|
809
|
+
# @param paths [Array<String>] project file paths.
|
|
810
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
811
|
+
# @return [Hash{String => Rigor::Type::Singleton}]
|
|
812
|
+
def discovered_classes_for_paths(paths, buffer: nil)
|
|
813
|
+
accumulator = {}
|
|
814
|
+
paths.each do |path|
|
|
815
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
816
|
+
source = File.read(physical)
|
|
817
|
+
root = Prism.parse(source, filepath: path).value
|
|
818
|
+
collect_class_decls(root, [], accumulator)
|
|
819
|
+
rescue StandardError
|
|
820
|
+
# Skip files that fail to parse or read; the per-file
|
|
821
|
+
# analyzer surfaces the parse error separately.
|
|
822
|
+
next
|
|
823
|
+
end
|
|
824
|
+
accumulator.freeze
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Class-only variant of `record_declarations` — descends
|
|
828
|
+
# into nested module bodies (so `module Foo; class Bar`
|
|
829
|
+
# registers `Foo::Bar`) but never registers the module
|
|
830
|
+
# itself in `accumulator`.
|
|
831
|
+
def collect_class_decls(node, qualified_prefix, accumulator)
|
|
832
|
+
return unless node.is_a?(Prism::Node)
|
|
833
|
+
|
|
834
|
+
case node
|
|
835
|
+
when Prism::ClassNode
|
|
836
|
+
name = qualified_name_for(node.constant_path)
|
|
837
|
+
if name
|
|
838
|
+
full = (qualified_prefix + [name]).join("::")
|
|
839
|
+
accumulator[full] = Type::Combinator.singleton_of(full)
|
|
840
|
+
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
841
|
+
end
|
|
842
|
+
when Prism::ModuleNode
|
|
843
|
+
name = qualified_name_for(node.constant_path)
|
|
844
|
+
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
848
|
+
end
|
|
849
|
+
|
|
706
850
|
# Walks the program once for `Prism::ModuleNode` and
|
|
707
851
|
# `Prism::ClassNode`, recording the `Singleton[<qualified>]`
|
|
708
852
|
# type for the outermost `constant_path` node of each
|
|
@@ -760,6 +760,19 @@ module Rigor
|
|
|
760
760
|
# name as a Symbol, so the produced type is `Constant[:name]`.
|
|
761
761
|
def eval_def(node)
|
|
762
762
|
body_scope = build_method_entry_scope(node)
|
|
763
|
+
# Parameter default value expressions (e.g. `self.x` in
|
|
764
|
+
# `def copy(x: self.x)`) execute when the method is
|
|
765
|
+
# *invoked*, not when the `def` is read; their `self` is
|
|
766
|
+
# the instance receiver, not the surrounding class body.
|
|
767
|
+
# Walk the parameters subtree under `body_scope` so the
|
|
768
|
+
# scope-index records the instance `self_type` for every
|
|
769
|
+
# node inside parameter defaults. `propagate` would
|
|
770
|
+
# otherwise drop them to the outer class-body scope (where
|
|
771
|
+
# `self_type` is `singleton(C)`), making `self.foo` look
|
|
772
|
+
# like a singleton-side call. Observed surfacing 915 false
|
|
773
|
+
# positives in `prism-1.9.0`'s auto-generated `copy`
|
|
774
|
+
# methods alone.
|
|
775
|
+
sub_eval(node.parameters, body_scope, class_context: @class_context) if node.parameters
|
|
763
776
|
sub_eval(node.body, body_scope, class_context: @class_context) if node.body
|
|
764
777
|
[Type::Combinator.constant_of(node.name), scope]
|
|
765
778
|
end
|
|
@@ -783,6 +796,21 @@ module Rigor
|
|
|
783
796
|
def eval_call(node)
|
|
784
797
|
call_type = scope.type_of(node, tracer: tracer)
|
|
785
798
|
evaluate_block_if_present(node)
|
|
799
|
+
# `ruby2_keywords def foo(...)` (and similar wrappers like
|
|
800
|
+
# `private def`, `public def`, `module_function def`) parse
|
|
801
|
+
# the def as the call's positional argument; the
|
|
802
|
+
# ExpressionTyper#type_of_def handler types it as
|
|
803
|
+
# `Constant[:foo]` without walking the body. Without
|
|
804
|
+
# explicitly evaluating the argument-position def, the body's
|
|
805
|
+
# scope-index entries inherit the outer class-body
|
|
806
|
+
# `self_type = singleton(C)` from `ScopeIndexer.propagate`,
|
|
807
|
+
# so `self.helper` inside reports `undefined method 'helper'
|
|
808
|
+
# for singleton(C)`. Walking each argument-position def under
|
|
809
|
+
# the current evaluator (not a sub_eval — the def's effects
|
|
810
|
+
# do not bind into the surrounding scope) populates the
|
|
811
|
+
# scope index with the correct instance / singleton
|
|
812
|
+
# `self_type` for the def's body.
|
|
813
|
+
evaluate_def_arguments(node)
|
|
786
814
|
post_scope = record_closure_escape_if_any(node)
|
|
787
815
|
post_scope = apply_rbs_extended_assertions(node, post_scope)
|
|
788
816
|
post_scope = apply_plugin_assertions(node, post_scope)
|
|
@@ -790,6 +818,15 @@ module Rigor
|
|
|
790
818
|
[call_type, post_scope]
|
|
791
819
|
end
|
|
792
820
|
|
|
821
|
+
def evaluate_def_arguments(call_node)
|
|
822
|
+
args = call_node.arguments
|
|
823
|
+
return unless args.respond_to?(:arguments)
|
|
824
|
+
|
|
825
|
+
args.arguments.each do |arg|
|
|
826
|
+
eval_def(arg) if arg.is_a?(Prism::DefNode)
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
793
830
|
# v0.0.3 — recognises a small catalogue of RSpec
|
|
794
831
|
# matcher patterns as assert-shaped narrows on the
|
|
795
832
|
# local passed to `expect(...)`. The pattern is
|
|
@@ -1399,7 +1436,37 @@ module Rigor
|
|
|
1399
1436
|
end
|
|
1400
1437
|
|
|
1401
1438
|
def singleton_def?(def_node)
|
|
1402
|
-
def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
|
|
1439
|
+
return true if def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
|
|
1440
|
+
|
|
1441
|
+
def_receiver_targets_lexical_self?(def_node.receiver)
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
1445
|
+
# `module Meta`) — explicit-receiver def that semantically
|
|
1446
|
+
# equals `def self.bar` because the receiver constant
|
|
1447
|
+
# resolves to `self` at the def-site. Matched against the
|
|
1448
|
+
# current class context's tail to cover both the
|
|
1449
|
+
# `def OpenURI.x` form (single segment) and the
|
|
1450
|
+
# `def OpenURI::Meta.x` form (qualified path). Cross-class
|
|
1451
|
+
# receivers (`def Bar.baz` inside `module Foo` where the
|
|
1452
|
+
# receiver names a different constant) are not promoted to
|
|
1453
|
+
# singleton at this slice.
|
|
1454
|
+
def def_receiver_targets_lexical_self?(receiver)
|
|
1455
|
+
return false if @class_context.empty?
|
|
1456
|
+
|
|
1457
|
+
prefix = @class_context.map(&:name)
|
|
1458
|
+
case receiver
|
|
1459
|
+
when Prism::ConstantReadNode
|
|
1460
|
+
receiver.name.to_s == prefix.last
|
|
1461
|
+
when Prism::ConstantPathNode
|
|
1462
|
+
rendered = render_constant_path(receiver)
|
|
1463
|
+
return false unless rendered
|
|
1464
|
+
|
|
1465
|
+
path = rendered.split("::")
|
|
1466
|
+
prefix.last(path.length) == path
|
|
1467
|
+
else
|
|
1468
|
+
false
|
|
1469
|
+
end
|
|
1403
1470
|
end
|
|
1404
1471
|
|
|
1405
1472
|
# Slice A-engine. Inside a class body `class Foo; ...; end`,
|
|
@@ -1452,12 +1519,45 @@ module Rigor
|
|
|
1452
1519
|
end
|
|
1453
1520
|
|
|
1454
1521
|
def singleton_context_for(node)
|
|
1455
|
-
|
|
1456
|
-
|
|
1522
|
+
case node.expression
|
|
1523
|
+
when Prism::SelfNode
|
|
1524
|
+
return @class_context if @class_context.empty?
|
|
1525
|
+
|
|
1526
|
+
outer = @class_context[0..-2]
|
|
1527
|
+
last = @class_context.last
|
|
1528
|
+
outer + [ClassFrame.new(name: last.name, singleton: true)]
|
|
1529
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1530
|
+
target = singleton_constant_target(node.expression)
|
|
1531
|
+
return @class_context unless target
|
|
1532
|
+
|
|
1533
|
+
# `class << Foo` inside `class Foo` (the canonical
|
|
1534
|
+
# pattern in Ruby's own time.rb) is semantically
|
|
1535
|
+
# `class << self` — replace the enclosing frame with a
|
|
1536
|
+
# singleton frame for the same name so method
|
|
1537
|
+
# registration and `self_type` lookup land on Foo.
|
|
1538
|
+
# When the target names a different constant (rare
|
|
1539
|
+
# cross-class form), append a fresh singleton frame
|
|
1540
|
+
# tagged with the target FQN; the bodies are scoped to
|
|
1541
|
+
# that target rather than to the lexical enclosing
|
|
1542
|
+
# class.
|
|
1543
|
+
if !@class_context.empty? && @class_context.last.name == target
|
|
1544
|
+
outer = @class_context[0..-2]
|
|
1545
|
+
outer + [ClassFrame.new(name: target, singleton: true)]
|
|
1546
|
+
else
|
|
1547
|
+
[ClassFrame.new(name: target, singleton: true)]
|
|
1548
|
+
end
|
|
1549
|
+
else
|
|
1550
|
+
@class_context
|
|
1551
|
+
end
|
|
1552
|
+
end
|
|
1457
1553
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1554
|
+
def singleton_constant_target(expression)
|
|
1555
|
+
case expression
|
|
1556
|
+
when Prism::ConstantReadNode
|
|
1557
|
+
expression.name.to_s
|
|
1558
|
+
when Prism::ConstantPathNode
|
|
1559
|
+
render_constant_path(expression)
|
|
1560
|
+
end
|
|
1461
1561
|
end
|
|
1462
1562
|
|
|
1463
1563
|
# The qualified name of the immediately-enclosing class (joining
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-16 Tier C output — one synthetic method declared by a
|
|
6
|
+
# plugin's `Plugin::Macro::HeredocTemplate` entry, after the
|
|
7
|
+
# pre-pass has interpolated the call-site literal symbol into
|
|
8
|
+
# the template name. Stored in {SyntheticMethodIndex} and
|
|
9
|
+
# consulted by {MethodDispatcher} below the RBS dispatch tier.
|
|
10
|
+
#
|
|
11
|
+
# Per ADR-16 § WD13 (cost-bounded best-effort): the v0.1.x
|
|
12
|
+
# delivery commitment is the floor — method names emit; their
|
|
13
|
+
# return types degrade to `Dynamic[T]` until slice 6
|
|
14
|
+
# (precision promotion) routes the recorded `return_type`
|
|
15
|
+
# string through ADR-13's `Plugin::TypeNodeResolver` chain.
|
|
16
|
+
# The string is preserved so the ceiling slice can resolve it
|
|
17
|
+
# without re-walking.
|
|
18
|
+
#
|
|
19
|
+
# The `provenance` Hash carries debug / `--explain` metadata:
|
|
20
|
+
# plugin id, the template's call shape, and the source
|
|
21
|
+
# location of the originating DSL call. Surfaced through the
|
|
22
|
+
# dispatcher's `macro.tier_c.*` provenance markers.
|
|
23
|
+
class SyntheticMethod
|
|
24
|
+
INSTANCE = :instance
|
|
25
|
+
SINGLETON = :singleton
|
|
26
|
+
VALID_KINDS = [INSTANCE, SINGLETON].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :class_name, :method_name, :return_type, :kind, :provenance
|
|
29
|
+
|
|
30
|
+
def initialize(class_name:, method_name:, return_type:, kind: INSTANCE, provenance: {})
|
|
31
|
+
validate!(class_name, method_name, return_type, kind, provenance)
|
|
32
|
+
@class_name = class_name.dup.freeze
|
|
33
|
+
@method_name = method_name.to_sym
|
|
34
|
+
@return_type = return_type.dup.freeze
|
|
35
|
+
@kind = kind
|
|
36
|
+
@provenance = provenance.transform_keys(&:to_sym).transform_values do |v|
|
|
37
|
+
v.is_a?(String) ? v.dup.freeze : v
|
|
38
|
+
end.freeze
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def instance? = kind == INSTANCE
|
|
43
|
+
def singleton? = kind == SINGLETON
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
"class_name" => class_name,
|
|
48
|
+
"method_name" => method_name.to_s,
|
|
49
|
+
"return_type" => return_type,
|
|
50
|
+
"kind" => kind.to_s,
|
|
51
|
+
"provenance" => provenance.transform_keys(&:to_s)
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
other.is_a?(SyntheticMethod) && to_h == other.to_h
|
|
57
|
+
end
|
|
58
|
+
alias eql? ==
|
|
59
|
+
|
|
60
|
+
def hash
|
|
61
|
+
to_h.hash
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate!(class_name, method_name, return_type, kind, provenance)
|
|
67
|
+
unless class_name.is_a?(String) && !class_name.empty?
|
|
68
|
+
raise ArgumentError, "SyntheticMethod#class_name must be non-empty String, got #{class_name.inspect}"
|
|
69
|
+
end
|
|
70
|
+
unless method_name.is_a?(Symbol) || (method_name.is_a?(String) && !method_name.empty?)
|
|
71
|
+
raise ArgumentError, "SyntheticMethod#method_name must be Symbol/non-empty String, got #{method_name.inspect}"
|
|
72
|
+
end
|
|
73
|
+
unless return_type.is_a?(String) && !return_type.empty?
|
|
74
|
+
raise ArgumentError, "SyntheticMethod#return_type must be non-empty String, got #{return_type.inspect}"
|
|
75
|
+
end
|
|
76
|
+
unless VALID_KINDS.include?(kind)
|
|
77
|
+
raise ArgumentError, "SyntheticMethod#kind must be one of #{VALID_KINDS.inspect}, got #{kind.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return if provenance.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
raise ArgumentError, "SyntheticMethod#provenance must be a Hash, got #{provenance.inspect}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|