rigortype 0.1.18 → 0.1.19

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  9. data/lib/rigor/analysis/check_rules.rb +266 -63
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
  13. data/lib/rigor/analysis/runner.rb +58 -21
  14. data/lib/rigor/analysis/worker_session.rb +21 -11
  15. data/lib/rigor/bleeding_edge.rb +123 -0
  16. data/lib/rigor/cache/descriptor.rb +86 -8
  17. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  18. data/lib/rigor/cli/annotate_command.rb +100 -15
  19. data/lib/rigor/cli/check_command.rb +3 -0
  20. data/lib/rigor/cli/plugins_command.rb +2 -4
  21. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  22. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  23. data/lib/rigor/cli/triage_command.rb +6 -3
  24. data/lib/rigor/cli/triage_renderer.rb +15 -1
  25. data/lib/rigor/cli.rb +9 -1
  26. data/lib/rigor/configuration/severity_profile.rb +13 -1
  27. data/lib/rigor/configuration.rb +57 -1
  28. data/lib/rigor/environment/rbs_loader.rb +25 -0
  29. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  30. data/lib/rigor/inference/budget_trace.rb +29 -2
  31. data/lib/rigor/inference/expression_typer.rb +1052 -43
  32. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  33. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  34. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  35. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  36. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  37. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  38. data/lib/rigor/inference/method_dispatcher.rb +72 -1
  39. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  40. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  41. data/lib/rigor/inference/mutation_widening.rb +142 -0
  42. data/lib/rigor/inference/narrowing.rb +270 -37
  43. data/lib/rigor/inference/scope_indexer.rb +696 -25
  44. data/lib/rigor/inference/statement_evaluator.rb +963 -16
  45. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  46. data/lib/rigor/plugin/base.rb +235 -79
  47. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  48. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  49. data/lib/rigor/plugin/macro.rb +2 -3
  50. data/lib/rigor/plugin/manifest.rb +4 -24
  51. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  52. data/lib/rigor/plugin/registry.rb +12 -11
  53. data/lib/rigor/scope/discovery_index.rb +2 -0
  54. data/lib/rigor/scope.rb +132 -6
  55. data/lib/rigor/sig_gen/generator.rb +8 -0
  56. data/lib/rigor/triage/catalogue.rb +4 -19
  57. data/lib/rigor/triage.rb +69 -1
  58. data/lib/rigor/type/combinator.rb +29 -0
  59. data/lib/rigor/version.rb +1 -1
  60. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  61. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  62. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  63. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  64. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
  65. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
  66. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  67. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  68. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  69. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  70. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  71. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  72. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
  73. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  74. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  75. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  76. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +25 -0
  77. data/sig/rigor/analysis/fact_store.rbs +3 -0
  78. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  79. data/sig/rigor/plugin/base.rbs +5 -2
  80. data/sig/rigor/plugin/manifest.rbs +1 -2
  81. data/sig/rigor/scope.rbs +10 -1
  82. data/sig/rigor/type.rbs +1 -0
  83. data/sig/rigor.rbs +1 -1
  84. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  85. data/skills/rigor-plugin-author/SKILL.md +6 -4
  86. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  87. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  88. metadata +7 -2
  89. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -37,12 +37,15 @@ rows the rule positions with `diagnostic` (the bundled plugins follow
37
37
  this `Analyzer.violations_for` split, which also makes the logic
38
38
  unit-testable without running the whole engine).
39
39
 
40
- > The legacy `diagnostics_for_file(path:, scope:, root:)` hook where
41
- > you hand-rolled a `def walk` / `compact_child_nodes.each` recursion
42
- > over `root` yourself is the **deprecated whole-file escape valve**.
43
- > Use it only for a genuinely file-scoped result (a single load-error
44
- > row, or a check that needs the whole parsed file at once). New
45
- > node-scoped checks use `node_rule`.
40
+ > `diagnostics_for_file(path:, scope:, root:)` is the **file-rule**
41
+ > surface use it for a genuinely file-scoped result (a single
42
+ > load-error row, a cross-file aggregation, or a check that needs the
43
+ > whole parsed file at once). It is not deprecated; it is the right tool
44
+ > when a check isn't per-node. Per-node checks use `node_rule` (you don't
45
+ > hand-roll a `def walk` / `compact_child_nodes.each` recursion — the
46
+ > engine owns the walk). Map a violation array to diagnostics with
47
+ > `diagnostics_for(violations, path:, node:)` rather than a hand-rolled
48
+ > `.map { diagnostic(...) }`.
46
49
 
47
50
  ### Recognising call sites
48
51
 
@@ -59,7 +62,9 @@ re-deriving the `unescaped.to_sym` shape.
59
62
  validate references): declare `node_file_context { |root, scope| … }`.
60
63
  It runs once per file before the walk and its return value is threaded
61
64
  to every rule as `file_context`. (A *cross-file* collect belongs in
62
- `#prepare` + `services.fact_store` instead see
65
+ `#prepare` + `services.fact_store.publish`; the consuming plugin reads
66
+ it with `read_fact(plugin_id:, name:)` — the nil-inclusive memo means
67
+ you never hand-roll an `@x_resolved` flag. See
63
68
  [`01-plan-and-scaffold.md`](01-plan-and-scaffold.md).)
64
69
  - **Where the node sits**: `context` (a `Rigor::Plugin::NodeContext`)
65
70
  carries the lexical ancestor chain — `context.enclosing_def`,
@@ -213,16 +218,16 @@ plugin is confident; a wrong contribution propagates downstream.
213
218
  `rigor plugins --capabilities` catalogue enumerates — run it to see
214
219
  exactly what each loaded plugin contributes.
215
220
 
216
- > **The deprecated escape valve.** The original fat hook,
217
- > `flow_contribution_for(call_node:, scope:)`, returns a single
218
- > `Rigor::FlowContribution` and is still consulted alongside the narrow
219
- > DSLs. It is retained only for the two shapes the narrow DSLs do not
220
- > express: a **method-gated return type** (an RSpec `let(:x) { … }`
221
- > binding, a Sorbet `sig`-driven return — keyed on the method, not a
222
- > fixed receiver class) and a **dynamic per-project receiver set**
223
- > (ActiveStorage's `Attached::One` on discovered model classes). If your
224
- > plugin needs one of those, use `flow_contribution_for`; otherwise
225
- > prefer `dynamic_return` / `type_specifier`. These return-type surfaces
221
+ > **The removed fat hook.** The original `flow_contribution_for(call_node:,
222
+ > scope:)` was **deleted pre-1.0 in ADR-52 WD3** — defining it now raises
223
+ > `ArgumentError` at load time. The two shapes it used to cover are
224
+ > expressed by `dynamic_return`'s callable gates: a **method-gated return
225
+ > type** (an RSpec `let(:x) { … }` binding, a Sorbet `sig`-driven return —
226
+ > keyed on the method, not a fixed receiver class) uses
227
+ > `dynamic_return methods: -> { [...] }` or `file_methods: ->(path) { [...] }`;
228
+ > a **dynamic per-project receiver set** (ActiveStorage's `Attached::One`
229
+ > on discovered model classes) uses `dynamic_return receivers: -> { [...] }`,
230
+ > the callable resolved once after `#prepare`. These return-type surfaces
226
231
  > are the most contract-sensitive part of the API — implement one only
227
232
  > if the plugin genuinely needs to sharpen call-site types; a
228
233
  > diagnostics-only plugin skips them entirely.
@@ -23,6 +23,8 @@ The JSON shape:
23
23
  {
24
24
  "summary": { "total": 489, "error": 480, "warning": 9, "info": 0 },
25
25
  "distribution": [ { "rule": "call.undefined-method", "count": 437 } ],
26
+ "selectors": [ { "receiver": "String", "method": "squish", "count": 31,
27
+ "files": 12, "rules": { "call.undefined-method": 31 } } ],
26
28
  "hotspots": [ { "file": "app/models/status.rb", "count": 42,
27
29
  "by_rule": { "call.undefined-method": 40 } } ],
28
30
  "hints": [
@@ -32,11 +34,26 @@ The JSON shape:
32
34
  }
33
35
  ```
34
36
 
35
- Use the three sections like this:
37
+ Use the sections like this:
36
38
 
37
39
  - **`summary` / `distribution`** — the scale, and which rules
38
40
  dominate. Decides nothing on its own; feeds the mode sanity-check
39
41
  (>100 errors → acknowledge mode is the right default).
42
+ - **`selectors`** — the by-(class, method) axis. Each row is a
43
+ dispatch target (`String#squish`) with its `count`, the `files` it
44
+ spans, and the `rules` that fired. Read it with `jq` to find the
45
+ *shape* of the problem before touching code — these are structured
46
+ fields, never parse the `message`:
47
+ ```sh
48
+ # the 10 methods responsible for the most diagnostics
49
+ rigor triage --format json | jq -r '.selectors[:10][] | "\(.count)\t\(.receiver)#\(.method)"'
50
+ # methods missing on the same receiver across many files = one config
51
+ # gap (an unloaded core-ext / unseen monkey-patch), not many bugs
52
+ rigor triage --format json | jq '.selectors[] | select(.files >= 5)'
53
+ ```
54
+ A high `count` + high `files` selector is almost always a *systemic
55
+ cause* (a plugin / `pre_eval:` fix clears it in bulk); a low `count`
56
+ selector is a candidate genuine bug.
40
57
  - **`hotspots`** — files carrying the most diagnostics. A single hot
41
58
  file is often one structural cause, not many bugs.
42
59
  - **`hints`** — the heuristic catalogue. Each hint names a *likely
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.18
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
@@ -289,6 +289,7 @@ files:
289
289
  - lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb
290
290
  - lib/rigor/analysis/check_rules/dead_assignment_collector.rb
291
291
  - lib/rigor/analysis/check_rules/ivar_write_collector.rb
292
+ - lib/rigor/analysis/check_rules/main_pass_collector.rb
292
293
  - lib/rigor/analysis/check_rules/rule_walk.rb
293
294
  - lib/rigor/analysis/check_rules/self_closedness_scanner.rb
294
295
  - lib/rigor/analysis/check_rules/unreachable_clause_collector.rb
@@ -318,6 +319,7 @@ files:
318
319
  - lib/rigor/analysis/worker_session.rb
319
320
  - lib/rigor/ast.rb
320
321
  - lib/rigor/ast/type_node.rb
322
+ - lib/rigor/bleeding_edge.rb
321
323
  - lib/rigor/builtins/hkt_builtins.rb
322
324
  - lib/rigor/builtins/imported_refinements.rb
323
325
  - lib/rigor/builtins/regex_refinement.rb
@@ -353,6 +355,7 @@ files:
353
355
  - lib/rigor/cli/plugins_renderer.rb
354
356
  - lib/rigor/cli/prism_colorizer.rb
355
357
  - lib/rigor/cli/renderable.rb
358
+ - lib/rigor/cli/show_bleedingedge_command.rb
356
359
  - lib/rigor/cli/sig_gen_command.rb
357
360
  - lib/rigor/cli/skill_command.rb
358
361
  - lib/rigor/cli/trace_command.rb
@@ -386,6 +389,7 @@ files:
386
389
  - lib/rigor/flow_contribution/merger.rb
387
390
  - lib/rigor/inference/acceptance.rb
388
391
  - lib/rigor/inference/block_parameter_binder.rb
392
+ - lib/rigor/inference/body_fixpoint.rb
389
393
  - lib/rigor/inference/budget_trace.rb
390
394
  - lib/rigor/inference/builtins/array_catalog.rb
391
395
  - lib/rigor/inference/builtins/comparable_catalog.rb
@@ -421,6 +425,7 @@ files:
421
425
  - lib/rigor/inference/indexed_narrowing.rb
422
426
  - lib/rigor/inference/macro_block_self_type.rb
423
427
  - lib/rigor/inference/method_dispatcher.rb
428
+ - lib/rigor/inference/method_dispatcher/array_to_h_folding.rb
424
429
  - lib/rigor/inference/method_dispatcher/block_folding.rb
425
430
  - lib/rigor/inference/method_dispatcher/call_context.rb
426
431
  - lib/rigor/inference/method_dispatcher/cgi_folding.rb
@@ -435,6 +440,7 @@ files:
435
440
  - lib/rigor/inference/method_dispatcher/overload_selector.rb
436
441
  - lib/rigor/inference/method_dispatcher/rbs_dispatch.rb
437
442
  - lib/rigor/inference/method_dispatcher/receiver_affinity.rb
443
+ - lib/rigor/inference/method_dispatcher/reduce_folding.rb
438
444
  - lib/rigor/inference/method_dispatcher/regexp_folding.rb
439
445
  - lib/rigor/inference/method_dispatcher/set_folding.rb
440
446
  - lib/rigor/inference/method_dispatcher/shape_dispatch.rb
@@ -489,7 +495,6 @@ files:
489
495
  - lib/rigor/plugin/loader.rb
490
496
  - lib/rigor/plugin/macro.rb
491
497
  - lib/rigor/plugin/macro/block_as_method.rb
492
- - lib/rigor/plugin/macro/external_file.rb
493
498
  - lib/rigor/plugin/macro/heredoc_template.rb
494
499
  - lib/rigor/plugin/macro/nested_class_template.rb
495
500
  - lib/rigor/plugin/macro/trait_registry.rb
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rigor
4
- module Plugin
5
- module Macro
6
- # ADR-16 Tier D declaration: "files matching `glob` are
7
- # analysed as if their body were pasted at a call site whose
8
- # `self` is an instance of `receiver_type` (and whose `@ivar`
9
- # facts come from `bound_ivars`)."
10
- #
11
- # Worked motivating cases (per the per-library survey):
12
- #
13
- # - Redmine's `WebhookPayload#instance_eval(File.read(path), path, 1)`
14
- # at `app/models/webhook_payload.rb:71`. The payload templates
15
- # under `config/webhooks/*.rb` run with `self` typed as
16
- # `Redmine::WebhookPayload` and ivars like `@event` / `@issue`
17
- # / `@user` pre-bound by the caller.
18
- # - tDiary Core's plugin loader pattern — `misc/plugin/*.rb`
19
- # files loaded under `instance_eval` with the tDiary plugin
20
- # instance as `self`.
21
- #
22
- # ## Authoring shape
23
- #
24
- # manifest(
25
- # id: "redmine-webhook-payloads",
26
- # version: "0.1.0",
27
- # external_files: [
28
- # Rigor::Plugin::Macro::ExternalFile.new(
29
- # glob: "config/webhooks/*.rb",
30
- # receiver_type: "Redmine::WebhookPayload",
31
- # bound_ivars: {
32
- # "@event" => "Symbol",
33
- # "@issue" => "Issue?",
34
- # "@user" => "User"
35
- # }
36
- # )
37
- # ]
38
- # )
39
- #
40
- # ## Fields
41
- #
42
- # - `glob` — non-empty String pattern. Interpreted relative
43
- # to the project root (the directory containing `.rigor.yml`)
44
- # at scan time. Slice 5a accepts any non-empty glob
45
- # pattern syntactically; the engine integration (slice 5b)
46
- # pins the resolution rule.
47
- # - `receiver_type` — non-empty String. The class name `self`
48
- # inside the loaded file binds to. Engine integration (slice
49
- # 5b) narrows the file-entry scope's `self_type` to
50
- # `Nominal[receiver_type]`.
51
- # - `bound_ivars` — Hash<String, String>. Each key MUST start
52
- # with `@`; each value is a non-empty type-name String. The
53
- # engine pre-binds these as ivar facts in the file-entry
54
- # scope (slice 5b).
55
- #
56
- # ## Slice 5a scope
57
- #
58
- # **This file ships the value class + manifest hook ONLY.**
59
- # The engine integration that (a) adds matched files to the
60
- # analysis set, (b) narrows the file-entry `self_type`, and
61
- # (c) pre-binds `bound_ivars` as ivar facts is **queued for
62
- # slice 5b**, gated on demonstrated demand. The survey
63
- # identifies only Redmine + tDiary as concrete consumers;
64
- # premature engine work is deferred until those cases (or
65
- # equivalents) materialise as committed plugin targets.
66
- #
67
- # With only this slice landed, plugin authors CAN declare a
68
- # Tier D manifest entry today — the declaration round-trips
69
- # through `Manifest#to_h` (cache-key stable) and is exposed
70
- # on `Manifest#external_files` — but the substrate does not
71
- # yet act on it. The contract is forward-compatible: when
72
- # slice 5b lands, the engine reads the same declarations and
73
- # plugin gems do not need to change.
74
- class ExternalFile
75
- attr_reader :glob, :receiver_type, :bound_ivars
76
-
77
- def initialize(glob:, receiver_type:, bound_ivars: {})
78
- validate_glob!(glob)
79
- validate_receiver_type!(receiver_type)
80
- validate_bound_ivars!(bound_ivars)
81
-
82
- @glob = glob.dup.freeze
83
- @receiver_type = receiver_type.dup.freeze
84
- @bound_ivars = bound_ivars.to_h { |k, v| [k.dup.freeze, v.dup.freeze] }.freeze
85
- freeze
86
- end
87
-
88
- def to_h
89
- {
90
- "glob" => glob,
91
- "receiver_type" => receiver_type,
92
- "bound_ivars" => bound_ivars
93
- }
94
- end
95
-
96
- def ==(other)
97
- other.is_a?(ExternalFile) && to_h == other.to_h
98
- end
99
- alias eql? ==
100
-
101
- def hash
102
- to_h.hash
103
- end
104
-
105
- private
106
-
107
- def validate_glob!(value)
108
- return if value.is_a?(String) && !value.empty?
109
-
110
- raise ArgumentError,
111
- "Plugin::Macro::ExternalFile#glob must be a non-empty String, got #{value.inspect}"
112
- end
113
-
114
- def validate_receiver_type!(value)
115
- return if value.is_a?(String) && !value.empty?
116
-
117
- raise ArgumentError,
118
- "Plugin::Macro::ExternalFile#receiver_type must be a non-empty String, got #{value.inspect}"
119
- end
120
-
121
- def validate_bound_ivars!(value)
122
- unless value.is_a?(Hash)
123
- raise ArgumentError,
124
- "Plugin::Macro::ExternalFile#bound_ivars must be a Hash, got #{value.inspect}"
125
- end
126
-
127
- value.each do |k, v|
128
- unless k.is_a?(String) && k.start_with?("@") && k.length > 1
129
- raise ArgumentError,
130
- "Plugin::Macro::ExternalFile#bound_ivars key must be a String starting with `@`, " \
131
- "got #{k.inspect}"
132
- end
133
- next if v.is_a?(String) && !v.empty?
134
-
135
- raise ArgumentError,
136
- "Plugin::Macro::ExternalFile#bound_ivars value must be a non-empty String, " \
137
- "got #{v.inspect}"
138
- end
139
- end
140
- end
141
- end
142
- end
143
- end