rigortype 0.3.0 → 0.3.1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/array.yml +416 -392
  4. data/data/builtins/ruby_core/file.yml +42 -42
  5. data/data/builtins/ruby_core/hash.yml +302 -302
  6. data/data/builtins/ruby_core/io.yml +191 -191
  7. data/data/builtins/ruby_core/numeric.yml +321 -366
  8. data/data/builtins/ruby_core/proc.yml +124 -124
  9. data/data/builtins/ruby_core/range.yml +21 -21
  10. data/data/builtins/ruby_core/rational.yml +39 -39
  11. data/data/builtins/ruby_core/re.yml +65 -65
  12. data/data/builtins/ruby_core/set.yml +106 -106
  13. data/data/builtins/ruby_core/struct.yml +14 -14
  14. data/data/core_overlay/string_scanner.rbs +6 -5
  15. data/docs/handbook/01-getting-started.md +22 -34
  16. data/docs/handbook/06-classes.md +1 -1
  17. data/docs/handbook/07-rbs-and-extended.md +76 -101
  18. data/docs/handbook/08-understanding-errors.md +114 -247
  19. data/docs/handbook/09-plugins.md +54 -144
  20. data/docs/handbook/README.md +5 -3
  21. data/docs/handbook/appendix-liskov.md +4 -2
  22. data/docs/handbook/appendix-phpstan.md +14 -7
  23. data/docs/handbook/appendix-steep.md +4 -2
  24. data/docs/handbook/appendix-type-theory.md +3 -1
  25. data/docs/manual/02-cli-reference.md +32 -0
  26. data/docs/manual/04-diagnostics.md +36 -4
  27. data/docs/manual/06-baseline.md +35 -1
  28. data/docs/manual/08-skills.md +6 -1
  29. data/docs/manual/09-editor-integration.md +3 -2
  30. data/docs/manual/plugins/rigor-actioncable.md +32 -0
  31. data/docs/manual/plugins/rigor-devise.md +4 -2
  32. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
  33. data/lib/rigor/analysis/check_rules.rb +34 -13
  34. data/lib/rigor/analysis/run_cache_key.rb +10 -0
  35. data/lib/rigor/analysis/runner.rb +2 -1
  36. data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
  37. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
  38. data/lib/rigor/cache/store.rb +99 -24
  39. data/lib/rigor/cli/check_command.rb +12 -6
  40. data/lib/rigor/cli/check_invocation.rb +84 -0
  41. data/lib/rigor/cli/doctor_command.rb +6 -8
  42. data/lib/rigor/cli/skill_command.rb +21 -1
  43. data/lib/rigor/cli/skill_deep_probe.rb +172 -0
  44. data/lib/rigor/cli/skill_describe.rb +75 -9
  45. data/lib/rigor/environment/default_libraries.rb +5 -4
  46. data/lib/rigor/environment.rb +10 -1
  47. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
  48. data/lib/rigor/language_server/buffer_resolution.rb +6 -3
  49. data/lib/rigor/language_server/buffer_table.rb +46 -6
  50. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
  51. data/lib/rigor/language_server/incremental_sync.rb +159 -0
  52. data/lib/rigor/language_server/server.rb +19 -9
  53. data/lib/rigor/language_server.rb +1 -0
  54. data/lib/rigor/plugin/base.rb +29 -2
  55. data/lib/rigor/sig_gen/writer.rb +183 -47
  56. data/lib/rigor/version.rb +1 -1
  57. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
  58. data/sig/rigor/cache.rbs +6 -0
  59. data/sig/rigor/inference/void_origin.rbs +18 -0
  60. data/sig/rigor/plugin/base.rbs +4 -3
  61. metadata +7 -3
@@ -279,9 +279,7 @@ module Rigor
279
279
  return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
280
280
 
281
281
  state = MergeState.new(source: source, decls: decls, applied: [], skipped: [])
282
- supers = merged_superclasses(candidates)
283
- candidates.group_by(&:class_name).each { |class_name, methods| merge_class(state, class_name, methods, supers) }
284
- merge_class_shells(state, collect_class_shells(candidates), merged_namespace_kinds(candidates))
282
+ merge_candidates(state, candidates)
285
283
 
286
284
  action = state.applied.empty? ? :noop : :updated
287
285
  unless action == :updated
@@ -304,25 +302,42 @@ module Rigor
304
302
  action: action, applied: state.applied, skipped: state.skipped)
305
303
  end
306
304
 
305
+ # Applies every class group, then every requested shell, then normalises the layout the three steps
306
+ # leave behind. Each step mutates `state` in place and re-parses, so the next one sees current offsets.
307
+ def merge_candidates(state, candidates)
308
+ supers = merged_superclasses(candidates)
309
+ kinds = merged_namespace_kinds(candidates)
310
+ shells = collect_class_shells(candidates)
311
+ groups = candidates.group_by(&:class_name)
312
+ # Ancestors first, so `Foo` is created (or found) before `Foo::Bar` looks for a parent to nest under.
313
+ # A plain sort suffices: a name always sorts before every name it is a strict prefix of.
314
+ groups.keys.sort.each { |name| merge_class(state, name, groups.fetch(name), kinds, supers) }
315
+ merge_class_shells(state, shells, kinds, supers)
316
+ collapse_nested_declarations(state, groups.keys + shells.to_a)
317
+ end
318
+
307
319
  # ADR-14 gap-#3 (e): for every requested class shell that isn't already declared in the target file,
308
320
  # insert an empty `class Const\nend` block inside the nearest existing ancestor. Shells already covered by
309
321
  # an existing declaration are silently a no-op. The `applied` accumulator does NOT grow — shells are
310
322
  # structural declarations, not methods, so the action-count surface (`updated +N`) keeps reflecting method
311
323
  # changes only.
312
- def merge_class_shells(state, shells, kinds)
324
+ def merge_class_shells(state, shells, kinds, supers)
313
325
  shells.each do |qualified|
314
326
  next if find_class_decl(state.decls, qualified)
315
327
 
316
- insert_class_shell(state, qualified, kinds)
328
+ insert_namespace_chain(state, qualified, kinds, supers, [])
317
329
  end
318
330
  end
319
331
 
320
- def insert_class_shell(state, qualified, kinds)
332
+ # Splices the missing part of `qualified`'s namespace chain into the nearest declaration the file
333
+ # already has for one of its ancestors, or at top level when it has none. `methods` land on the leaf
334
+ # node, so this is the single insertion path for both an empty class shell and a brand-new class.
335
+ def insert_namespace_chain(state, qualified, kinds, supers, methods)
321
336
  segments = qualified.split("::")
322
337
  anchor_segs, missing = split_at_existing_ancestor(state.decls, segments)
323
338
  anchor_decl = anchor_segs.empty? ? nil : find_class_decl(state.decls, anchor_segs.join("::"))
324
- depth = anchor_decl ? anchor_decl_indent_depth(anchor_decl) : 0
325
- snippet = build_shell_snippet(missing, anchor_segs, kinds, depth)
339
+ depth = anchor_decl ? member_indent_depth(anchor_decl) : 0
340
+ snippet = render_chain_snippet(missing, anchor_segs, kinds, supers, depth, methods)
326
341
  state.source = if anchor_decl
327
342
  insert_before_end(state.source, anchor_decl, snippet)
328
343
  else
@@ -331,6 +346,113 @@ module Rigor
331
346
  state.decls = parse_signature(state.source) || state.decls
332
347
  end
333
348
 
349
+ # ADR-14 gap-#3 follow-up (c), update half: the create path folds a strict-prefix pair into one nested
350
+ # tree, but a file written before that fix — or by hand — can still carry the flat sibling layout
351
+ # (`class Foo` next to a top-level `class Foo::Bar`). After merging, relocate each declaration this run
352
+ # touched underneath its parent's declaration when the same file holds both, so an update converges on
353
+ # the same canonical layout a fresh generation would produce.
354
+ #
355
+ # Scope is deliberately the touched names only: an unrelated flat pair elsewhere in the file is none of
356
+ # sig-gen's business, and rewriting it would be a layout change the user never asked for. Shallowest
357
+ # first, so `Foo::Bar` has already moved under `Foo` by the time `Foo::Bar::Baz` looks for its parent.
358
+ def collapse_nested_declarations(state, names)
359
+ names.uniq.sort.each { |name| relocate_under_parent(state, name) }
360
+ end
361
+
362
+ def relocate_under_parent(state, qualified)
363
+ segments = qualified.split("::")
364
+ return if segments.size < 2
365
+
366
+ parent_name = segments[0...-1].join("::")
367
+ parent = find_class_decl(state.decls, parent_name)
368
+ decl = parent && find_class_decl(state.decls, qualified)
369
+ return if decl.nil? || overlapping?(parent, decl)
370
+
371
+ region = decl_region(state.source, decl)
372
+ block = region && relocated_block(state.source, decl, parent, segments.last, region)
373
+ return if block.nil?
374
+
375
+ apply_relocation(state, region, parent_name, block)
376
+ end
377
+
378
+ # Cuts the declaration's region out, re-parses so the parent's byte range reflects the removal, and
379
+ # splices the re-indented block back in as the parent's last member. Any step that cannot be carried
380
+ # out cleanly (an unparseable intermediate, a parent that vanished) abandons the move and leaves the
381
+ # merged source exactly as it was — a flat layout is cosmetic, a mangled `.rbs` is not.
382
+ def apply_relocation(state, region, parent_name, block)
383
+ source = splice_out(state.source, region)
384
+ decls = parse_signature(source)
385
+ anchor = decls && find_class_decl(decls, parent_name)
386
+ return if anchor.nil?
387
+
388
+ state.source = insert_before_end(source, anchor, block)
389
+ state.decls = parse_signature(state.source) || state.decls
390
+ end
391
+
392
+ # True when the two declarations' source ranges are not disjoint — either one already nests the other,
393
+ # or the file's shape is one this pass does not understand. Both are reasons not to move anything.
394
+ def overlapping?(one, other)
395
+ one.location.start_pos < other.location.end_pos && other.location.start_pos < one.location.end_pos
396
+ end
397
+
398
+ # The declaration's full source region, extended to cover its leading comment and annotations and to
399
+ # end just past the newline that closes it. Returns `nil` when either edge shares a line with something
400
+ # else, because cutting there would move — or strand — text the declaration does not own.
401
+ def decl_region(source, decl)
402
+ start_pos = ([decl.location.start_pos] + leading_positions(decl)).min
403
+ line_start = line_start_index(source, start_pos)
404
+ return nil unless blank_range?(source, line_start, start_pos)
405
+
406
+ finish = decl.location.end_pos
407
+ line_end = source.index("\n", finish) || source.size
408
+ return nil unless blank_range?(source, finish, line_end)
409
+
410
+ line_start...(line_end < source.size ? line_end + 1 : line_end)
411
+ end
412
+
413
+ def leading_positions(decl)
414
+ positions = decl.annotations.filter_map { |a| a.location&.start_pos }
415
+ comment_location = decl.comment&.location
416
+ positions << comment_location.start_pos if comment_location
417
+ positions
418
+ end
419
+
420
+ # The moved text, with its compact `Foo::Bar` head shortened to `Bar` and every line pushed in to the
421
+ # parent's member depth. The name's own sub-location drives the rewrite, so a superclass, type
422
+ # parameters, and the whole body survive byte-for-byte.
423
+ def relocated_block(source, decl, parent, short_name, region)
424
+ name_location = decl.location[:name]
425
+ return nil if name_location.nil?
426
+
427
+ text = source[region].to_s
428
+ head = name_location.start_pos - region.begin
429
+ tail = name_location.end_pos - region.begin
430
+ renamed = text[0...head].to_s + short_name + text[tail..].to_s
431
+ reindent(renamed, member_indent_depth(parent) - decl_indent_depth(decl))
432
+ end
433
+
434
+ def reindent(text, delta)
435
+ return text unless delta.positive?
436
+
437
+ prefix = INDENT * delta
438
+ text.lines.map { |line| line.match?(/\A\s*\z/) ? line : prefix + line }.join
439
+ end
440
+
441
+ # Removes `region`, collapsing the newline run left behind at the seam so the cut never shows up as a
442
+ # widening gap (or a trailing blank line at EOF) in the file it edited. Spacing that was already fine
443
+ # is left exactly as the user wrote it.
444
+ def splice_out(source, region)
445
+ before = source[0...region.begin].to_s
446
+ after = source[region.end..].to_s
447
+ return after.sub(/\A\n+/, "") if before.match?(/\A\s*\z/)
448
+ return before.sub(/\n{2,}\z/, "\n") if after.match?(/\A\s*\z/)
449
+
450
+ seam = before[/\n+\z/].to_s.size + after[/\A\n+/].to_s.size
451
+ return before + after if seam <= 2
452
+
453
+ "#{before.sub(/\n+\z/, "\n\n")}#{after.sub(/\A\n+/, '')}"
454
+ end
455
+
334
456
  def split_at_existing_ancestor(decls, segments)
335
457
  (segments.size - 1).downto(0).each do |i|
336
458
  ancestor = segments[0...i].join("::")
@@ -339,37 +461,61 @@ module Rigor
339
461
  [[], segments]
340
462
  end
341
463
 
342
- # Pulls the indent depth (in `INDENT` units) one level deeper than the anchor decl's own column.
343
- # Pre-existing members might be missing (an empty `class Foo; end`) so the keyword column is the robust
344
- # signal.
345
- def anchor_decl_indent_depth(decl)
346
- decl_column = decl.location[:keyword].start_column
347
- (decl_column / INDENT.size) + 1
464
+ # The indent depth (in `INDENT` units) a member of `decl` sits at: one level deeper than the
465
+ # declaration's own keyword column. Pre-existing members might be missing (an empty `class Foo; end`)
466
+ # so the keyword column is the robust signal.
467
+ def member_indent_depth(decl)
468
+ decl_indent_depth(decl) + 1
348
469
  end
349
470
 
350
- def build_shell_snippet(missing, anchor_segs, kinds, depth)
471
+ def decl_indent_depth(decl)
472
+ decl.location[:keyword].start_column / INDENT.size
473
+ end
474
+
475
+ # Renders the `missing` segment chain as one nested block, carrying `methods` on its leaf, by handing a
476
+ # synthesised tree node to the create path's renderer. Both paths therefore agree on the keyword, the
477
+ # superclass suffix, and the indentation of every level. A leaf with no methods is a class shell, which
478
+ # is exactly the create path's `shell:` node (gap-#3 (e)).
479
+ def render_chain_snippet(missing, anchor_segs, kinds, supers, depth, methods)
351
480
  return "" if missing.empty?
352
481
 
353
- head, *rest = missing
354
- qualified = (anchor_segs + [head]).join("::")
355
- indent = INDENT * depth
356
- if rest.empty?
357
- keyword = kinds[qualified] || :class
358
- "#{indent}#{keyword} #{head}\n#{indent}end\n"
359
- else
360
- inner = build_shell_snippet(rest, anchor_segs + [head], kinds, depth + 1)
361
- keyword = kinds[qualified] || :module
362
- "#{indent}#{keyword} #{head}\n#{inner}#{indent}end\n"
482
+ node = missing.reverse.each_with_index.inject(nil) do |child, (segment, index)|
483
+ { name: segment, children: child ? { child[:name] => child } : {},
484
+ methods: index.zero? ? methods : [], shell: index.zero? && methods.empty? }
363
485
  end
486
+ render_tree_node(node, kinds, supers, depth, anchor_segs)
364
487
  end
365
488
 
489
+ # Splices `snippet` in just before the declaration's closing `end`. When that `end` starts its own line
490
+ # the insertion point moves to the START of that line: the snippet carries its own indentation, and
491
+ # anchoring at the keyword would otherwise donate the `end`'s indent to the snippet's first line and
492
+ # strand the `end` in column zero.
366
493
  def insert_before_end(source, decl, snippet)
494
+ return source if snippet.empty?
495
+
367
496
  end_pos = decl.location[:end].start_pos
368
- source[0...end_pos] + snippet + source[end_pos..]
497
+ line_start = line_start_index(source, end_pos)
498
+ return source[0...line_start] + snippet + source[line_start..] if blank_range?(source, line_start, end_pos)
499
+
500
+ "#{source[0...end_pos]}\n#{snippet}#{source[end_pos..]}"
369
501
  end
370
502
 
503
+ # Appends a top-level block, separated from whatever precedes it by exactly one blank line.
371
504
  def append_top_level(source, snippet)
372
- ends_with_newline?(source) ? source + snippet : "#{source}\n#{snippet}"
505
+ return snippet if source.empty?
506
+
507
+ base = ends_with_newline?(source) ? source : "#{source}\n"
508
+ base.end_with?("\n\n") ? base + snippet : "#{base}\n#{snippet}"
509
+ end
510
+
511
+ def line_start_index(source, pos)
512
+ return 0 unless pos.positive?
513
+
514
+ (source.rindex("\n", pos - 1) || -1) + 1
515
+ end
516
+
517
+ def blank_range?(source, from, to)
518
+ source[from...to].to_s.match?(/\A[ \t]*\z/)
373
519
  end
374
520
 
375
521
  def parse_signature(source)
@@ -379,14 +525,15 @@ module Rigor
379
525
  nil
380
526
  end
381
527
 
382
- def merge_class(state, class_name, methods, supers = {})
528
+ def merge_class(state, class_name, methods, kinds, supers)
383
529
  decl = find_class_decl(state.decls, class_name)
384
- state.source = if decl.nil?
385
- append_new_class(state.source, class_name, methods, state.applied, supers[class_name])
386
- else
387
- merge_into_existing_class(state.source, decl, methods, state.applied, state.skipped)
388
- end
389
- state.decls = parse_signature(state.source) || state.decls
530
+ if decl.nil?
531
+ state.applied.concat(methods)
532
+ insert_namespace_chain(state, class_name, kinds, supers, methods)
533
+ else
534
+ state.source = merge_into_existing_class(state.source, decl, methods, state.applied, state.skipped)
535
+ state.decls = parse_signature(state.source) || state.decls
536
+ end
390
537
  end
391
538
 
392
539
  # Walks the parsed decl tree recursively, tracking the enclosing module/class prefix, and returns the
@@ -411,16 +558,6 @@ module Rigor
411
558
  nil
412
559
  end
413
560
 
414
- # Appends an entirely new `class Foo … end` block at the end of the file (with a leading blank line as
415
- # separator).
416
- def append_new_class(source, class_name, methods, applied, superclass = nil)
417
- body = methods.map { |c| "#{INDENT}#{c.rbs}" }.join("\n")
418
- header = superclass ? "class #{class_name} < #{superclass}" : "class #{class_name}"
419
- snippet = "\n#{header}\n#{body}\nend\n"
420
- applied.concat(methods)
421
- ends_with_newline?(source) ? source + snippet : "#{source}\n#{snippet}"
422
- end
423
-
424
561
  def ends_with_newline?(source)
425
562
  source.end_with?("\n")
426
563
  end
@@ -477,9 +614,8 @@ module Rigor
477
614
  def insert_into_class(source, decl, new_methods)
478
615
  return source if new_methods.empty?
479
616
 
480
- end_pos = decl.location[:end].start_pos
481
- addition = new_methods.map { |c| "#{INDENT}#{c.rbs}\n" }.join
482
- source[0...end_pos] + addition + source[end_pos..]
617
+ indent = INDENT * member_indent_depth(decl)
618
+ insert_before_end(source, decl, new_methods.map { |c| "#{indent}#{c.rbs}\n" }.join)
483
619
  end
484
620
 
485
621
  # Walks the class's existing method declarations; for each replaceable candidate that matches a member
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -42,6 +42,25 @@ module Rigor
42
42
  # informational only (deferred: cross-plugin handoff to a JS-side analyzer).
43
43
  # - **`broadcast_to` arity isn't checked.** The method takes any record + any data hash; there's no
44
44
  # useful arity envelope.
45
+ #
46
+ # ## `#receive(data)` protocol contract (ADR-28)
47
+ #
48
+ # ActionCable's `Connection::Subscriptions#perform_action` dispatches an incoming message to the
49
+ # action named by the payload's `"action"` key, defaulting to `:receive` when that key is absent —
50
+ # i.e. `#receive(data)` is the framework-documented catch-all handler for messages with no explicit
51
+ # action. Either way the single positional argument is always the `ActiveSupport::JSON.decode` of the
52
+ # client-sent payload, which is a JSON object and therefore a `Hash` by convention (the bundled JS
53
+ # client only ever calls `perform(action, data = {})` with an object). This plugin declares a
54
+ # path-scoped protocol contract (`method_name: :receive`, `param_types: [{index: 0, type_name:
55
+ # "Hash"}]`) so `data` types as `Hash` instead of `Dynamic[Top]` inside a channel's `#receive` body.
56
+ #
57
+ # Deliberately narrower than Hanami's `#handle`: custom action methods (`def speak(data)`) also
58
+ # receive the decoded Hash, but their names are project-chosen, not a single fixed Symbol a
59
+ # `ProtocolContract` can name — only `:receive` is a framework-reserved, uniformly-shaped method name.
60
+ # Per ADR-28, the contract is path-scoped, not class-scoped: any `#receive(data)` defined anywhere
61
+ # under the (possibly multi-root) `channel_search_paths` is typed, even on a stray non-channel class
62
+ # that happens to live in that directory — the same accepted-risk shape as `rigor-hanami`'s
63
+ # `#handle`.
45
64
  class Actioncable < Rigor::Plugin::Base
46
65
  manifest(
47
66
  id: "actioncable",
@@ -53,7 +72,15 @@ module Rigor
53
72
  kind: :array,
54
73
  default: ["ApplicationCable::Channel", "ActionCable::Channel::Base"]
55
74
  }
56
- }
75
+ },
76
+ protocol_contracts: [
77
+ Rigor::Plugin::ProtocolContract.new(
78
+ path_glob: "app/channels/**/*.rb",
79
+ method_name: :receive,
80
+ param_types: [{ index: 0, type_name: "Hash" }]
81
+ # return_type_name: nil — receive's return value is discarded by the framework dispatcher.
82
+ )
83
+ ]
57
84
  )
58
85
 
59
86
  # `watch:` covers every `.rb` file under the channel search paths so the cache invalidates when
@@ -70,6 +97,20 @@ module Rigor
70
97
  def init(_services)
71
98
  @channel_search_paths = Array(config.fetch("channel_search_paths")).map(&:to_s)
72
99
  @channel_base_classes = Array(config.fetch("channel_base_classes")).map(&:to_s)
100
+
101
+ # ADR-28 WD5 — `channel_search_paths` is user-configurable and may name multiple roots; retarget
102
+ # the manifest's default `app/channels/**/*.rb` glob to the actual configured root(s) so the
103
+ # contract neither goes inert on a project that renames/adds channel directories nor (more
104
+ # importantly, per the project's false-positive discipline) risks matching an unrelated `#receive`
105
+ # outside them. `File::FNM_EXTGLOB` (enabled by `Registry#contracts_for_path`) makes a `{a,b}`
106
+ # brace group a single valid glob for multiple roots.
107
+ @protocol_contracts = manifest.protocol_contracts.map { |c| c.with_path_glob(channel_search_glob) }
108
+ end
109
+
110
+ # ADR-28 — override so the per-project `channel_search_paths` config reaches both the engine's
111
+ # parameter-provision tier and this plugin's own path-scoped reasoning.
112
+ def protocol_contracts
113
+ @protocol_contracts || manifest.protocol_contracts
73
114
  end
74
115
 
75
116
  # File-level only: the load-error emission. Per-call broadcast validation runs over the
@@ -91,6 +132,15 @@ module Rigor
91
132
 
92
133
  private
93
134
 
135
+ # Builds the `#receive` contract's `path_glob` from the configured `channel_search_paths`. A single
136
+ # root becomes a plain `**/*.rb` glob; multiple roots become an `FNM_EXTGLOB` brace group so every
137
+ # configured root — and only a configured root — is covered.
138
+ def channel_search_glob
139
+ roots = @channel_search_paths.map { |p| p.chomp("/") }
140
+ base = roots.length == 1 ? roots.first : "{#{roots.join(',')}}"
141
+ "#{base}/**/*.rb"
142
+ end
143
+
94
144
  def load_error_diagnostic(path)
95
145
  error = producer_error(:channel_index)
96
146
  Rigor::Analysis::Diagnostic.new(
data/sig/rigor/cache.rbs CHANGED
@@ -14,6 +14,12 @@ module Rigor
14
14
  # PRODUCER_ID constant and a private `self.compute(loader)`.
15
15
  class RbsCacheProducer
16
16
  def self.fetch: (loader: untyped, store: untyped) -> untyped
17
+
18
+ # The generation cap this producer declares to `Cache::Store#evict!`
19
+ # (issue #151). `rigor sig-gen` infers the literal `2` from the base
20
+ # body; the contract is the wider `Integer`, since a subclass may
21
+ # override it.
22
+ def self.generation_cap: () -> Integer
17
23
  end
18
24
  end
19
25
  end
@@ -0,0 +1,18 @@
1
+ module Rigor
2
+ module Inference
3
+ # `VoidOrigin = Data.define(:class_name, :method_name, :kind)`. Declared as a `::Data` subclass because
4
+ # that is the shape the runtime actually builds; `sig-gen` cannot yet read a `Data.define` assignment
5
+ # (it reports the block body's `#label` against the enclosing namespace and drops the members).
6
+ class VoidOrigin < ::Data
7
+ attr_reader class_name: String
8
+ attr_reader method_name: Symbol
9
+ attr_reader kind: Symbol
10
+
11
+ def self.new: (class_name: String, method_name: Symbol, kind: Symbol) -> instance
12
+ | (String class_name, Symbol method_name, Symbol kind) -> instance
13
+
14
+ # A human-facing `Class#method` / `Class.method` label for the diagnostic message.
15
+ def label: () -> String
16
+ end
17
+ end
18
+ end
@@ -14,7 +14,7 @@ class Rigor::Plugin::Base
14
14
  # the no-arg `manifest` reads the cached value back.
15
15
  def self.manifest: (**untyped fields) -> Rigor::Plugin::Manifest
16
16
 
17
- def self.producer: (untyped id, ?watch: untyped, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
17
+ def self.producer: (untyped id, ?watch: untyped, ?serialize: untyped, ?deserialize: untyped, ?generation_cap: untyped) { (untyped params) -> untyped } -> Symbol
18
18
  def self.producers: () -> Hash[Symbol, untyped]
19
19
 
20
20
  def self.node_rule: (untyped node_type) { (*untyped) -> untyped } -> untyped
@@ -23,11 +23,12 @@ class Rigor::Plugin::Base
23
23
  def self.node_file_context: () { (untyped root, untyped scope) -> untyped } -> untyped
24
24
  def self.node_file_context_block: () -> untyped
25
25
 
26
- def self.dynamic_return: (?receivers: (Array[String] | ^() -> Array[String])?, ?methods: (Array[untyped] | ^() -> Array[untyped])?) { (untyped call_node, untyped scope) -> untyped } -> nil
26
+ def self.dynamic_return: (?receivers: (Array[String] | ^() -> Array[String])?, ?methods: (Array[untyped] | ^() -> Array[untyped])?, ?file_methods: (^(untyped path) -> Array[untyped])?) { (untyped call_node, untyped scope) -> untyped } -> nil
27
27
  def self.dynamic_returns: () -> Array[untyped]
28
28
 
29
29
  def self.narrowing_facts: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
30
- # Deprecating alias for `narrowing_facts` (ADR-80); removed in 0.3.0.
30
+ # The engine-facing reader for the declared `narrowing_facts` rules. Named
31
+ # `type_specifiers` before ADR-80; that spelling was removed in 0.3.0.
31
32
  def self.narrowing_facts_rules: () -> Array[untyped]
32
33
 
33
34
  def self.suggest: (untyped name, untyped candidates) -> String?
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: language_server-protocol
@@ -460,6 +460,7 @@ files:
460
460
  - lib/rigor/cli/annotate_command.rb
461
461
  - lib/rigor/cli/baseline_command.rb
462
462
  - lib/rigor/cli/check_command.rb
463
+ - lib/rigor/cli/check_invocation.rb
463
464
  - lib/rigor/cli/check_runner_factory.rb
464
465
  - lib/rigor/cli/command.rb
465
466
  - lib/rigor/cli/coverage_command.rb
@@ -491,6 +492,7 @@ files:
491
492
  - lib/rigor/cli/show_bleedingedge_command.rb
492
493
  - lib/rigor/cli/sig_gen_command.rb
493
494
  - lib/rigor/cli/skill_command.rb
495
+ - lib/rigor/cli/skill_deep_probe.rb
494
496
  - lib/rigor/cli/skill_describe.rb
495
497
  - lib/rigor/cli/trace_command.rb
496
498
  - lib/rigor/cli/trace_renderer.rb
@@ -622,6 +624,7 @@ files:
622
624
  - lib/rigor/language_server/folding_range_provider.rb
623
625
  - lib/rigor/language_server/hover_provider.rb
624
626
  - lib/rigor/language_server/hover_renderer.rb
627
+ - lib/rigor/language_server/incremental_sync.rb
625
628
  - lib/rigor/language_server/loop.rb
626
629
  - lib/rigor/language_server/project_context.rb
627
630
  - lib/rigor/language_server/selection_range_provider.rb
@@ -876,6 +879,7 @@ files:
876
879
  - sig/rigor/inference/builtins.rbs
877
880
  - sig/rigor/inference/builtins/method_catalog.rbs
878
881
  - sig/rigor/inference/builtins/numeric_catalog.rbs
882
+ - sig/rigor/inference/void_origin.rbs
879
883
  - sig/rigor/plugin.rbs
880
884
  - sig/rigor/plugin/access_denied_error.rbs
881
885
  - sig/rigor/plugin/base.rbs
@@ -987,7 +991,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
987
991
  - !ruby/object:Gem::Version
988
992
  version: '0'
989
993
  requirements: []
990
- rubygems_version: 3.7.2
994
+ rubygems_version: 4.0.10
991
995
  specification_version: 4
992
996
  summary: Inference-first static analysis for Ruby.
993
997
  test_files: []