rigortype 0.1.3 → 0.1.4

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +3 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +146 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +30 -9
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +5 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +52 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3542aa2842a06783d02f8f35b7f31247eb8d10dce91214b31364839ad7087809
4
- data.tar.gz: 6beca5ec330dab3264cb64a110bd4e6a790e16a92c5f1cf10383799b988d2bb9
3
+ metadata.gz: 8142401ce01630e0adcc3e1d59dedfc0a123ae247617b7bf91d33cbffe4cb193
4
+ data.tar.gz: ca747bb44214c0bf7dca8203f1c2fcf84657b8a639dbdc5448ede1f32b3f48ae
5
5
  SHA512:
6
- metadata.gz: 33d98371534cd4d193b39afe08dc5b442cd90e0909388a9e9a20413d8ae766665d8bb7a8573bb2236997a72ce178e35c7d2c62c64ceac804f1a31bf88b079950
7
- data.tar.gz: 0fca38a07730117fa58b42d8c14642e86540a492ce0513c4612dbf58049600bb8adc224fc32cdfc518ba581a870c478b8dfbbc02ce43bf78eb251c3d1dd771db
6
+ metadata.gz: 02ef30047bcbf17ee716634315664522449610396c0645cf2e9f289fab7b1bc5667ac60a3cbe4069b59c7435bc1ad2ff9142f35f525df019b439ef74ad58191b
7
+ data.tar.gz: 6e6fe48ffce033b46a1f44be6b2eff62c3e11733f6fd5a583a17e631e18a752d3b77464783da88e9c9f5f0ee345440ca5c751d05e3d77d5253334f3a988bbcf4
data/README.md CHANGED
@@ -79,6 +79,12 @@ bundle exec rigor type-of lib/foo.rb:10:5
79
79
  # Report Scope#type_of coverage across a tree (handy when
80
80
  # diagnosing why a particular call site reads as `untyped`).
81
81
  bundle exec rigor type-scan lib
82
+
83
+ # Emit RBS skeletons from inference results — review with
84
+ # `--diff`, write to `sig/` with `--write`. ADR-14 sig-gen.
85
+ bundle exec rigor sig-gen --print lib/foo.rb
86
+ bundle exec rigor sig-gen --diff lib/foo.rb
87
+ bundle exec rigor sig-gen --write lib/foo.rb
82
88
  ```
83
89
 
84
90
  ### Sample output
@@ -145,6 +151,7 @@ out of the box, without you writing a single annotation.
145
151
  | **Intersection** (`Type::Intersection`) | Composition of multiple refinements | `non-empty-lowercase-string = non-empty-string ∩ lowercase-string` |
146
152
  | **Tuple / HashShape** | Heterogeneous arrays / known-key hashes that carry per-position / per-key types | `[1, "two", :three]` types as `Tuple[Constant<1>, Constant<"two">, Constant<:three>]`; `{name: "Alice", age: 30}` as `HashShape{name: Constant<"Alice">, age: Constant<30>}` |
147
153
  | **Union** (`Type::Union`) | "One of these literal values" — finite enums Rigor can enumerate | `Constant<:zero> \| Constant<:small> \| Constant<:large>` |
154
+ | **`Method` binding** (`Type::BoundMethod`) | The receiver / method-name pair `Object#method(:sym)` produces, so `.call` / `.()` / `[]` recover the precise backing dispatch | `"1".method(:to_i).call` resolves to `Constant<1>` instead of `untyped` |
148
155
  | **`Dynamic[T]`** | The gradual carrier — wraps a static facet with a "could be anything" admission | `Dynamic[Top]` is the conservative fallback Rigor uses when it cannot prove a narrower type |
149
156
 
150
157
  Each refinement / range / literal carrier **erases to its base
@@ -188,6 +195,12 @@ label = case n
188
195
  label # Constant<:zero> | Constant<:small>
189
196
  # | Constant<:large>
190
197
 
198
+ # Method bindings keep their receiver — `.method(:sym).call`
199
+ # round-trips through the original dispatch.
200
+ [:to_i, :to_f, :to_sym].map { |m| "1".method(m).call }
201
+ # Tuple[Constant<1>, Constant<1.0>, Constant<:"1">]
202
+ # — per-element fold + BoundMethod backward fold
203
+
191
204
  # RBS::Extended directives let you tighten beyond what RBS expresses.
192
205
  class Slug
193
206
  %a{rigor:v1:return: non-empty-string}
@@ -353,6 +366,10 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
353
366
  optional policies, per-element block fold over
354
367
  `map`, `select`, `filter_map`, `flat_map`, `find` /
355
368
  `find_index`, `count`, `any?` / `all?` / `none?`, `zip`.
369
+ `&:symbol` block-pass on these methods is treated as
370
+ `{ |x| x.symbol }` and dispatches against the element type
371
+ so `Hash#transform_values(&:freeze)` returns `Hash[K, V]`
372
+ instead of `Enumerator[...]`.
356
373
  - **Constant folding** — aggressive arithmetic / string /
357
374
  Symbol / Tuple-shaped `divmod` folding, cartesian fold over
358
375
  `Union[Constant…]`, integer-range arithmetic
@@ -370,26 +387,48 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
370
387
  - **Refinement carriers** — `Type::Difference`,
371
388
  `Type::Refined`, `Type::Intersection` provide the
372
389
  imported-built-in catalogue end-to-end through
373
- `Builtins::ImportedRefinements`.
390
+ `Builtins::ImportedRefinements`. The parser accepts Symbol
391
+ / String literals and `|`-unions at type-arg position
392
+ (`pick_of[Shape, :a | :b]`, `Pick[T, "name" | "email"]`).
393
+ - **`Method` carrier (`Type::BoundMethod`)** —
394
+ `Object#method(:sym)` lifts into a binding carrier so
395
+ `.call` / `.()` / `[]` recover the precise dispatch
396
+ (`"1".method(:to_i).call` resolves to `Constant<1>`).
397
+ Reflective Method members (`#owner` / `#name` / `#arity`)
398
+ still resolve via the Method RBS sig.
374
399
  - **`RBS::Extended` directive routes** — `return:`, `param:`
375
400
  (call-site + body-side), `assert:` /
376
401
  `predicate-if-(true|false)` accept refinement payloads, and
377
402
  roll up into a single `Rigor::FlowContribution` bundle per
378
403
  method (the v0.1.0 plugin contribution merger reads bundles
379
404
  directly).
405
+ - **Opt-in gem-source inference (ADR-10)** — gems listed under
406
+ `dependencies.source_inference:` have their `lib/` walked.
407
+ Per-gem budget, per-gem-version cache slice,
408
+ `dynamic.dependency-source.*` diagnostic family covering
409
+ gem-not-found / budget-exceeded / config-conflict /
410
+ boundary-cross (the last surfaces RBS+gem-source overlap
411
+ on `mode: :full` gems for audit).
380
412
 
381
413
  The full per-release surface lives in
382
414
  [`CHANGELOG.md`](CHANGELOG.md). The internal contracts the
383
415
  analyzer guarantees live under
384
416
  [`docs/internal-spec/`](docs/internal-spec/).
385
417
 
386
- ## Plugins (v0.1.0)
418
+ ## Plugins
419
+
420
+ `v0.1.0` introduced the extension API; `v0.1.x` rounds it out
421
+ with the [ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin
422
+ fact channel (one plugin publishes a fact like `:model_index`,
423
+ another consumes it), [ADR-11](docs/adr/11-sorbet-input-adapter.md)
424
+ Sorbet ingestion, and [ADR-13](docs/adr/13-typenode-resolver-plugin.md)
425
+ plugin-supplied type-vocabulary resolvers. **Nineteen worked
426
+ examples** ship under [`examples/`](examples/) — each is a
427
+ fully-shaped plugin gem with a runnable demo and an end-to-end
428
+ integration spec.
387
429
 
388
- `v0.1.0` adds an extension API so projects can teach Rigor about
389
- their own DSLs. Seven worked examples ship under
390
- [`examples/`](examples/) — each is a fully-shaped plugin gem
391
- with a runnable demo and an end-to-end integration spec, and
392
- each spotlights a different facet of the plugin contract:
430
+ **Plugin-contract teaching examples** (focus on a single
431
+ extension-point):
393
432
 
394
433
  - [`rigor-deprecations`](examples/rigor-deprecations/) —
395
434
  smallest possible plugin (~80 lines); config-driven rules.
@@ -403,21 +442,45 @@ each spotlights a different facet of the plugin contract:
403
442
  - [`rigor-units`](examples/rigor-units/) — local-variable flow
404
443
  tracking through arithmetic.
405
444
  - [`rigor-routes`](examples/rigor-routes/) — `Plugin::IoBoundary`
406
- reads under `TrustPolicy` plus cache producers (slice 2 +
407
- slice 6).
408
- - [`rigor-activerecord`](examples/rigor-activerecord/)
409
- validates `Model.find` / `.find_by` / `.where` against
410
- `db/schema.rb` and discovered AR model classes; combines
411
- DSL interpretation, multi-file `IoBoundary`, and chained
412
- cache producers the most architecturally complete example.
445
+ reads under `TrustPolicy` plus cache producers.
446
+ - [`rigor-typescript-utility-types`](examples/rigor-typescript-utility-types/)
447
+ `Plugin::TypeNodeResolver` chain wiring TS-canonical names
448
+ (`Pick` / `Omit` / `Partial` / `Required` / `Readonly`) onto
449
+ Rigor's shape-projection type functions.
450
+
451
+ **Rails ecosystem plugins** (Tier 1 + Tier 2 + Tier 3 + Sorbet):
452
+
453
+ - Tier 1: [`rigor-rails-routes`](examples/rigor-rails-routes/),
454
+ [`rigor-rails-i18n`](examples/rigor-rails-i18n/),
455
+ [`rigor-actionmailer`](examples/rigor-actionmailer/),
456
+ [`rigor-activejob`](examples/rigor-activejob/).
457
+ - Tier 2: [`rigor-actionpack`](examples/rigor-actionpack/)
458
+ (4 phases — routes / filters / renders / strong-params),
459
+ [`rigor-factorybot`](examples/rigor-factorybot/),
460
+ [`rigor-activerecord`](examples/rigor-activerecord/) —
461
+ publishes `:model_index` via ADR-9 for the other two
462
+ to consume.
463
+ - Tier 3: [`rigor-pundit`](examples/rigor-pundit/),
464
+ [`rigor-sidekiq`](examples/rigor-sidekiq/),
465
+ [`rigor-rspec`](examples/rigor-rspec/),
466
+ [`rigor-actioncable`](examples/rigor-actioncable/).
467
+ - Parallel: [`rigor-sorbet`](examples/rigor-sorbet/) — ingests
468
+ Sorbet `sig` / `T.let` / `T.cast` / `T.must` / `T.bind` /
469
+ `T.assert_type!` / `T.reveal_type` / `T.absurd` and RBI
470
+ files as type sources.
413
471
 
414
472
  [`examples/README.md`](examples/README.md) is the plugin
415
473
  authoring landing page — comparison table, recommended reading
416
474
  order, and the architectural map of which surface each example
417
475
  exercises. The binding contract for the plugin API lives in
418
- [`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md)
419
- and the slice-by-slice normative specs under
420
- [`docs/internal-spec/plugin*.md`](docs/internal-spec/).
476
+ [`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md);
477
+ the slice-by-slice normative specs are under
478
+ [`docs/internal-spec/plugin*.md`](docs/internal-spec/); the
479
+ sibling ADRs that extend it ride the same surface
480
+ ([ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin facts,
481
+ [ADR-11](docs/adr/11-sorbet-input-adapter.md) Sorbet adapter,
482
+ [ADR-13](docs/adr/13-typenode-resolver-plugin.md) TypeNode
483
+ resolver).
421
484
 
422
485
  ## Configuration
423
486
 
@@ -447,22 +510,53 @@ Common knobs the file exposes:
447
510
 
448
511
  ## Status
449
512
 
450
- Current released version: **`v0.0.8`** (the eighth preview).
451
- The analyzer is usable on real Ruby code today but the rule
452
- catalogue is deliberately narrow — Rigor's stance is to surface
453
- zero false positives while the inference surface stabilises.
454
- The roadmap is tracked in
455
- [`docs/MILESTONES.md`](docs/MILESTONES.md); release-by-release
513
+ Current released version: **`v0.1.2`**. The analyzer is usable
514
+ on real Ruby code today; the rule catalogue is deliberately
515
+ narrow — Rigor's stance is to surface zero false positives
516
+ while the inference surface stabilises. The roadmap is tracked
517
+ in [`docs/MILESTONES.md`](docs/MILESTONES.md); release-by-release
456
518
  detail lives in [`CHANGELOG.md`](CHANGELOG.md).
457
519
 
458
- `v0.0.9` is the active development cluster on `master` and
459
- covers the persistent cache infrastructure (`.rigor/cache/`,
460
- `--cache-stats`, `--clear-cache`, `--no-cache`),
461
- paired-complement Refined narrowing, `literal-string` flow
462
- tracking, the `Rigor::FlowContribution` bundle struct, and
463
- six additional built-in catalogues (Random, Struct, Encoding,
464
- Regexp + MatchData, Proc / Method / UnboundMethod, Exception).
465
- The next release after `0.0.9` will be `0.1.0`.
520
+ `v0.1.4` is the active development cluster on `master` and
521
+ delivers:
522
+
523
+ - **[ADR-10](docs/adr/10-dependency-source-inference.md) closed
524
+ end-to-end** opt-in gem-source inference, per-gem budget,
525
+ cache slice, and the `dynamic.dependency-source.boundary-cross`
526
+ `:info` diagnostic that surfaces RBS / gem-source overlap
527
+ under `mode: :full`.
528
+ - **[ADR-11](docs/adr/11-sorbet-input-adapter.md) primary surface
529
+ + per-call-site assertion gating** — `rigor-sorbet` ingests
530
+ Sorbet `sig { ... }` blocks, `T.let` / `T.cast` / `T.must` /
531
+ `T.bind` / `T.assert_type!` / `T.reveal_type` / `T.absurd`,
532
+ and RBI files. Per-call-site `enforce_sigil` gates assertion
533
+ recognisers by the caller file's `# typed:` sigil.
534
+ - **[ADR-13](docs/adr/13-typenode-resolver-plugin.md) plugin
535
+ TypeNode resolver + TypeScript-utility-type adapter** —
536
+ `Plugin::TypeNodeResolver` extension point + five
537
+ Rigor-canonical shape-projection type functions
538
+ (`pick_of` / `omit_of` / `partial_of` / `required_of` /
539
+ `readonly_of`) + the opt-in `rigor-typescript-utility-types`
540
+ plugin mapping TS spellings onto the core functions.
541
+ `Pick[T, :a | :b]` round-trips through the directive grammar.
542
+ - **[ADR-14](docs/adr/14-rbs-sig-generation.md) — `rigor sig-gen`
543
+ CLI** — emits RBS from inference results across five
544
+ classifications (`new-file` / `new-method` / `tighter-return`
545
+ / `equivalent` / `skipped`); `--params=untyped` default,
546
+ `--params=observed` opt-in via `--observe=PATH`.
547
+ - **`Method` carrier (`Type::BoundMethod`)** —
548
+ `Object#method(:sym).call` / `.()` / `[]` round-trip with
549
+ full precision instead of collapsing to `untyped`.
550
+ - **Rails ecosystem (Tier 1 + Tier 2)** — `rigor-rails-routes`,
551
+ `rigor-rails-i18n`, `rigor-actionmailer`, `rigor-activejob`,
552
+ `rigor-actionpack` (4 phases), `rigor-factorybot`, and
553
+ `rigor-activerecord` publishing `:model_index` via the
554
+ ADR-9 cross-plugin fact channel.
555
+
556
+ Nineteen worked plugin examples now ship under
557
+ [`examples/`](examples/) — see
558
+ [`examples/README.md`](examples/README.md) for the comparison
559
+ table.
466
560
 
467
561
  ## Contributing
468
562
 
@@ -262,7 +262,7 @@ module Rigor
262
262
  # errors, internal analyzer errors) are NEVER
263
263
  # suppressed — they represent failures the user cannot
264
264
  # silence away.
265
- def filter_suppressed(diagnostics, comments:, disabled_rules:) # rubocop:disable Metrics/CyclomaticComplexity
265
+ def filter_suppressed(diagnostics, comments:, disabled_rules:)
266
266
  line_suppressions, file_suppressions = parse_suppression_comments(comments)
267
267
  disabled = expand_rule_tokens(disabled_rules)
268
268
 
@@ -329,7 +329,7 @@ module Rigor
329
329
  class << self
330
330
  private
331
331
 
332
- def undefined_method_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
332
+ def undefined_method_diagnostic(path, call_node, scope_index)
333
333
  return nil if call_node.receiver.nil?
334
334
 
335
335
  scope = scope_index[call_node]
@@ -429,7 +429,7 @@ module Rigor
429
429
  # by `undefined_method_diagnostic`; it returns nil
430
430
  # when the call's receiver / RBS coverage / call shape
431
431
  # disqualifies the rule.
432
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
432
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
433
433
  def wrong_arity_diagnostic(path, call_node, scope_index)
434
434
  return nil if call_node.receiver.nil?
435
435
  return nil unless plain_positional_call?(call_node)
@@ -459,7 +459,7 @@ module Rigor
459
459
 
460
460
  build_arity_diagnostic(path, call_node, class_name, min, max, actual)
461
461
  end
462
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
462
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
463
463
 
464
464
  def plain_positional_call?(call_node)
465
465
  arguments = call_node.arguments
@@ -534,7 +534,7 @@ module Rigor
534
534
  # and union receivers where every member already
535
535
  # disqualifies the call (avoid duplicating the
536
536
  # undefined-method diagnostic).
537
- def nil_receiver_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
537
+ def nil_receiver_diagnostic(path, call_node, scope_index)
538
538
  return nil if call_node.receiver.nil?
539
539
  # Safe-navigation calls (`recv&.method`) already
540
540
  # short-circuit on nil at runtime, so a nil-bearing
@@ -615,7 +615,7 @@ module Rigor
615
615
  # The diagnostic does NOT count toward `Result#error_count`
616
616
  # so a fixture peppered with `dump_type` calls still
617
617
  # passes `rigor check`.
618
- def dump_type_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity
618
+ def dump_type_diagnostic(path, call_node, scope_index)
619
619
  return nil unless rigor_testing_call?(call_node, :dump_type)
620
620
  return nil if call_node.arguments.nil? || call_node.arguments.arguments.empty?
621
621
 
@@ -644,7 +644,7 @@ module Rigor
644
644
  # is emitted; matching calls produce no output. This
645
645
  # lets a fixture document its expected types inline:
646
646
  # subsequent `rigor check` runs flag any drift.
647
- def assert_type_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
647
+ def assert_type_diagnostic(path, call_node, scope_index)
648
648
  return nil unless rigor_testing_call?(call_node, :assert_type)
649
649
  return nil if call_node.arguments.nil? || call_node.arguments.arguments.size < 2
650
650
 
@@ -972,7 +972,6 @@ module Rigor
972
972
  )
973
973
  end
974
974
 
975
- # rubocop:disable Metrics/ParameterLists
976
975
  def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
977
976
  location = node.name_loc || node.location
978
977
  Diagnostic.new(
@@ -985,7 +984,6 @@ module Rigor
985
984
  severity: :error
986
985
  )
987
986
  end
988
- # rubocop:enable Metrics/ParameterLists
989
987
 
990
988
  # Returns the dead-branch node for a literal-predicate
991
989
  # if/unless, or nil when no observable branch is dead.
@@ -1034,7 +1032,6 @@ module Rigor
1034
1032
  # (no splat / kw / block-pass / forwarded).
1035
1033
  # - Per-argument: skip when EITHER side is `Dynamic`
1036
1034
  # (the call cannot be statically refuted).
1037
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1038
1035
  def argument_type_diagnostic(path, call_node, scope_index)
1039
1036
  return nil if call_node.receiver.nil?
1040
1037
  return nil unless plain_positional_call?(call_node)
@@ -1059,15 +1056,13 @@ module Rigor
1059
1056
  return nil if method_def.nil? || method_def == true
1060
1057
  return nil unless method_def.method_types.size == 1
1061
1058
 
1062
- param_overrides = Rigor::RbsExtended.param_type_override_map(method_def)
1059
+ param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
1063
1060
  mismatch = first_argument_mismatch(method_def.method_types.first, call_node, scope, param_overrides)
1064
1061
  return nil if mismatch.nil?
1065
1062
 
1066
1063
  build_argument_type_diagnostic(path, call_node, class_name, mismatch)
1067
1064
  end
1068
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1069
1065
 
1070
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1071
1066
  def first_argument_mismatch(method_type, call_node, scope, param_overrides)
1072
1067
  function = method_type.type
1073
1068
  return nil unless argument_check_eligible?(function)
@@ -1094,7 +1089,6 @@ module Rigor
1094
1089
  end
1095
1090
  nil
1096
1091
  end
1097
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1098
1092
 
1099
1093
  def argument_check_eligible?(function)
1100
1094
  # See `arity_eligible?`: `UntypedFunction` lacks
@@ -1132,7 +1126,6 @@ module Rigor
1132
1126
  )
1133
1127
  end
1134
1128
 
1135
- # rubocop:disable Metrics/ParameterLists
1136
1129
  def build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1137
1130
  location = call_node.message_loc || call_node.location
1138
1131
  range = min == max ? min.to_s : "#{min}..#{max}"
@@ -1147,7 +1140,6 @@ module Rigor
1147
1140
  severity: :error
1148
1141
  )
1149
1142
  end
1150
- # rubocop:enable Metrics/ParameterLists
1151
1143
 
1152
1144
  def build_undefined_method_diagnostic(path, call_node, receiver_type)
1153
1145
  location = call_node.message_loc || call_node.location
@@ -1186,7 +1178,7 @@ module Rigor
1186
1178
  # :maybe → emit at :warning. Promoted to :error
1187
1179
  # under `severity_profile: strict` per
1188
1180
  # ADR-8 § "Severity profile".
1189
- def return_type_mismatch_diagnostic(path, def_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1181
+ def return_type_mismatch_diagnostic(path, def_node, scope_index)
1190
1182
  return nil if def_node.body.nil?
1191
1183
 
1192
1184
  last_expr = body_last_expression(def_node.body)
@@ -1255,7 +1247,7 @@ module Rigor
1255
1247
  end
1256
1248
  return nil if method_def.nil?
1257
1249
 
1258
- override = Rigor::RbsExtended.read_return_type_override(method_def)
1250
+ override = Rigor::RbsExtended.read_return_type_override(method_def, environment: scope.environment)
1259
1251
  return override if override
1260
1252
 
1261
1253
  declared_return_union(method_def, scope.environment)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ module DependencySourceInference
6
+ # ADR-10 slice 5c — per-run accumulator for the
7
+ # `dynamic.dependency-source.boundary-cross` `:info`
8
+ # diagnostic.
9
+ #
10
+ # The diagnostic fires when both an authoritative source
11
+ # (RBS today; plugins later) AND a `mode: :full` opt-in
12
+ # gem's source catalog resolve the same `(class_name,
13
+ # method_name)`. The dispatcher takes the authoritative
14
+ # source's answer (per ADR-10's tier order), but records
15
+ # the boundary crossing so the user can audit whether RBS
16
+ # and the gem source have drifted.
17
+ #
18
+ # The accumulator deduplicates per `(class_name,
19
+ # method_name, gem_name)` so a method called from many
20
+ # files yields one diagnostic.
21
+ #
22
+ # Used in the same pattern as
23
+ # {Rigor::RbsExtended::Reporter}: the dispatcher writes
24
+ # events into the per-run instance; {Rigor::Analysis::Runner}
25
+ # drains it at end-of-run into a flat
26
+ # `Rigor::Analysis::Diagnostic` list.
27
+ class BoundaryCrossReporter
28
+ Entry = Data.define(:class_name, :method_name, :gem_name, :rbs_display)
29
+
30
+ def initialize
31
+ @entries = []
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ # @return [Array<Entry>] frozen snapshot of the recorded
36
+ # boundary-cross events. Each entry is a Data with
37
+ # `class_name` (String), `method_name` (Symbol),
38
+ # `gem_name` (String), and `rbs_display` (String —
39
+ # the authoritative-side type's human-facing form,
40
+ # embedded into the diagnostic message).
41
+ def entries
42
+ @mutex.synchronize { @entries.dup.freeze }
43
+ end
44
+
45
+ def empty?
46
+ @mutex.synchronize { @entries.empty? }
47
+ end
48
+
49
+ # Records one boundary-cross event. Deduplicates on the
50
+ # `(class_name, method_name, gem_name)` triple — the
51
+ # diagnostic per-receiver-per-method-per-owning-gem is
52
+ # the actionable unit.
53
+ def record(class_name:, method_name:, gem_name:, rbs_display:)
54
+ entry = Entry.new(
55
+ class_name: class_name, method_name: method_name,
56
+ gem_name: gem_name, rbs_display: rbs_display
57
+ )
58
+ @mutex.synchronize do
59
+ return if @entries.any? { |existing| same_key?(existing, entry) }
60
+
61
+ @entries << entry
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def same_key?(existing, entry)
68
+ existing.class_name == entry.class_name &&
69
+ existing.method_name == entry.method_name &&
70
+ existing.gem_name == entry.gem_name
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -23,37 +23,63 @@ module Rigor
23
23
 
24
24
  # @param dependencies [Rigor::Configuration::Dependencies]
25
25
  # @return [Index]
26
- def build(dependencies) # rubocop:disable Metrics/MethodLength
26
+ def build(dependencies)
27
27
  return Index::EMPTY if dependencies.empty?
28
28
 
29
- resolved = []
30
- unresolvable = []
31
- catalog = {}
32
- class_to_gem = {}
33
- budget_exceeded = []
34
- budget = dependencies.budget_per_gem
35
-
29
+ state = BuildState.new
36
30
  dependencies.source_inference.each do |entry|
37
31
  next if entry.disabled?
38
32
 
39
- outcome = GemResolver.resolve(entry)
33
+ state.absorb(GemResolver.resolve(entry), dependencies.budget_per_gem, self)
34
+ end
35
+
36
+ state.to_index(dependencies)
37
+ end
38
+
39
+ # Per-build mutable accumulator. The original inline
40
+ # variables (`resolved` / `unresolvable` / `catalog` /
41
+ # `class_to_gem` / `budget_exceeded` / `gem_modes`)
42
+ # pushed the method past the AbcSize budget; the
43
+ # struct collects the same fields, narrows
44
+ # `absorb`'s branching, and yields one
45
+ # `Index.new(...)` call from `to_index`.
46
+ class BuildState
47
+ def initialize
48
+ @resolved = []
49
+ @unresolvable = []
50
+ @catalog = {}
51
+ @class_to_gem = {}
52
+ @budget_exceeded = []
53
+ @gem_modes = {}
54
+ end
55
+
56
+ def absorb(outcome, budget, builder)
40
57
  case outcome
41
58
  when GemResolver::Resolved
42
- resolved << outcome
43
- walked = walker_outcome_for(outcome, budget)
44
- catalog.merge!(walked.catalog)
45
- record_class_to_gem(walked.catalog, outcome.gem_name, class_to_gem)
46
- budget_exceeded << outcome.gem_name if walked.truncated?
47
- when GemResolver::Unresolvable then unresolvable << outcome
59
+ absorb_resolved(outcome, budget, builder)
60
+ when GemResolver::Unresolvable
61
+ @unresolvable << outcome
48
62
  end
49
63
  end
50
64
 
51
- Index.new(
52
- resolved_gems: resolved, unresolvable: unresolvable,
53
- method_catalog: catalog, budget_exceeded: budget_exceeded,
54
- class_to_gem: class_to_gem,
55
- budget_overrun_strategy: dependencies.budget_overrun_strategy
56
- )
65
+ def absorb_resolved(resolved, budget, builder)
66
+ @resolved << resolved
67
+ @gem_modes[resolved.gem_name] = resolved.mode
68
+ walked = builder.walker_outcome_for(resolved, budget)
69
+ @catalog.merge!(walked.catalog)
70
+ builder.record_class_to_gem(walked.catalog, resolved.gem_name, @class_to_gem)
71
+ @budget_exceeded << resolved.gem_name if walked.truncated?
72
+ end
73
+
74
+ def to_index(dependencies)
75
+ Index.new(
76
+ resolved_gems: @resolved, unresolvable: @unresolvable,
77
+ method_catalog: @catalog, budget_exceeded: @budget_exceeded,
78
+ class_to_gem: @class_to_gem,
79
+ budget_overrun_strategy: dependencies.budget_overrun_strategy,
80
+ gem_modes: @gem_modes
81
+ )
82
+ end
57
83
  end
58
84
 
59
85
  # ADR-10 5b — per-class reverse-lookup table (β budget
@@ -21,7 +21,7 @@ module Rigor
21
21
  # a String so it round-trips into cache descriptors
22
22
  # (slice 3) without leaking a `Gem::Version` instance
23
23
  # through public surfaces.
24
- Resolved = Data.define(:gem_name, :version, :gem_dir, :mode, :roots) do
24
+ class Resolved < Data.define(:gem_name, :version, :gem_dir, :mode, :roots)
25
25
  def descriptor_key
26
26
  [gem_name, version, mode].freeze
27
27
  end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # by walking the resolved gems' `roots:`.
17
17
  class Index
18
18
  attr_reader :resolved_gems, :unresolvable, :method_catalog, :budget_exceeded,
19
- :class_to_gem, :budget_overrun_strategy
19
+ :class_to_gem, :budget_overrun_strategy, :gem_modes
20
20
 
21
21
  # @param method_catalog [Hash{[String, Symbol] => Symbol}]
22
22
  # the flat `(class_name, method_name) → :instance | :singleton`
@@ -38,10 +38,19 @@ module Rigor
38
38
  # budget-exceeded gem's classes degrade to
39
39
  # `Dynamic[top]` instead of falling through to the
40
40
  # user-class fallback.
41
- def initialize( # rubocop:disable Metrics/ParameterLists
41
+ # @param gem_modes [Hash<String, Symbol>] per-gem mode
42
+ # table (`gem_name → :disabled | :when_missing |
43
+ # :full`). ADR-10 slice 5c consults this through
44
+ # {#mode_for} to identify call sites where gem-source
45
+ # and RBS both contribute under `mode: :full`. The map
46
+ # is keyed on `gem_name` (not class) because re-opened
47
+ # classes belong to the first gem they appeared in
48
+ # per `class_to_gem`; `mode_for(class_name)` chains
49
+ # the two lookups.
50
+ def initialize(
42
51
  resolved_gems: [], unresolvable: [], method_catalog: {},
43
52
  budget_exceeded: [], class_to_gem: {},
44
- budget_overrun_strategy: :walker_cap
53
+ budget_overrun_strategy: :walker_cap, gem_modes: {}
45
54
  )
46
55
  @resolved_gems = resolved_gems.freeze
47
56
  @unresolvable = unresolvable.freeze
@@ -49,6 +58,7 @@ module Rigor
49
58
  @budget_exceeded = budget_exceeded.freeze
50
59
  @class_to_gem = class_to_gem.freeze
51
60
  @budget_overrun_strategy = budget_overrun_strategy
61
+ @gem_modes = gem_modes.freeze
52
62
  freeze
53
63
  end
54
64
 
@@ -59,6 +69,25 @@ module Rigor
59
69
  @class_to_gem[class_name]
60
70
  end
61
71
 
72
+ # ADR-10 slice 5c — per-class mode lookup. Chains
73
+ # `class_to_gem` + `gem_modes`; returns `nil` when the
74
+ # class isn't owned by any opt-in gem in this run.
75
+ def mode_for(class_name)
76
+ gem_name = @class_to_gem[class_name]
77
+ return nil if gem_name.nil?
78
+
79
+ @gem_modes[gem_name]
80
+ end
81
+
82
+ # ADR-10 slice 5c — true when the receiver class belongs
83
+ # to a gem the user opted into `mode: :full` for. The
84
+ # dispatcher consults this AFTER an authoritative-source
85
+ # (RBS / plugin) dispatch resolves so it can record the
86
+ # boundary-crossing for audit.
87
+ def full_mode?(class_name)
88
+ mode_for(class_name) == :full
89
+ end
90
+
62
91
  # Looks up the recorded method kind for a
63
92
  # `(class_name, method_name)` pair. Returns `:instance`
64
93
  # / `:singleton` when the walker observed a definition
@@ -41,7 +41,7 @@ module Rigor
41
41
  # this per-gem so the Runner can surface a single
42
42
  # `dynamic.dependency-source.budget-exceeded` warning
43
43
  # naming the affected gem(s).
44
- Outcome = Data.define(:catalog, :truncated) do
44
+ class Outcome < Data.define(:catalog, :truncated)
45
45
  def truncated? = truncated
46
46
  end
47
47
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "dependency_source_inference/boundary_cross_reporter"
3
4
  require_relative "dependency_source_inference/gem_resolver"
4
5
  require_relative "dependency_source_inference/index"
5
6
  require_relative "dependency_source_inference/walker"
@@ -24,10 +24,8 @@ module Rigor
24
24
  # ADR-2 § "Plugin Diagnostic Provenance") let consumers
25
25
  # distinguish where a diagnostic originated without committing
26
26
  # to the plugin API itself.
27
- # rubocop:disable Metrics/ParameterLists
28
27
  def initialize(path:, line:, column:, message:, severity: :error, rule: nil,
29
28
  source_family: DEFAULT_SOURCE_FAMILY)
30
- # rubocop:enable Metrics/ParameterLists
31
29
  @path = path
32
30
  @line = line
33
31
  @column = column