rigortype 0.1.5 → 0.1.7
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 +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- 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/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -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/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- 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 +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- 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/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -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/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- metadata +56 -1
|
@@ -53,24 +53,36 @@ module Rigor
|
|
|
53
53
|
# inheritance resolution against RBS-known classes
|
|
54
54
|
# (ActiveRecord::Base, Dry::Struct, etc.) that aren't
|
|
55
55
|
# declared in project source.
|
|
56
|
+
# @param fact_store [Rigor::Plugin::FactStore, nil]
|
|
57
|
+
# the per-run cross-plugin fact store. ADR-18 lookups
|
|
58
|
+
# (`Plugin::Macro::HeredocTemplate::Emit#returns_from_arg`)
|
|
59
|
+
# consult this at scan time to resolve per-call-site
|
|
60
|
+
# return types from published facts; without it, those
|
|
61
|
+
# emit rows fall back to their static `returns:` (or
|
|
62
|
+
# `"untyped"` → `Dynamic[Top]`).
|
|
63
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
64
|
+
# editor-mode buffer binding. When set, reads for the
|
|
65
|
+
# logical path resolve to the buffer's physical path so
|
|
66
|
+
# the pre-pass sees the in-flight bytes instead of the
|
|
67
|
+
# on-disk copy.
|
|
56
68
|
# @return [Rigor::Inference::SyntheticMethodIndex]
|
|
57
|
-
def scan(plugin_registry:, paths:, environment: nil)
|
|
69
|
+
def scan(plugin_registry:, paths:, environment: nil, fact_store: nil, buffer: nil)
|
|
58
70
|
templates = collect_templates(plugin_registry)
|
|
59
71
|
registries = collect_trait_registries(plugin_registry)
|
|
60
72
|
return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty?
|
|
61
73
|
|
|
62
|
-
asts = parse_paths(paths)
|
|
74
|
+
asts = parse_paths(paths, buffer: buffer)
|
|
63
75
|
hierarchy = build_hierarchy(asts)
|
|
64
76
|
concern_index = build_concern_index(asts)
|
|
65
77
|
|
|
66
78
|
entries = []
|
|
67
79
|
asts.each do |path, ast|
|
|
68
80
|
walk_class_bodies(ast) do |class_name, call_node|
|
|
69
|
-
collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path)
|
|
81
|
+
collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path, fact_store)
|
|
70
82
|
collect_trait_entries(entries, registries, class_name, call_node, hierarchy, environment, path)
|
|
71
83
|
collect_concern_re_targeted_entries(
|
|
72
84
|
entries, call_node, class_name, concern_index,
|
|
73
|
-
templates, registries, hierarchy, environment, path
|
|
85
|
+
templates, registries, hierarchy, environment, path, fact_store
|
|
74
86
|
)
|
|
75
87
|
end
|
|
76
88
|
end
|
|
@@ -107,10 +119,11 @@ module Rigor
|
|
|
107
119
|
end
|
|
108
120
|
end
|
|
109
121
|
|
|
110
|
-
def parse_paths(paths)
|
|
122
|
+
def parse_paths(paths, buffer: nil)
|
|
111
123
|
paths.to_h do |path|
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
125
|
+
source = File.read(physical)
|
|
126
|
+
[path, Prism.parse(source, filepath: path).value]
|
|
114
127
|
rescue StandardError
|
|
115
128
|
[path, nil]
|
|
116
129
|
end
|
|
@@ -209,7 +222,7 @@ module Rigor
|
|
|
209
222
|
# `collect_trait_entries` fire just as if the calls had been
|
|
210
223
|
# written directly in X's body.
|
|
211
224
|
def collect_concern_re_targeted_entries(entries, call_node, class_name, concern_index, # rubocop:disable Metrics/ParameterLists
|
|
212
|
-
templates, registries, hierarchy, environment, path)
|
|
225
|
+
templates, registries, hierarchy, environment, path, fact_store = nil)
|
|
213
226
|
return unless call_node.name == :include && call_node.receiver.nil?
|
|
214
227
|
return if concern_index.empty?
|
|
215
228
|
|
|
@@ -220,7 +233,7 @@ module Rigor
|
|
|
220
233
|
next unless deferred
|
|
221
234
|
|
|
222
235
|
deferred.each do |inner_call|
|
|
223
|
-
collect_entries(entries, templates, class_name, inner_call, hierarchy, environment, path)
|
|
236
|
+
collect_entries(entries, templates, class_name, inner_call, hierarchy, environment, path, fact_store)
|
|
224
237
|
collect_trait_entries(entries, registries, class_name, inner_call, hierarchy, environment, path)
|
|
225
238
|
end
|
|
226
239
|
end
|
|
@@ -318,7 +331,7 @@ module Rigor
|
|
|
318
331
|
parent_str ? "#{parent_str}::#{name}" : name
|
|
319
332
|
end
|
|
320
333
|
|
|
321
|
-
def collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path)
|
|
334
|
+
def collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path, fact_store = nil) # rubocop:disable Metrics/ParameterLists
|
|
322
335
|
templates.each do |(plugin_id, template)|
|
|
323
336
|
next unless call_node.name == template.method_name
|
|
324
337
|
next unless class_inherits_from?(class_name, template.receiver_constraint, hierarchy, environment)
|
|
@@ -326,7 +339,7 @@ module Rigor
|
|
|
326
339
|
symbol_arg = literal_symbol_arg(call_node, template.symbol_arg_position)
|
|
327
340
|
next if symbol_arg.nil?
|
|
328
341
|
|
|
329
|
-
emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node)
|
|
342
|
+
emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node, fact_store)
|
|
330
343
|
end
|
|
331
344
|
end
|
|
332
345
|
|
|
@@ -439,28 +452,31 @@ module Rigor
|
|
|
439
452
|
)
|
|
440
453
|
end
|
|
441
454
|
|
|
442
|
-
def emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node)
|
|
455
|
+
def emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node, fact_store = nil) # rubocop:disable Metrics/ParameterLists
|
|
443
456
|
template.emit.each do |row|
|
|
444
457
|
entries << build_synthetic_method(
|
|
445
458
|
class_name: class_name, name_arg: symbol_arg, row: row,
|
|
446
459
|
template: template, plugin_id: plugin_id, path: path, call_node: call_node,
|
|
447
|
-
kind: SyntheticMethod::INSTANCE
|
|
460
|
+
kind: SyntheticMethod::INSTANCE, fact_store: fact_store
|
|
448
461
|
)
|
|
449
462
|
end
|
|
450
463
|
template.class_level_emit.each do |row|
|
|
451
464
|
entries << build_synthetic_method(
|
|
452
465
|
class_name: class_name, name_arg: symbol_arg, row: row,
|
|
453
466
|
template: template, plugin_id: plugin_id, path: path, call_node: call_node,
|
|
454
|
-
kind: SyntheticMethod::SINGLETON
|
|
467
|
+
kind: SyntheticMethod::SINGLETON, fact_store: fact_store
|
|
455
468
|
)
|
|
456
469
|
end
|
|
457
470
|
end
|
|
458
471
|
|
|
459
|
-
|
|
472
|
+
# rubocop:disable Metrics/ParameterLists
|
|
473
|
+
def build_synthetic_method(class_name:, name_arg:, row:, template:, plugin_id:, path:, call_node:, kind:,
|
|
474
|
+
fact_store: nil)
|
|
475
|
+
# rubocop:enable Metrics/ParameterLists
|
|
460
476
|
SyntheticMethod.new(
|
|
461
477
|
class_name: class_name,
|
|
462
478
|
method_name: interpolate(row.name, name_arg).to_sym,
|
|
463
|
-
return_type: row
|
|
479
|
+
return_type: resolve_emit_return_type(row, call_node, fact_store),
|
|
464
480
|
kind: kind,
|
|
465
481
|
provenance: {
|
|
466
482
|
plugin_id: plugin_id,
|
|
@@ -472,6 +488,68 @@ module Rigor
|
|
|
472
488
|
)
|
|
473
489
|
end
|
|
474
490
|
|
|
491
|
+
# ADR-18 three-tier fallback for the synthetic method's
|
|
492
|
+
# `return_type` string:
|
|
493
|
+
#
|
|
494
|
+
# 1. When `row.returns_from_arg` is present AND the
|
|
495
|
+
# call-site argument at the declared position is a
|
|
496
|
+
# resolvable constant reference AND the fact_store
|
|
497
|
+
# has a matching value, use that as the return type.
|
|
498
|
+
# 2. Else if `row.returns` is a non-empty String, use it
|
|
499
|
+
# (the slice-6b static path).
|
|
500
|
+
# 3. Else use `"untyped"` so the dispatcher's
|
|
501
|
+
# `promote_via_return_type` sentinel chain yields
|
|
502
|
+
# `Dynamic[Top]`.
|
|
503
|
+
def resolve_emit_return_type(row, call_node, fact_store)
|
|
504
|
+
resolved = resolve_returns_from_arg(row.returns_from_arg, call_node, fact_store)
|
|
505
|
+
return resolved if resolved
|
|
506
|
+
return row.returns if row.returns
|
|
507
|
+
|
|
508
|
+
"untyped"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def resolve_returns_from_arg(returns_from_arg, call_node, fact_store)
|
|
512
|
+
return nil if returns_from_arg.nil?
|
|
513
|
+
|
|
514
|
+
source_rep = argument_source_representation(call_node, returns_from_arg.position)
|
|
515
|
+
return nil if source_rep.nil?
|
|
516
|
+
return nil if fact_store.nil?
|
|
517
|
+
|
|
518
|
+
fact = fact_store.read(plugin_id: returns_from_arg.plugin_id, name: returns_from_arg.fact)
|
|
519
|
+
return nil unless fact.is_a?(Hash)
|
|
520
|
+
|
|
521
|
+
fact[source_rep]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Extracts the source-text qualified-constant representation
|
|
525
|
+
# of the call's positional argument (e.g.,
|
|
526
|
+
# `"Types::String"`). Returns nil for non-constant shapes
|
|
527
|
+
# (literals, method chains, blocks, …). The floor
|
|
528
|
+
# intentionally accepts only ConstantReadNode /
|
|
529
|
+
# ConstantPathNode per ADR-18; chained-call argument
|
|
530
|
+
# resolution stays deferred.
|
|
531
|
+
def argument_source_representation(call_node, position)
|
|
532
|
+
args = call_node.arguments&.arguments
|
|
533
|
+
return nil if args.nil? || position >= args.size
|
|
534
|
+
|
|
535
|
+
node = args[position]
|
|
536
|
+
case node
|
|
537
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
538
|
+
when Prism::ConstantPathNode then qualified_constant_name(node)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def qualified_constant_name(node)
|
|
543
|
+
case node
|
|
544
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
545
|
+
when Prism::ConstantPathNode
|
|
546
|
+
parent_name = node.parent.nil? ? nil : qualified_constant_name(node.parent)
|
|
547
|
+
return nil if !node.parent.nil? && parent_name.nil?
|
|
548
|
+
|
|
549
|
+
parent_name.nil? ? node.name.to_s : "#{parent_name}::#{node.name}"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
475
553
|
def interpolate(template_name, name_arg)
|
|
476
554
|
template_name.gsub(Rigor::Plugin::Macro::HeredocTemplate::NAME_PLACEHOLDER, name_arg.to_s)
|
|
477
555
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module LanguageServer
|
|
5
|
+
# Per-session virtual file table. The LSP server maintains the
|
|
6
|
+
# canonical view of every open buffer here; analysis (slice 4+)
|
|
7
|
+
# reads from this table instead of disk so in-flight edits are
|
|
8
|
+
# reflected immediately.
|
|
9
|
+
#
|
|
10
|
+
# Keyed by `DocumentUri` (LSP `file://...` URIs). v1 ships
|
|
11
|
+
# FULL text sync (LSP `TextDocumentSyncKind::Full = 1`) so each
|
|
12
|
+
# `didChange` carries the entire buffer text — there's no
|
|
13
|
+
# incremental edit application yet. Incremental sync is slice
|
|
14
|
+
# 10 (deferred per the design doc).
|
|
15
|
+
class BufferTable
|
|
16
|
+
# @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
|
|
17
|
+
# @!attribute bytes [String] the current full text of the buffer.
|
|
18
|
+
# @!attribute version [Integer] the monotonically increasing LSP version number.
|
|
19
|
+
Entry = Data.define(:uri, :bytes, :version)
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@entries = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Records a `textDocument/didOpen` event. Replaces any
|
|
26
|
+
# existing entry (LSP clients may re-open a previously closed
|
|
27
|
+
# URI; the new version is authoritative).
|
|
28
|
+
def open(uri:, bytes:, version:)
|
|
29
|
+
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Records a `textDocument/didChange` event under FULL sync.
|
|
33
|
+
# The full new buffer text replaces the entry. If the client
|
|
34
|
+
# sends a `didChange` for a URI that was never opened (spec
|
|
35
|
+
# violation), the entry is still created — defensive.
|
|
36
|
+
def change(uri:, bytes:, version:)
|
|
37
|
+
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Records a `textDocument/didClose` event. The entry is
|
|
41
|
+
# removed. Subsequent reads via `#[]` return nil.
|
|
42
|
+
def close(uri:)
|
|
43
|
+
@entries.delete(uri)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](uri)
|
|
47
|
+
@entries[uri]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def open?(uri)
|
|
51
|
+
@entries.key?(uri)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def size
|
|
55
|
+
@entries.size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def uris
|
|
59
|
+
@entries.keys
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|