rigortype 0.1.2 → 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.
- checksums.yaml +4 -4
- data/README.md +135 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
- data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +206 -6
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +59 -6
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +235 -0
- data/lib/rigor/configuration.rb +45 -11
- data/lib/rigor/environment.rb +47 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +7 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +233 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +70 -6
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +49 -7
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +6 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +58 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8142401ce01630e0adcc3e1d59dedfc0a123ae247617b7bf91d33cbffe4cb193
|
|
4
|
+
data.tar.gz: ca747bb44214c0bf7dca8203f1c2fcf84657b8a639dbdc5448ede1f32b3f48ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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}
|
|
@@ -239,6 +252,16 @@ Rigor consults, in order:
|
|
|
239
252
|
Rigor walks `def` / `define_method` / `attr_*` /
|
|
240
253
|
`Data.define(*Symbol)` so user-defined methods on a class
|
|
241
254
|
are recognised.
|
|
255
|
+
5. **Opt-in gem-source inference (ADR-10).** Gems listed
|
|
256
|
+
under `dependencies.source_inference:` in `.rigor.yml`
|
|
257
|
+
have their `lib/` walked the same way project source is,
|
|
258
|
+
so methods on those gems' classes resolve even without
|
|
259
|
+
RBS. Inferred returns crossing the gem boundary are
|
|
260
|
+
wrapped in `Dynamic[T]` so the call site retains the
|
|
261
|
+
provenance — RBS / RBS::Inline / generated stubs / plugin
|
|
262
|
+
contracts always win on conflict. Default behaviour is
|
|
263
|
+
unchanged: gems not listed stay at the
|
|
264
|
+
RBS-or-`Dynamic[Top]` boundary.
|
|
242
265
|
|
|
243
266
|
If a type cannot be proved, the engine returns `Dynamic[Top]`
|
|
244
267
|
(Rigor's gradual carrier) and stays silent — Rigor never invents
|
|
@@ -343,6 +366,10 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
|
|
|
343
366
|
optional policies, per-element block fold over
|
|
344
367
|
`map`, `select`, `filter_map`, `flat_map`, `find` /
|
|
345
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[...]`.
|
|
346
373
|
- **Constant folding** — aggressive arithmetic / string /
|
|
347
374
|
Symbol / Tuple-shaped `divmod` folding, cartesian fold over
|
|
348
375
|
`Union[Constant…]`, integer-range arithmetic
|
|
@@ -360,26 +387,48 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
|
|
|
360
387
|
- **Refinement carriers** — `Type::Difference`,
|
|
361
388
|
`Type::Refined`, `Type::Intersection` provide the
|
|
362
389
|
imported-built-in catalogue end-to-end through
|
|
363
|
-
`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.
|
|
364
399
|
- **`RBS::Extended` directive routes** — `return:`, `param:`
|
|
365
400
|
(call-site + body-side), `assert:` /
|
|
366
401
|
`predicate-if-(true|false)` accept refinement payloads, and
|
|
367
402
|
roll up into a single `Rigor::FlowContribution` bundle per
|
|
368
403
|
method (the v0.1.0 plugin contribution merger reads bundles
|
|
369
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).
|
|
370
412
|
|
|
371
413
|
The full per-release surface lives in
|
|
372
414
|
[`CHANGELOG.md`](CHANGELOG.md). The internal contracts the
|
|
373
415
|
analyzer guarantees live under
|
|
374
416
|
[`docs/internal-spec/`](docs/internal-spec/).
|
|
375
417
|
|
|
376
|
-
## Plugins
|
|
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.
|
|
377
429
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
[`examples/`](examples/) — each is a fully-shaped plugin gem
|
|
381
|
-
with a runnable demo and an end-to-end integration spec, and
|
|
382
|
-
each spotlights a different facet of the plugin contract:
|
|
430
|
+
**Plugin-contract teaching examples** (focus on a single
|
|
431
|
+
extension-point):
|
|
383
432
|
|
|
384
433
|
- [`rigor-deprecations`](examples/rigor-deprecations/) —
|
|
385
434
|
smallest possible plugin (~80 lines); config-driven rules.
|
|
@@ -393,21 +442,45 @@ each spotlights a different facet of the plugin contract:
|
|
|
393
442
|
- [`rigor-units`](examples/rigor-units/) — local-variable flow
|
|
394
443
|
tracking through arithmetic.
|
|
395
444
|
- [`rigor-routes`](examples/rigor-routes/) — `Plugin::IoBoundary`
|
|
396
|
-
reads under `TrustPolicy` plus cache producers
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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.
|
|
403
471
|
|
|
404
472
|
[`examples/README.md`](examples/README.md) is the plugin
|
|
405
473
|
authoring landing page — comparison table, recommended reading
|
|
406
474
|
order, and the architectural map of which surface each example
|
|
407
475
|
exercises. The binding contract for the plugin API lives in
|
|
408
|
-
[`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md)
|
|
409
|
-
|
|
410
|
-
[`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).
|
|
411
484
|
|
|
412
485
|
## Configuration
|
|
413
486
|
|
|
@@ -437,22 +510,53 @@ Common knobs the file exposes:
|
|
|
437
510
|
|
|
438
511
|
## Status
|
|
439
512
|
|
|
440
|
-
Current released version: **`v0.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
[`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
|
|
446
518
|
detail lives in [`CHANGELOG.md`](CHANGELOG.md).
|
|
447
519
|
|
|
448
|
-
`v0.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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.
|
|
456
560
|
|
|
457
561
|
## Contributing
|
|
458
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:)
|
|
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)
|
|
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/
|
|
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/
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gem_resolver"
|
|
4
|
+
require_relative "index"
|
|
5
|
+
require_relative "walker"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module DependencySourceInference
|
|
10
|
+
# Folds a `Configuration::Dependencies` value into a
|
|
11
|
+
# frozen {Index}. Resolves each non-disabled entry through
|
|
12
|
+
# {GemResolver}, walks each resolved gem's `roots:` via
|
|
13
|
+
# {Walker.walk} under the configured `budget_per_gem` cap,
|
|
14
|
+
# and aggregates the per-gem method catalogs into the
|
|
15
|
+
# Index's flat `(class_name, method_name) → kind` table.
|
|
16
|
+
#
|
|
17
|
+
# Entries with `mode: :disabled` are skipped without
|
|
18
|
+
# resolution attempts so users can "list and disable" a
|
|
19
|
+
# gem in configuration without provoking a missing-gem
|
|
20
|
+
# diagnostic.
|
|
21
|
+
module Builder
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param dependencies [Rigor::Configuration::Dependencies]
|
|
25
|
+
# @return [Index]
|
|
26
|
+
def build(dependencies)
|
|
27
|
+
return Index::EMPTY if dependencies.empty?
|
|
28
|
+
|
|
29
|
+
state = BuildState.new
|
|
30
|
+
dependencies.source_inference.each do |entry|
|
|
31
|
+
next if entry.disabled?
|
|
32
|
+
|
|
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)
|
|
57
|
+
case outcome
|
|
58
|
+
when GemResolver::Resolved
|
|
59
|
+
absorb_resolved(outcome, budget, builder)
|
|
60
|
+
when GemResolver::Unresolvable
|
|
61
|
+
@unresolvable << outcome
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
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
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ADR-10 5b — per-class reverse-lookup table (β budget
|
|
86
|
+
# semantics). Records `class_name → gem_name` for every
|
|
87
|
+
# class observed in the gem's catalog. First-write-wins:
|
|
88
|
+
# if two opt-in gems re-open the same class, the first
|
|
89
|
+
# gem to harvest the class owns it in the reverse index.
|
|
90
|
+
# The dispatcher only consults this map when the
|
|
91
|
+
# `budget_overrun_strategy` is `:dependency_silence`,
|
|
92
|
+
# so the storage cost is never paid back unless the
|
|
93
|
+
# user opts in.
|
|
94
|
+
def record_class_to_gem(catalog, gem_name, class_to_gem)
|
|
95
|
+
catalog.each_key do |(class_name, _method_name)|
|
|
96
|
+
class_to_gem[class_name] ||= gem_name
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Per-resolved-gem walk. Isolated so a single gem's
|
|
101
|
+
# filesystem error / parse failure cannot abort the
|
|
102
|
+
# build; the walker swallows its own per-file errors,
|
|
103
|
+
# and a top-level raise here degrades the gem to "no
|
|
104
|
+
# contributions" without touching the rest of the run.
|
|
105
|
+
def walker_outcome_for(resolved, budget)
|
|
106
|
+
Walker.walk(gem_dir: resolved.gem_dir, roots: resolved.roots, budget: budget)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
Walker::Outcome.new(catalog: {}.freeze, truncated: false)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
module DependencySourceInference
|
|
6
|
+
# Maps a `Configuration::Dependencies::Entry` to the gem's
|
|
7
|
+
# on-disk installation directory by consulting RubyGems
|
|
8
|
+
# (`Gem.loaded_specs` first, falling back to
|
|
9
|
+
# `Gem::Specification.find_by_name`). Returns either a
|
|
10
|
+
# frozen {Resolved} value object or an {Unresolvable} value
|
|
11
|
+
# describing why the gem cannot participate in this run.
|
|
12
|
+
#
|
|
13
|
+
# Resolution failures are surfaced as
|
|
14
|
+
# `dynamic.dependency-source.gem-not-found` diagnostics by
|
|
15
|
+
# {Analysis::Runner} rather than crashing the run, so a
|
|
16
|
+
# missing gem in `dependencies.source_inference` degrades
|
|
17
|
+
# cleanly to "no contributions from that gem" — every other
|
|
18
|
+
# gem and the project source remain unaffected.
|
|
19
|
+
module GemResolver
|
|
20
|
+
# Successful resolution. `version` is the spec version as
|
|
21
|
+
# a String so it round-trips into cache descriptors
|
|
22
|
+
# (slice 3) without leaking a `Gem::Version` instance
|
|
23
|
+
# through public surfaces.
|
|
24
|
+
class Resolved < Data.define(:gem_name, :version, :gem_dir, :mode, :roots)
|
|
25
|
+
def descriptor_key
|
|
26
|
+
[gem_name, version, mode].freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Unresolvable reasons. `:not_in_bundle` covers both the
|
|
31
|
+
# "RubyGems doesn't know this gem" case and the
|
|
32
|
+
# `LoadError`-style raise from `find_by_name`. Future
|
|
33
|
+
# reasons (`:c_extension_only`, `:no_lib_root`) are
|
|
34
|
+
# introduced as the walker discovers them in slice 2b.
|
|
35
|
+
Unresolvable = Data.define(:gem_name, :reason)
|
|
36
|
+
|
|
37
|
+
VALID_REASONS = %i[not_in_bundle].freeze
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# @param entry [Rigor::Configuration::Dependencies::Entry]
|
|
42
|
+
# @return [Resolved, Unresolvable]
|
|
43
|
+
def resolve(entry)
|
|
44
|
+
spec = locate_gem_spec(entry.gem)
|
|
45
|
+
return Unresolvable.new(gem_name: entry.gem, reason: :not_in_bundle) if spec.nil?
|
|
46
|
+
|
|
47
|
+
Resolved.new(
|
|
48
|
+
gem_name: entry.gem,
|
|
49
|
+
version: spec.version.to_s,
|
|
50
|
+
gem_dir: spec.full_gem_path, # rigor:disable undefined-method
|
|
51
|
+
mode: entry.mode,
|
|
52
|
+
roots: entry.roots
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Locator. `Gem.loaded_specs` reflects the bundle (cheap
|
|
57
|
+
# lookup, no filesystem walk); `find_by_name` is the
|
|
58
|
+
# broader fallback for gems present on the gem path but
|
|
59
|
+
# not yet `require`'d. `Gem::MissingSpecError` is a
|
|
60
|
+
# `LoadError` subclass, so the rescue covers both
|
|
61
|
+
# missing-spec and load-error signals.
|
|
62
|
+
def locate_gem_spec(name)
|
|
63
|
+
Gem.loaded_specs[name] || begin
|
|
64
|
+
Gem::Specification.find_by_name(name)
|
|
65
|
+
rescue LoadError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|