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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -79
  3. data/lib/rigor/analysis/baseline.rb +347 -0
  4. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  5. data/lib/rigor/analysis/check_rules.rb +68 -3
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  7. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/runner.rb +309 -22
  11. data/lib/rigor/analysis/worker_session.rb +14 -2
  12. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  13. data/lib/rigor/builtins/static_return_refinements.rb +142 -0
  14. data/lib/rigor/cache/store.rb +33 -3
  15. data/lib/rigor/cli/baseline_command.rb +377 -0
  16. data/lib/rigor/cli/lsp_command.rb +129 -0
  17. data/lib/rigor/cli/type_of_command.rb +44 -5
  18. data/lib/rigor/cli.rb +142 -13
  19. data/lib/rigor/configuration.rb +58 -2
  20. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  21. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  22. data/lib/rigor/environment/rbs_loader.rb +67 -2
  23. data/lib/rigor/environment/reporters.rb +40 -0
  24. data/lib/rigor/environment.rb +119 -9
  25. data/lib/rigor/flow_contribution/fact.rb +20 -10
  26. data/lib/rigor/inference/acceptance.rb +48 -3
  27. data/lib/rigor/inference/expression_typer.rb +64 -2
  28. data/lib/rigor/inference/hkt_body.rb +171 -0
  29. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  30. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  31. data/lib/rigor/inference/hkt_registry.rb +223 -0
  32. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
  33. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
  34. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  35. data/lib/rigor/inference/method_dispatcher.rb +174 -6
  36. data/lib/rigor/inference/narrowing.rb +103 -1
  37. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  38. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  39. data/lib/rigor/inference/scope_indexer.rb +209 -19
  40. data/lib/rigor/inference/statement_evaluator.rb +172 -11
  41. data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
  42. data/lib/rigor/language_server/buffer_table.rb +63 -0
  43. data/lib/rigor/language_server/completion_provider.rb +438 -0
  44. data/lib/rigor/language_server/debouncer.rb +86 -0
  45. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  46. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  47. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  48. data/lib/rigor/language_server/hover_provider.rb +74 -0
  49. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  50. data/lib/rigor/language_server/loop.rb +71 -0
  51. data/lib/rigor/language_server/project_context.rb +145 -0
  52. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  53. data/lib/rigor/language_server/server.rb +384 -0
  54. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  55. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  56. data/lib/rigor/language_server/uri.rb +40 -0
  57. data/lib/rigor/language_server.rb +29 -0
  58. data/lib/rigor/plugin/base.rb +63 -0
  59. data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
  60. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  61. data/lib/rigor/plugin/manifest.rb +54 -7
  62. data/lib/rigor/plugin/registry.rb +19 -0
  63. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  64. data/lib/rigor/rbs_extended.rb +82 -2
  65. data/lib/rigor/sig_gen/generator.rb +12 -3
  66. data/lib/rigor/type/app.rb +107 -0
  67. data/lib/rigor/type.rb +1 -0
  68. data/lib/rigor/version.rb +1 -1
  69. data/sig/rigor/environment.rbs +10 -4
  70. data/sig/rigor/inference.rbs +2 -0
  71. data/sig/rigor.rbs +4 -1
  72. 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
- source = File.read(path)
113
- [path, Prism.parse(source).value]
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
- def build_synthetic_method(class_name:, name_arg:, row:, template:, plugin_id:, path:, call_node:, kind:) # rubocop:disable Metrics/ParameterLists
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.returns,
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