rigortype 0.1.5 → 0.1.6
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 +36 -50
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +74 -12
- data/lib/rigor/configuration.rb +38 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +45 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +106 -9
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +47 -0
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +154 -3
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +125 -11
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +8 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +3 -1
- metadata +54 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a2903b58458e4dafb9acfb05574d615031a79b8b7b521b02029bd30b07ef433
|
|
4
|
+
data.tar.gz: cf13a88ee96fe6a3acb471fb8f8936407b28bae6bd92852e8263b11ec0738870
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5761ae7907222592d4fd8a7c747e24f414c95fc2ce2ce2b0385a8277b0860aaa89c76c2da94b37575f3a79ac6dddbd4fd21fd16f8064ded1aecbb0e5fadb68f7
|
|
7
|
+
data.tar.gz: 8c98d8abd24b9eacef42ba5dbcade9481427a03b61c57306efb273b34ca7b2cbdc73535d3c7495aba90b13051274f9df715a6064f98e9ffb9bdbc995e573560e
|
data/README.md
CHANGED
|
@@ -15,13 +15,32 @@ for any class it can find, and reports a small but trustworthy
|
|
|
15
15
|
catalogue of bugs (undefined methods on typed receivers, wrong
|
|
16
16
|
positional arity, provable `Integer / 0`, …).
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
**Two design commitments drive Rigor.**
|
|
19
|
+
|
|
20
|
+
1. **Types are facts, not wishes.** Hand-written type
|
|
21
|
+
annotations drift from the implementation the moment they
|
|
22
|
+
are written. Rigor infers from the code itself — every
|
|
23
|
+
carrier in its type vocabulary is derived from what your
|
|
24
|
+
source actually produces, not from a signature you authored
|
|
25
|
+
and might forget to update. When you do want RBS in
|
|
26
|
+
`sig/`, [`rigor sig-gen`](docs/adr/14-rbs-sig-generation.md)
|
|
27
|
+
emits it from inference results so the written form starts
|
|
28
|
+
in sync with reality, and `tighter-return` candidates flag
|
|
29
|
+
the cases where an existing `.rbs` is already weaker than
|
|
30
|
+
what the implementation provably returns.
|
|
31
|
+
2. **Programmable inference beyond unions.** A plain union
|
|
32
|
+
(`Integer | nil`) is not the type story Ruby needs. Rigor
|
|
33
|
+
reasons about *what values an expression actually
|
|
34
|
+
produces* — literal values, integer ranges, refinement
|
|
35
|
+
carriers, per-position tuple / hash shapes, bound-method
|
|
36
|
+
bindings — and exposes a plugin extension API plus an
|
|
37
|
+
[ADR-16](docs/adr/16-macro-expansion.md) macro / DSL
|
|
38
|
+
expansion substrate so Rails-shape DSLs are first-class
|
|
39
|
+
type sources rather than analysis blind spots.
|
|
40
|
+
|
|
41
|
+
See **[Beyond `Integer` and `String`](#beyond-integer-and-string-rigors-richer-type-vocabulary)**
|
|
42
|
+
for the full type-model story; the carrier-zoo table is the
|
|
43
|
+
short pitch.
|
|
25
44
|
|
|
26
45
|
When you want tighter types than RBS expresses, refine them
|
|
27
46
|
through the
|
|
@@ -532,7 +551,7 @@ Common knobs the file exposes:
|
|
|
532
551
|
|
|
533
552
|
## Status
|
|
534
553
|
|
|
535
|
-
Current released version: **`v0.1.
|
|
554
|
+
Current released version: **`v0.1.5`**. The analyzer is usable
|
|
536
555
|
on real Ruby code today; the rule catalogue is deliberately
|
|
537
556
|
narrow — Rigor's stance is to surface zero false positives
|
|
538
557
|
while the inference surface stabilises. Forward-looking commitments
|
|
@@ -540,50 +559,19 @@ while the inference surface stabilises. Forward-looking commitments
|
|
|
540
559
|
[`docs/ROADMAP.md`](docs/ROADMAP.md); the release-by-release
|
|
541
560
|
"what shipped" record is [`CHANGELOG.md`](CHANGELOG.md).
|
|
542
561
|
|
|
543
|
-
`v0.1.
|
|
544
|
-
|
|
545
|
-
- **
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
-
|
|
551
|
-
+ per-call-site assertion gating** — `rigor-sorbet` ingests
|
|
552
|
-
Sorbet `sig { ... }` blocks, `T.let` / `T.cast` / `T.must` /
|
|
553
|
-
`T.bind` / `T.assert_type!` / `T.reveal_type` / `T.absurd`,
|
|
554
|
-
and RBI files. Per-call-site `enforce_sigil` gates assertion
|
|
555
|
-
recognisers by the caller file's `# typed:` sigil.
|
|
556
|
-
- **[ADR-13](docs/adr/13-typenode-resolver-plugin.md) plugin
|
|
557
|
-
TypeNode resolver + TypeScript-utility-type adapter** —
|
|
558
|
-
`Plugin::TypeNodeResolver` extension point + five
|
|
559
|
-
Rigor-canonical shape-projection type functions
|
|
560
|
-
(`pick_of` / `omit_of` / `partial_of` / `required_of` /
|
|
561
|
-
`readonly_of`) + the opt-in `rigor-typescript-utility-types`
|
|
562
|
-
plugin mapping TS spellings onto the core functions.
|
|
563
|
-
`Pick[T, :a | :b]` round-trips through the directive grammar.
|
|
564
|
-
- **[ADR-14](docs/adr/14-rbs-sig-generation.md) — `rigor sig-gen`
|
|
565
|
-
CLI** — emits RBS from inference results across five
|
|
566
|
-
classifications (`new-file` / `new-method` / `tighter-return`
|
|
567
|
-
/ `equivalent` / `skipped`); `--params=untyped` default,
|
|
568
|
-
`--params=observed` opt-in via `--observe=PATH`.
|
|
569
|
-
- **`Method` carrier (`Type::BoundMethod`)** —
|
|
570
|
-
`Object#method(:sym).call` / `.()` / `[]` round-trip with
|
|
571
|
-
full precision instead of collapsing to `untyped`.
|
|
572
|
-
- **Rails ecosystem (Tier 1 + Tier 2)** — `rigor-rails-routes`,
|
|
573
|
-
`rigor-rails-i18n`, `rigor-actionmailer`, `rigor-activejob`,
|
|
574
|
-
`rigor-actionpack` (4 phases), `rigor-factorybot`, and
|
|
575
|
-
`rigor-activerecord` publishing `:model_index` via the
|
|
576
|
-
ADR-9 cross-plugin fact channel.
|
|
562
|
+
`v0.1.5` (released 2026-05-16) delivered (full slice list in `CHANGELOG.md` § `[0.1.5]`):
|
|
563
|
+
|
|
564
|
+
- **ADR-15 Ractor migration end-to-end** (Phases 1–4c + 4b.x) — opt-in `rigor check --workers=N` parallelism; pool ≡ sequential proven on 14 real-world projects (31,840 files); spec-suite wall-clock 162s → 27s on 12 cores via `parallel_tests`.
|
|
565
|
+
- **[ADR-16](docs/adr/16-macro-expansion.md) macro / DSL expansion substrate** — four-tier declarative manifest contract (block-as-method, trait-inlining registry, heredoc-template, external-file) with Tier B/C precision promotion and three worked consumer plugins (`rigor-sinatra`, `rigor-devise`, `rigor-dry-struct`). Closes ROADMAP O2 at the WD13 floor.
|
|
566
|
+
- **Real-world Rails / Ruby survey** — fourteen projects swept; opt-in `rigor-activesupport-core-ext` RBS bundle delivers `−75 %` total diagnostics; built-in vendored gem RBS for six native-extension gems (`pg` / `mysql2` / `nokogiri` / `bcrypt` / `redis` / `idn-ruby`); Bundler-aware sig discovery; `RbsLoader#env` failure-memo (~550× speedup on a conflicting sig).
|
|
567
|
+
- **O4 Layer 3 target-project RBS source discovery (slices 1+2+3)** — `Gemfile.lock` parse + bundle-sig filter, `rbs_collection.lock.yaml` awareness, missing-gem `:info` diagnostic.
|
|
568
|
+
- **DEFAULT_LIBRARIES stdlib coverage expansion** — out-of-the-box RBS classes available 1,273 → 1,427 (+154); 31 additional stdlib libraries auto-load.
|
|
569
|
+
- **`is_a?(C)` lexical-nesting constant resolution** — predicate-narrowing now mirrors Ruby's `Module.nesting`-driven lookup.
|
|
577
570
|
|
|
578
571
|
Twenty-four worked plugin examples now ship under
|
|
579
572
|
[`examples/`](examples/) — see
|
|
580
573
|
[`examples/README.md`](examples/README.md) for the comparison
|
|
581
|
-
table.
|
|
582
|
-
pending) also delivered the [ADR-16](docs/adr/16-macro-expansion.md)
|
|
583
|
-
macro / DSL expansion substrate (four-tier declarative
|
|
584
|
-
manifest contract + engine integration + Tier B/C precision
|
|
585
|
-
promotion); see `CHANGELOG.md` `[Unreleased]` for the full
|
|
586
|
-
landing notes.
|
|
574
|
+
table.
|
|
587
575
|
|
|
588
576
|
## Contributing
|
|
589
577
|
|
|
@@ -594,5 +582,3 @@ skill documentation contributors should know about.
|
|
|
594
582
|
## License
|
|
595
583
|
|
|
596
584
|
Mozilla Public License Version 2.0. See [`LICENSE`](LICENSE).
|
|
597
|
-
</content>
|
|
598
|
-
</invoke>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Binds one logical project path (the path the user is editing,
|
|
6
|
+
# e.g. `lib/foo.rb`) to a physical file containing the in-flight
|
|
7
|
+
# buffer bytes (e.g. `/tmp/9539itfeh2.rb`). When the runner /
|
|
8
|
+
# workers / pre-passes need to read source for the logical path,
|
|
9
|
+
# they read from the physical path instead; when they emit a
|
|
10
|
+
# `Diagnostic`, the path is the logical one so editors highlight
|
|
11
|
+
# the buffer the user is actually looking at.
|
|
12
|
+
#
|
|
13
|
+
# See `docs/design/20260516-editor-mode.md` for the design.
|
|
14
|
+
# The CLI surfaces this through paired `--tmp-file` /
|
|
15
|
+
# `--instead-of` flags on `rigor check` and `rigor type-of`;
|
|
16
|
+
# programmatic callers pass a `BufferBinding` to `Runner.new`.
|
|
17
|
+
BufferBinding = Data.define(:logical_path, :physical_path) do
|
|
18
|
+
# Returns the physical path to read bytes from when the caller
|
|
19
|
+
# is about to parse `path`. For non-logical paths returns the
|
|
20
|
+
# input unchanged. Cheap to call on every path; the binding is
|
|
21
|
+
# singular today (one buffer per run).
|
|
22
|
+
def resolve(path)
|
|
23
|
+
path == logical_path ? physical_path : path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the path the caller should report in user-facing
|
|
27
|
+
# output (diagnostics, run stats) when it currently holds the
|
|
28
|
+
# physical path. The inverse of `#resolve`. Non-physical paths
|
|
29
|
+
# pass through unchanged, so it is safe to stamp every
|
|
30
|
+
# outgoing path through this helper.
|
|
31
|
+
def display_path(path)
|
|
32
|
+
path == physical_path ? logical_path : path
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -944,8 +944,18 @@ module Rigor
|
|
|
944
944
|
# / Constant / Tuple / HashShape; the wrapper exists so
|
|
945
945
|
# the ivar rule can extend the envelope (or apply
|
|
946
946
|
# different filters) without disturbing the call rules.
|
|
947
|
+
#
|
|
948
|
+
# `TrueClass` / `FalseClass` are both normalised to
|
|
949
|
+
# `"bool"` here so the common boolean-flag idiom
|
|
950
|
+
# (`@loaded = false` in `initialize` then `@loaded = true`
|
|
951
|
+
# on first work) doesn't fire the mismatch rule. A real
|
|
952
|
+
# `bool → String` drift still trips because the second
|
|
953
|
+
# write's `ivar_class_for` returns `"String"`.
|
|
947
954
|
def ivar_class_for(type)
|
|
948
|
-
concrete_class_name(type)
|
|
955
|
+
name = concrete_class_name(type)
|
|
956
|
+
return "bool" if %w[TrueClass FalseClass].include?(name)
|
|
957
|
+
|
|
958
|
+
name
|
|
949
959
|
end
|
|
950
960
|
|
|
951
961
|
def build_always_truthy_condition_diagnostic(path, predicate_node, polarity)
|
|
@@ -54,7 +54,7 @@ module Rigor
|
|
|
54
54
|
)
|
|
55
55
|
@resolved_gems = resolved_gems.freeze
|
|
56
56
|
@unresolvable = unresolvable.freeze
|
|
57
|
-
@method_catalog = method_catalog.freeze
|
|
57
|
+
@method_catalog = normalize_catalog(method_catalog).freeze
|
|
58
58
|
@budget_exceeded = budget_exceeded.freeze
|
|
59
59
|
@class_to_gem = class_to_gem.freeze
|
|
60
60
|
@budget_overrun_strategy = budget_overrun_strategy
|
|
@@ -62,6 +62,19 @@ module Rigor
|
|
|
62
62
|
freeze
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Accepts both the post-heuristic `CatalogEntry` shape
|
|
66
|
+
# (the Walker's current output) and the legacy bare-Symbol
|
|
67
|
+
# `:instance` / `:singleton` shape (older tests + earlier
|
|
68
|
+
# in-tree callers). Symbol values are wrapped into
|
|
69
|
+
# `CatalogEntry(kind: value, return_type: nil)` so the
|
|
70
|
+
# downstream dispatcher always sees a single shape.
|
|
71
|
+
def normalize_catalog(catalog)
|
|
72
|
+
catalog.transform_values do |value|
|
|
73
|
+
value.is_a?(Symbol) ? Walker::CatalogEntry.new(kind: value) : value
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
private :normalize_catalog
|
|
77
|
+
|
|
65
78
|
# @return [String, nil] the gem that owns `class_name`
|
|
66
79
|
# (first-write-wins); `nil` when the class isn't in
|
|
67
80
|
# any opt-in gem's catalog.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../type"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module DependencySourceInference
|
|
10
|
+
# Walker enhancement (ADR-10 § "Open questions"): pulls a
|
|
11
|
+
# **heuristic** return type out of a method body's tail
|
|
12
|
+
# expression. The heuristic is intentionally narrow — only
|
|
13
|
+
# the trivially-decidable shapes contribute. Everything
|
|
14
|
+
# else returns `nil`, which the dispatcher consumes as
|
|
15
|
+
# "fall back to `Dynamic[top]`" (the pre-enhancement
|
|
16
|
+
# behaviour).
|
|
17
|
+
#
|
|
18
|
+
# The contract is a strict floor:
|
|
19
|
+
#
|
|
20
|
+
# - Last statement is a literal scalar (Integer / Float /
|
|
21
|
+
# Symbol / true / false / nil) → `Constant<value>`.
|
|
22
|
+
# - Last statement is a String literal → `Nominal[String]`
|
|
23
|
+
# (NOT `Constant<"x">`; a String literal is mutable under
|
|
24
|
+
# `# frozen_string_literal: false`, so the analyzer cannot
|
|
25
|
+
# claim object identity).
|
|
26
|
+
# - Last statement is an Array / Hash literal →
|
|
27
|
+
# `Nominal[Array]` / `Nominal[Hash]` (element-type
|
|
28
|
+
# inference stays deferred — too expensive for the
|
|
29
|
+
# heuristic tier).
|
|
30
|
+
# - Last statement is `self` → `nil` (the caller's receiver
|
|
31
|
+
# nominal would be more accurate but the walker doesn't
|
|
32
|
+
# carry the receiver context; deferred).
|
|
33
|
+
# - Anything else → `nil`.
|
|
34
|
+
#
|
|
35
|
+
# The dispatcher wraps the returned type in `Dynamic[T]`
|
|
36
|
+
# before returning to the user per ADR-10's `Dynamic`-origin
|
|
37
|
+
# contract. The wrapping is the dispatcher's responsibility,
|
|
38
|
+
# not the heuristic's.
|
|
39
|
+
module ReturnTypeHeuristic
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @param def_node [Prism::DefNode]
|
|
43
|
+
# @return [Rigor::Type, nil] heuristic return type, or
|
|
44
|
+
# `nil` when the body's tail expression doesn't match
|
|
45
|
+
# any of the recognised shapes.
|
|
46
|
+
def extract(def_node)
|
|
47
|
+
body = def_node.body
|
|
48
|
+
return nil if body.nil?
|
|
49
|
+
|
|
50
|
+
tail = tail_expression(body)
|
|
51
|
+
literal_return_type(tail)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extracts the last evaluated expression of the method
|
|
55
|
+
# body. A `Prism::DefNode`'s body is either a
|
|
56
|
+
# `StatementsNode` (multi-statement body) or a single
|
|
57
|
+
# expression node directly. We dig past `BeginNode` /
|
|
58
|
+
# rescue wrappers to the protected body's tail.
|
|
59
|
+
def tail_expression(node)
|
|
60
|
+
case node
|
|
61
|
+
when Prism::StatementsNode
|
|
62
|
+
tail_expression(node.body.last) unless node.body.empty?
|
|
63
|
+
when Prism::BeginNode
|
|
64
|
+
tail_expression(node.statements) if node.statements
|
|
65
|
+
when nil
|
|
66
|
+
nil
|
|
67
|
+
else
|
|
68
|
+
node
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :tail_expression
|
|
72
|
+
|
|
73
|
+
# The per-shape heuristic. Per the module docstring,
|
|
74
|
+
# immutable scalar literals fold to `Constant<value>`;
|
|
75
|
+
# mutable container literals (String, Array, Hash) fold
|
|
76
|
+
# to the appropriate Nominal; everything else returns
|
|
77
|
+
# nil.
|
|
78
|
+
def literal_return_type(node)
|
|
79
|
+
case node
|
|
80
|
+
when Prism::IntegerNode, Prism::FloatNode then Type::Combinator.constant_of(node.value)
|
|
81
|
+
when Prism::SymbolNode then symbol_constant(node)
|
|
82
|
+
when Prism::TrueNode then Type::Combinator.constant_of(true)
|
|
83
|
+
when Prism::FalseNode then Type::Combinator.constant_of(false)
|
|
84
|
+
when Prism::NilNode then Type::Combinator.constant_of(nil)
|
|
85
|
+
when Prism::StringNode then Type::Combinator.nominal_of("String")
|
|
86
|
+
when Prism::ArrayNode then Type::Combinator.nominal_of("Array")
|
|
87
|
+
when Prism::HashNode then Type::Combinator.nominal_of("Hash")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
private_class_method :literal_return_type
|
|
91
|
+
|
|
92
|
+
# `Prism::SymbolNode#value` returns the Symbol's name as
|
|
93
|
+
# a String. We `.to_sym` it for the Constant carrier so
|
|
94
|
+
# `:foo` is `Constant<:foo>`, not `Constant<"foo">`.
|
|
95
|
+
def symbol_constant(node)
|
|
96
|
+
value = node.value
|
|
97
|
+
return nil if value.nil?
|
|
98
|
+
|
|
99
|
+
Type::Combinator.constant_of(value.to_sym)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :symbol_constant
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "return_type_heuristic"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Analysis
|
|
7
9
|
module DependencySourceInference
|
|
8
10
|
# Walks a resolved gem's `roots:` and collects the
|
|
9
|
-
# `(class_name, method_name) →
|
|
10
|
-
# method catalog. The walker is the source
|
|
11
|
-
# dispatcher tier
|
|
11
|
+
# `(class_name, method_name) → CatalogEntry(kind,
|
|
12
|
+
# return_type)` method catalog. The walker is the source
|
|
13
|
+
# of facts the dispatcher tier consults to recognise a
|
|
12
14
|
# method as defined by an opt-in gem and contribute a
|
|
13
|
-
# `Type::Dynamic
|
|
15
|
+
# `Type::Dynamic`-wrapped return at the call site.
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
17
|
+
# The dispatcher tier wraps every walker-contributed return
|
|
18
|
+
# in `Dynamic[T]` per ADR-10's gem-boundary contract. When
|
|
19
|
+
# the heuristic ({ReturnTypeHeuristic}) recognises the
|
|
20
|
+
# method body's tail expression, the dispatcher uses the
|
|
21
|
+
# heuristic's static facet; otherwise it falls back to
|
|
22
|
+
# `Dynamic[top]` (the pre-heuristic behaviour). The
|
|
23
|
+
# heuristic is intentionally narrow — only literal-tail
|
|
24
|
+
# method bodies fold; everything else degrades silently.
|
|
22
25
|
#
|
|
23
26
|
# Hard exclusions are NOT user-configurable, per ADR-10
|
|
24
27
|
# § "Hard exclusions": top-level `spec/`, `test/`, `bin/`,
|
|
@@ -45,6 +48,19 @@ module Rigor
|
|
|
45
48
|
def truncated? = truncated
|
|
46
49
|
end
|
|
47
50
|
|
|
51
|
+
# Per-method catalog entry. `kind` is `:instance` or
|
|
52
|
+
# `:singleton`; `return_type` is the
|
|
53
|
+
# {ReturnTypeHeuristic}-extracted static facet (a
|
|
54
|
+
# `Rigor::Type::*`) or `nil` when the heuristic declined.
|
|
55
|
+
# The dispatcher wraps a non-nil `return_type` in
|
|
56
|
+
# `Dynamic[T]`; a `nil` `return_type` falls back to
|
|
57
|
+
# `Dynamic[top]`.
|
|
58
|
+
class CatalogEntry < Data.define(:kind, :return_type)
|
|
59
|
+
def initialize(kind:, return_type: nil)
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
48
64
|
# Sentinel for "no cap" — used by callers that don't
|
|
49
65
|
# care about the budget (specs, tooling). Production
|
|
50
66
|
# code MUST pass an integer.
|
|
@@ -175,7 +191,11 @@ module Rigor
|
|
|
175
191
|
|
|
176
192
|
class_name = qualified_prefix.join("::")
|
|
177
193
|
kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
|
|
178
|
-
|
|
194
|
+
key = [class_name, node.name]
|
|
195
|
+
return if accumulator.key?(key) # first walk wins
|
|
196
|
+
|
|
197
|
+
return_type = ReturnTypeHeuristic.extract(node)
|
|
198
|
+
accumulator[key] = CatalogEntry.new(kind: kind, return_type: return_type)
|
|
179
199
|
end
|
|
180
200
|
|
|
181
201
|
# Resolves a `Prism::ConstantPathNode` /
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Frozen snapshot of the project-wide state {Runner} consumes
|
|
6
|
+
# before per-file analysis fires: the loaded plugin registry
|
|
7
|
+
# (with `#prepare` already invoked), the dependency-source
|
|
8
|
+
# index, the synthetic-method and project-patched indexes
|
|
9
|
+
# produced by the pre-pass scanners, and the diagnostics
|
|
10
|
+
# those passes emitted.
|
|
11
|
+
#
|
|
12
|
+
# Owners (`Rigor::LanguageServer::ProjectContext`, future
|
|
13
|
+
# editor / sig-gen integrations) build a ProjectScan once
|
|
14
|
+
# per project-state generation via
|
|
15
|
+
# `Runner#prepare_project_scan` and pass it to
|
|
16
|
+
# `Runner.new(prebuilt: ...)` so per-buffer publishes skip
|
|
17
|
+
# the scanner walks and `#prepare` re-runs. When watched
|
|
18
|
+
# project files change, the owner discards the ProjectScan
|
|
19
|
+
# and a fresh one builds on next read.
|
|
20
|
+
#
|
|
21
|
+
# Editor mode v1 contract reminder: scanners observe the
|
|
22
|
+
# bytes that were on disk at scan time, NOT the in-flight
|
|
23
|
+
# buffer. Edits to a file that itself declares synthetic
|
|
24
|
+
# methods (or `pre_eval:`-listed patches) are NOT visible
|
|
25
|
+
# until the owner invalidates the scan — typically via
|
|
26
|
+
# `workspace/didChangeWatchedFiles`. This is the same
|
|
27
|
+
# trade-off the LSP made when slice 7 cached only the
|
|
28
|
+
# `Environment`; extending the cache to the pre-pass
|
|
29
|
+
# outputs preserves the contract.
|
|
30
|
+
ProjectScan = Data.define(
|
|
31
|
+
:plugin_registry,
|
|
32
|
+
:dependency_source_index,
|
|
33
|
+
:synthetic_method_index,
|
|
34
|
+
:project_patched_methods,
|
|
35
|
+
:plugin_prepare_diagnostics,
|
|
36
|
+
:pre_eval_diagnostics
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|