rigortype 0.2.6 → 0.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/manual/02-cli-reference.md +2 -1
  4. data/docs/manual/08-skills.md +21 -0
  5. data/lib/rigor/cli/coverage_command.rb +42 -10
  6. data/lib/rigor/cli/skill_command.rb +52 -1
  7. data/lib/rigor/environment/rbs_loader.rb +28 -0
  8. data/lib/rigor/inference/statement_evaluator.rb +0 -4
  9. data/lib/rigor/sig_gen/generator.rb +25 -0
  10. data/lib/rigor/sig_gen/method_candidate.rb +7 -2
  11. data/lib/rigor/sig_gen/writer.rb +60 -13
  12. data/lib/rigor/version.rb +1 -1
  13. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +63 -2
  14. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +2 -3
  15. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +14 -24
  16. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +10 -3
  17. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +33 -76
  18. data/skills/rigor-ask/SKILL.md +21 -1
  19. data/skills/rigor-baseline-reduce/SKILL.md +16 -0
  20. data/skills/rigor-ci-setup/SKILL.md +96 -249
  21. data/skills/rigor-doctor/SKILL.md +39 -49
  22. data/skills/rigor-doctor/references/01-checks.md +52 -0
  23. data/skills/rigor-editor-setup/SKILL.md +14 -0
  24. data/skills/rigor-mcp-setup/SKILL.md +14 -0
  25. data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
  26. data/skills/rigor-plugin-author/SKILL.md +16 -0
  27. data/skills/rigor-plugin-review/SKILL.md +174 -0
  28. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
  29. data/skills/rigor-plugin-tune/SKILL.md +21 -2
  30. data/skills/rigor-project-init/SKILL.md +16 -0
  31. data/skills/rigor-protection-uplift/SKILL.md +15 -0
  32. data/skills/rigor-rbs-setup/SKILL.md +15 -0
  33. data/skills/rigor-upgrade/SKILL.md +16 -0
  34. metadata +7 -4
@@ -22,17 +22,20 @@ module Rigor
22
22
  @contracts = contracts
23
23
  end
24
24
 
25
- def check(path:, root:)
26
- @contracts.flat_map do |contract|
27
- next [] unless path_matches?(contract.path_glob, path)
28
-
29
- class_nodes(root).filter_map do |class_node|
30
- handle_def = find_handle(class_node, contract)
31
- if handle_def.nil?
32
- missing_handle_diagnostic(contract, path, class_node)
33
- else
34
- handle_arity_mismatch_diagnostic(contract, path, class_node, handle_def)
35
- end
25
+ # Per-`ClassNode` check over the engine-owned walk (ADR-37):
26
+ # called once per class node the `node_rule` in `hanami.rb`
27
+ # dispatches, against every contract whose `path_glob` matches
28
+ # the file. No cross-class-node state is needed, so the checker
29
+ # ships no `class_nodes` traversal of its own.
30
+ def check_class(class_node, path:)
31
+ @contracts.filter_map do |contract|
32
+ next unless path_matches?(contract.path_glob, path)
33
+
34
+ handle_def = find_handle(class_node, contract)
35
+ if handle_def.nil?
36
+ missing_handle_diagnostic(contract, path, class_node)
37
+ else
38
+ handle_arity_mismatch_diagnostic(contract, path, class_node, handle_def)
36
39
  end
37
40
  end
38
41
  end
@@ -49,12 +52,6 @@ module Rigor
49
52
  File.fnmatch?(File.join("**", glob), path, FNMATCH_FLAGS)
50
53
  end
51
54
 
52
- def class_nodes(root)
53
- found = []
54
- walk(root) { |node| found << node if node.is_a?(Prism::ClassNode) }
55
- found
56
- end
57
-
58
55
  def find_handle(class_node, contract)
59
56
  direct_defs(class_node).find do |def_node|
60
57
  def_node.name == contract.method_name &&
@@ -107,13 +104,6 @@ module Rigor
107
104
  path = class_node.constant_path
108
105
  path.respond_to?(:slice) ? path.slice : class_node.name.to_s
109
106
  end
110
-
111
- def walk(node, &)
112
- return if node.nil?
113
-
114
- yield node
115
- node.compact_child_nodes.each { |child| walk(child, &) }
116
- end
117
107
  end
118
108
  end
119
109
  end
@@ -98,11 +98,18 @@ module Rigor
98
98
  @protocol_contracts || manifest.protocol_contracts
99
99
  end
100
100
 
101
- def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
101
+ # ADR-37 per-class-node validation over the engine-owned walk.
102
+ # Each `Prism::ClassNode` is checked against every contract
103
+ # independently, so the plugin no longer ships its own `class_nodes`
104
+ # traversal; `ActionChecker#check_class` keeps the per-class
105
+ # contract logic. (A per-class contract check is exactly what
106
+ # `node_rule` is for — the return type is void, so no `scope`
107
+ # query is needed.)
108
+ node_rule Prism::ClassNode do |node, _scope, path|
102
109
  contracts = protocol_contracts
103
- return [] if contracts.empty?
110
+ next [] if contracts.empty?
104
111
 
105
- ActionChecker.new(contracts: contracts).check(path: path, root: root)
112
+ ActionChecker.new(contracts: contracts).check_class(node, path: path)
106
113
  end
107
114
  end
108
115
 
@@ -147,11 +147,7 @@ module Rigor
147
147
  # symlink-bearing form here. Look up under both so the
148
148
  # match is symlink-agnostic.
149
149
  errors = @parse_errors_by_path[path] || @parse_errors_by_path[canonicalize(path)] || []
150
- diagnostics = errors.map { |error| parse_error_diagnostic(path, error) }
151
- diagnostics.concat(absurd_reachable_diagnostics(path, root))
152
- diagnostics.concat(reveal_type_diagnostics(path, root))
153
- diagnostics.concat(assert_type_mismatch_diagnostics(path, root))
154
- diagnostics
150
+ errors.map { |error| parse_error_diagnostic(path, error) }
155
151
  end
156
152
 
157
153
  # ADR-52 slice 4 — per-call return-type path via the
@@ -179,6 +175,35 @@ module Rigor
179
175
  bind_post_return_facts(call_node, scope)
180
176
  end
181
177
 
178
+ # ADR-37 — the three per-call diagnostics ride the engine-owned
179
+ # walk instead of three hand-rolled `walk_for_*` recursions. Each
180
+ # candidate `T.` call is *recorded by object identity* during the
181
+ # inference pass (the `dynamic_return` / `narrowing_facts` rules
182
+ # above call `record_*`), so by the time these node rules fire in
183
+ # the diagnostics phase the sets are populated; the membership
184
+ # `delete` both gates the emission and pops the entry so a re-run
185
+ # cannot double-fire. The recorded set is the gate — no per-node
186
+ # `AbsurdRecognizer` / name check is needed here.
187
+ node_rule Prism::CallNode do |node, _scope, path|
188
+ next [] unless @reachable_absurd_nodes.delete(node)
189
+
190
+ [absurd_diagnostic(path, node)]
191
+ end
192
+
193
+ node_rule Prism::CallNode do |node, _scope, path|
194
+ display = @reveal_type_calls.delete(node)
195
+ next [] if display.nil?
196
+
197
+ [reveal_type_diagnostic(path, node, display)]
198
+ end
199
+
200
+ node_rule Prism::CallNode do |node, _scope, path|
201
+ recorded = @assert_type_mismatches.delete(node)
202
+ next [] if recorded.nil?
203
+
204
+ [assert_type_mismatch_diagnostic(path, node, *recorded)]
205
+ end
206
+
182
207
  private
183
208
 
184
209
  # Run-time method-name gate for the `dynamic_return` rule
@@ -536,31 +561,9 @@ module Rigor
536
561
  nil
537
562
  end
538
563
 
539
- # Walks the per-file AST looking for `T.absurd(x)` call
540
- # nodes and emits a `plugin.sorbet.absurd-reachable`
541
- # warning for any whose object identity matches
542
- # `@reachable_absurd_nodes` (populated during the engine's
543
- # earlier pass through the `dynamic_return` rule). Pops
544
- # matched entries so a duplicate run doesn't double-emit.
545
- def absurd_reachable_diagnostics(path, root)
546
- return [] if @reachable_absurd_nodes.empty?
547
-
548
- diagnostics = []
549
- walk_for_absurd(root) do |call_node|
550
- next unless @reachable_absurd_nodes.delete(call_node)
551
-
552
- diagnostics << absurd_diagnostic(path, call_node)
553
- end
554
- diagnostics
555
- end
556
-
557
- def walk_for_absurd(node, &)
558
- return unless node.is_a?(Prism::Node)
559
-
560
- yield node if node.is_a?(Prism::CallNode) && AbsurdRecognizer.absurd_call?(node)
561
- node.compact_child_nodes.each { |child| walk_for_absurd(child, &) }
562
- end
563
-
564
+ # Emits a `plugin.sorbet.absurd-reachable` warning for the
565
+ # `T.absurd(x)` call recorded in `@reachable_absurd_nodes` during
566
+ # inference; the node rule above does the identity match and pop.
564
567
  def absurd_diagnostic(path, call_node)
565
568
  Rigor::Analysis::Diagnostic.from_node(
566
569
  call_node,
@@ -591,29 +594,6 @@ module Rigor
591
594
  type.respond_to?(:describe) ? type.describe : type.inspect
592
595
  end
593
596
 
594
- def reveal_type_diagnostics(path, root)
595
- return [] if @reveal_type_calls.empty?
596
-
597
- diagnostics = []
598
- walk_for_reveal_type(root) do |call_node|
599
- display = @reveal_type_calls.delete(call_node)
600
- next if display.nil?
601
-
602
- diagnostics << reveal_type_diagnostic(path, call_node, display)
603
- end
604
- diagnostics
605
- end
606
-
607
- def walk_for_reveal_type(node, &)
608
- return unless node.is_a?(Prism::Node)
609
-
610
- if node.is_a?(Prism::CallNode) && node.name == :reveal_type &&
611
- TypeTranslator.sorbet_t_namespaced?(node.receiver)
612
- yield node
613
- end
614
- node.compact_child_nodes.each { |child| walk_for_reveal_type(child, &) }
615
- end
616
-
617
597
  def reveal_type_diagnostic(path, call_node, display)
618
598
  Rigor::Analysis::Diagnostic.from_node(
619
599
  call_node,
@@ -647,29 +627,6 @@ module Rigor
647
627
  @assert_type_mismatches[call_node] = [display_for_type(inferred), display_for_type(asserted)]
648
628
  end
649
629
 
650
- def assert_type_mismatch_diagnostics(path, root)
651
- return [] if @assert_type_mismatches.empty?
652
-
653
- diagnostics = []
654
- walk_for_assert_type(root) do |call_node|
655
- recorded = @assert_type_mismatches.delete(call_node)
656
- next if recorded.nil?
657
-
658
- diagnostics << assert_type_mismatch_diagnostic(path, call_node, *recorded)
659
- end
660
- diagnostics
661
- end
662
-
663
- def walk_for_assert_type(node, &)
664
- return unless node.is_a?(Prism::Node)
665
-
666
- if node.is_a?(Prism::CallNode) && node.name == :assert_type! &&
667
- TypeTranslator.sorbet_t_namespaced?(node.receiver)
668
- yield node
669
- end
670
- node.compact_child_nodes.each { |child| walk_for_assert_type(child, &) }
671
- end
672
-
673
630
  def assert_type_mismatch_diagnostic(path, call_node, inferred_display, asserted_display)
674
631
  Rigor::Analysis::Diagnostic.from_node(
675
632
  call_node,
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rigor-ask
3
3
  description: |
4
- Rigor is a niche, fast-moving Ruby type checker; its rules, flags, and type behaviour are version-specific, so what you "remember" about it is likely wrong or stale — do NOT answer from memory or guess. For ANY question about Rigor, use this skill and investigate procedurally: run `rigor docs` (handbook + manual, bundled OFFLINE and version-matched), `rigor explain` for a diagnostic id, and for the user's own code `rigor check` / `annotate` / `type-of`, then answer only from what you read. Covers: why a line is flagged or if it's a false positive; the type model (narrowing, refinements, `Dynamic`, RBS); config keys, flags, baselines; comparisons to Sorbet, Steep, mypy, PHPStan; whether it handles Rails, RSpec, or a gem; writing an RBS signature; "what is Rigor / why use it / is it right for us?". Trigger on any Rigor mention seeking understanding — even casual, comparative, or grumbling. Skip only when Rigor isn't mentioned, or it's purely "set it up / fix / reduce it for me" ( rigor-next-steps).
4
+ Answer any question about Rigor by investigating, not from memory Rigor is niche and version-specific, so run its tools and read its bundled docs, then answer from what you saw. Use `rigor docs` (handbook + manual, offline and version-matched) and `rigor explain <rule>`; for the user's own code, `rigor check` / `annotate` / `type-of`. Covers: why a line is flagged or whether it's a false positive; the type model (narrowing, refinements, `Dynamic`, RBS); config keys, flags, baselines; comparisons to Sorbet, Steep, mypy, PHPStan; whether Rigor handles Rails, RSpec, or a given gem; how to type a method; "what is Rigor / why use it / is it right for us?". Trigger on any Rigor question seeking understanding — even casual, comparative, or grumbling. Skip only when it's purely "set it up / fix / reduce it for me" (use rigor-next-steps).
5
5
  license: MPL-2.0
6
6
  metadata:
7
7
  version: 0.2.0
@@ -36,6 +36,26 @@ This is the user's shortcut: they only ever need to remember two skills —
36
36
  into the right lookup or analysis so they never have to remember the
37
37
  command.
38
38
 
39
+ ## Prefer the live map over this skill's tables
40
+
41
+ The doc-page names and command flags below are a snapshot; Rigor's pages
42
+ and CLI move release to release, and a vendored copy of this skill (e.g.
43
+ added via `npx skills`) lags further still. Treat the tables as
44
+ orientation, then confirm against the **installed** Rigor, which is always
45
+ current:
46
+
47
+ ```sh
48
+ rigor docs --list # the live doc map — what pages exist in THIS version
49
+ rigor --help # the live command + flag list
50
+ rigor skill --full rigor-ask # the current version of this skill itself
51
+ ```
52
+
53
+ Never cite a page, flag, or rule id you have not just seen in one of these
54
+ — that is this skill's whole discipline. If `rigor` is not installed, you
55
+ can still answer high-level "what is Rigor?" questions from the web
56
+ <https://rigor.typedduck.fail/llms.txt>, but for anything version-specific
57
+ install it first (see `rigor-next-steps`).
58
+
39
59
  ## The toolbox
40
60
 
41
61
  Everything here is read-only and needs no network.
@@ -19,6 +19,22 @@ This skill is for **users improving their own project**. It uses the
19
19
  published `rigor` executable on `PATH` and references only public CLI
20
20
  flags and config keys.
21
21
 
22
+ ## First: load the version-current copy
23
+
24
+ The step-by-step commands live in this skill's `references/` files, and
25
+ their exact flags drift between Rigor releases — so follow the copy that
26
+ ships with the **installed** Rigor rather than any vendored or frozen copy
27
+ of this file. Get the complete current procedure in one call:
28
+
29
+ ```sh
30
+ rigor skill --full rigor-baseline-reduce # this body + all its references/, inline
31
+ ```
32
+
33
+ If you already loaded this skill *via* `rigor skill` you have the current
34
+ copy — just proceed (read any `references/NN-*.md` from the directory the
35
+ header names). If `rigor` is not on `PATH`, this task needs it: run
36
+ **`rigor-next-steps`** to install Rigor first, then come back.
37
+
22
38
  ## Phase 0 — When to use this skill
23
39
 
24
40
  Trigger when the user says "reduce the rigor baseline", "fix some