rigortype 0.1.4 → 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 +69 -56
- 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/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +59 -2
- 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/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- 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.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
- 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/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -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/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +11 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +37 -2
- metadata +92 -1
|
@@ -9,7 +9,7 @@ module Rigor
|
|
|
9
9
|
# (`decimal-int-string`, `hex-int-string`, `octal-int-string`,
|
|
10
10
|
# `lowercase-string`, `uppercase-string`, `numeric-string`).
|
|
11
11
|
# See `docs/type-specification/imported-built-in-types.md` for
|
|
12
|
-
# the registry the refinements come from and `docs/
|
|
12
|
+
# the registry the refinements come from and `docs/ROADMAP.md`
|
|
13
13
|
# § "v0.1.1 — Planned" Track 1 slice 1 for the binding scope of
|
|
14
14
|
# this recogniser.
|
|
15
15
|
#
|
|
@@ -47,17 +47,22 @@ module Rigor
|
|
|
47
47
|
QUANTIFIER_SOURCE = '(?:\+|\{\d+(?:,\d+)?\})'
|
|
48
48
|
private_constant :QUANTIFIER_SOURCE
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
|
|
51
|
+
# because the outer Array contains two-element `[Regexp, Symbol]`
|
|
52
|
+
# rows whose inner Arrays are not frozen by the outer freeze.
|
|
53
|
+
# A worker Ractor iterating `RULES.find { ... }` would trip
|
|
54
|
+
# `Ractor::IsolationError` on the first row access.
|
|
55
|
+
RULES = Ractor.make_shareable([
|
|
56
|
+
[/\A\\d#{QUANTIFIER_SOURCE}\z/, :decimal_int_string],
|
|
57
|
+
[/\A\\h#{QUANTIFIER_SOURCE}\z/, :hex_int_string],
|
|
58
|
+
[/\A\[0-9a-fA-F\]#{QUANTIFIER_SOURCE}\z/, :hex_int_string],
|
|
59
|
+
[/\A\[0-9a-f\]#{QUANTIFIER_SOURCE}\z/, :hex_int_string],
|
|
60
|
+
[/\A\[0-9A-F\]#{QUANTIFIER_SOURCE}\z/, :hex_int_string],
|
|
61
|
+
[/\A\[0-7\]#{QUANTIFIER_SOURCE}\z/, :octal_int_string],
|
|
62
|
+
[/\A\[a-z\]#{QUANTIFIER_SOURCE}\z/, :lowercase_string],
|
|
63
|
+
[/\A\[A-Z\]#{QUANTIFIER_SOURCE}\z/, :uppercase_string],
|
|
64
|
+
[/\A\[\[:digit:\]\]#{QUANTIFIER_SOURCE}\z/, :numeric_string]
|
|
65
|
+
])
|
|
61
66
|
private_constant :RULES
|
|
62
67
|
|
|
63
68
|
BOUND_RE = /\{(\d+)(?:,(\d+))?\}\z/
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Builtins
|
|
7
|
+
# Static return-type refinements for stdlib (or other built-in)
|
|
8
|
+
# methods whose upstream RBS signature is broader than the
|
|
9
|
+
# method's documented behaviour and where adding a
|
|
10
|
+
# `%a{rigor:v1:return: ...}` annotation upstream is impractical
|
|
11
|
+
# (the RBS lives in the vendored `ruby/rbs` submodule).
|
|
12
|
+
#
|
|
13
|
+
# This tier sits in `MethodDispatcher.dispatch` between the
|
|
14
|
+
# HKT-builtin tier (which handles parametric / shape-bearing
|
|
15
|
+
# returns like `JSON.parse`'s `json::value`) and the RBS
|
|
16
|
+
# dispatch tier (the canonical lookup). It is consulted only
|
|
17
|
+
# when the method name and arg shape match an entry in the
|
|
18
|
+
# table below, so the standard RBS path stays in charge of
|
|
19
|
+
# every other call.
|
|
20
|
+
#
|
|
21
|
+
# Match policy:
|
|
22
|
+
#
|
|
23
|
+
# - Entries are keyed by `(owner_class_name, method_name,
|
|
24
|
+
# kind)`. `owner_class_name` is the class that *defines*
|
|
25
|
+
# the method (e.g., `"Kernel"`), not necessarily the
|
|
26
|
+
# receiver's static class — Kernel methods are mixed into
|
|
27
|
+
# every non-BasicObject class, so a `__dir__` call on any
|
|
28
|
+
# receiver routes here.
|
|
29
|
+
# - `kind: :both` matches both the singleton-receiver
|
|
30
|
+
# shape (`Kernel.__dir__`, `Singleton[Kernel]` receiver)
|
|
31
|
+
# AND the instance-receiver shape (an implicit-self call
|
|
32
|
+
# like `__dir__` inside any class body, or `obj.__dir__`
|
|
33
|
+
# on an instance).
|
|
34
|
+
# - `kind: :singleton` / `kind: :instance` restrict the
|
|
35
|
+
# match to one of the two shapes.
|
|
36
|
+
# - The handler is called with `(arg_types)` so future
|
|
37
|
+
# entries can refine based on argument types (e.g. a
|
|
38
|
+
# `File.expand_path(string)` entry that returns
|
|
39
|
+
# `non-empty-string` regardless of the upstream return).
|
|
40
|
+
#
|
|
41
|
+
# The override fires ABOVE RBS dispatch — if RBS would have
|
|
42
|
+
# returned a wider type (`String?` for `Kernel#__dir__`), the
|
|
43
|
+
# override returns the refined union (`non-empty-string | nil`)
|
|
44
|
+
# instead. RBS erasure of the refined return goes back to the
|
|
45
|
+
# original upstream shape, so downstream RBS-shaped observers
|
|
46
|
+
# see no difference.
|
|
47
|
+
module StaticReturnRefinements
|
|
48
|
+
# Pre-built carrier reused across calls so structural
|
|
49
|
+
# equality matches across analyzer invocations.
|
|
50
|
+
NON_EMPTY_STRING_OR_NIL = Type::Combinator.union(
|
|
51
|
+
Type::Combinator.non_empty_string,
|
|
52
|
+
Type::Combinator.constant_of(nil)
|
|
53
|
+
).freeze
|
|
54
|
+
private_constant :NON_EMPTY_STRING_OR_NIL
|
|
55
|
+
|
|
56
|
+
# `Kernel#__dir__` returns the canonical directory of the
|
|
57
|
+
# source file the call appears in, or `nil` when the file
|
|
58
|
+
# is invalid / not available (typically `-e` and similar
|
|
59
|
+
# one-liner contexts). When non-nil the value is always a
|
|
60
|
+
# filesystem-canonical path — never the empty string — so
|
|
61
|
+
# `non-empty-string` is exact.
|
|
62
|
+
KERNEL_DIR = ->(_arg_types) { NON_EMPTY_STRING_OR_NIL }
|
|
63
|
+
private_constant :KERNEL_DIR
|
|
64
|
+
|
|
65
|
+
# Frozen ((owner_class_name, method_name, kind) => handler)
|
|
66
|
+
# table. The kind tag is `:both`, `:singleton`, or
|
|
67
|
+
# `:instance`. New entries SHOULD prefer `:both` unless the
|
|
68
|
+
# singleton- and instance-side shapes genuinely differ.
|
|
69
|
+
OVERRIDES = {
|
|
70
|
+
["Kernel", :__dir__, :both] => KERNEL_DIR
|
|
71
|
+
}.freeze
|
|
72
|
+
private_constant :OVERRIDES
|
|
73
|
+
|
|
74
|
+
# Looks up a refined return type for the given call.
|
|
75
|
+
#
|
|
76
|
+
# @param owner_class_name [String, nil] the class on which
|
|
77
|
+
# the method is defined (e.g., `"Kernel"`). Pass `nil`
|
|
78
|
+
# when the caller hasn't resolved a defining owner yet —
|
|
79
|
+
# the lookup will then fall back to matching by
|
|
80
|
+
# `(method_name, kind)` against entries whose owner is
|
|
81
|
+
# currently in the table.
|
|
82
|
+
# @param method_name [Symbol]
|
|
83
|
+
# @param kind [Symbol] one of `:singleton`, `:instance`. The
|
|
84
|
+
# caller passes the shape of the actual call site; the
|
|
85
|
+
# table stores `:both` for entries that match either.
|
|
86
|
+
# @param arg_types [Array<Rigor::Type>] positional argument
|
|
87
|
+
# types. Forwarded to the handler so future entries can
|
|
88
|
+
# discriminate on argument shape.
|
|
89
|
+
# @return [Rigor::Type, nil] the refined return type, or
|
|
90
|
+
# `nil` when no override matches.
|
|
91
|
+
def self.lookup(owner_class_name:, method_name:, kind:, arg_types: [])
|
|
92
|
+
return nil if owner_class_name.nil?
|
|
93
|
+
|
|
94
|
+
method_sym = method_name.to_sym
|
|
95
|
+
handler = OVERRIDES[[owner_class_name, method_sym, :both]] ||
|
|
96
|
+
OVERRIDES[[owner_class_name, method_sym, kind]]
|
|
97
|
+
handler&.call(arg_types)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Indexed view by `(method_name, kind)` — used by the
|
|
101
|
+
# dispatcher when the receiver's owner is not yet resolved
|
|
102
|
+
# but the method name alone uniquely identifies a stdlib
|
|
103
|
+
# override (today: `__dir__` → Kernel). The table is small
|
|
104
|
+
# and the index rebuild cost trivial, but precomputing keeps
|
|
105
|
+
# `dispatch`'s hot path free of an O(n) scan.
|
|
106
|
+
OWNERS_BY_METHOD = OVERRIDES.each_with_object({}) do |((owner, mname, _kind), _h), acc|
|
|
107
|
+
acc[mname] ||= []
|
|
108
|
+
acc[mname] << owner unless acc[mname].include?(owner)
|
|
109
|
+
end.freeze
|
|
110
|
+
private_constant :OWNERS_BY_METHOD
|
|
111
|
+
|
|
112
|
+
# @return [Array<String>] the candidate owner class names
|
|
113
|
+
# for a bare method-name lookup. Empty when no override
|
|
114
|
+
# names this method.
|
|
115
|
+
def self.owners_for(method_name)
|
|
116
|
+
OWNERS_BY_METHOD[method_name.to_sym] || []
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -28,7 +28,9 @@ module Rigor
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def self.file_entries(loader)
|
|
31
|
-
loader.signature_paths
|
|
31
|
+
roots = loader.signature_paths +
|
|
32
|
+
Rigor::Environment::RbsLoader.vendored_gem_sig_paths
|
|
33
|
+
roots.flat_map do |root|
|
|
32
34
|
next [] unless root.directory?
|
|
33
35
|
|
|
34
36
|
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
|
+
require "monitor"
|
|
6
7
|
require "securerandom"
|
|
7
8
|
|
|
8
9
|
require_relative "descriptor"
|
|
@@ -21,7 +22,7 @@ module Rigor
|
|
|
21
22
|
# next write replaces the bad entry. The trailing SHA-256 catches
|
|
22
23
|
# accidental corruption (partial writes, FS errors); it is **not**
|
|
23
24
|
# a security boundary, per ADR-2's trusted-gem trust model.
|
|
24
|
-
class Store
|
|
25
|
+
class Store # rubocop:disable Metrics/ClassLength
|
|
25
26
|
# Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
|
|
26
27
|
# format version. Bumped on incompatible on-disk format changes
|
|
27
28
|
# (independent of {Descriptor::SCHEMA_VERSION}, which covers
|
|
@@ -30,16 +31,55 @@ module Rigor
|
|
|
30
31
|
|
|
31
32
|
VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
# @param root [String] cache root directory.
|
|
35
|
+
# @param read_only [Boolean] when true, every disk-side
|
|
36
|
+
# side-effect is suppressed: `fetch_or_compute` still
|
|
37
|
+
# reads existing entries (hits) and still runs the
|
|
38
|
+
# producer block on miss, but it does NOT write the
|
|
39
|
+
# produced value to disk, does NOT update the
|
|
40
|
+
# `schema_version.txt` marker, and does NOT touch the
|
|
41
|
+
# on-disk root directory. The in-process memo is still
|
|
42
|
+
# populated so repeated lookups within the same run stay
|
|
43
|
+
# cheap. Used by editor mode so multiple buffer-mode
|
|
44
|
+
# invocations can read from the same cache concurrently
|
|
45
|
+
# without churning it. See
|
|
46
|
+
# `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
47
|
+
def initialize(root:, read_only: false)
|
|
34
48
|
@root = root.to_s.dup.freeze
|
|
49
|
+
@read_only = read_only
|
|
35
50
|
@hits = 0
|
|
36
51
|
@misses = 0
|
|
37
52
|
@writes = 0
|
|
38
53
|
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
54
|
+
# Process-level in-memory layer keyed by
|
|
55
|
+
# `(producer_id, cache_key)`. Avoids the disk read +
|
|
56
|
+
# `Marshal.load` cost (the dominant share of repeated
|
|
57
|
+
# cache-hit calls per stackprof) when many short-lived
|
|
58
|
+
# `Analysis::Runner` instances share one `Store` — the
|
|
59
|
+
# spec process, the LSP daemon's repeated re-check
|
|
60
|
+
# path, and any other "many runs, same project" loop.
|
|
61
|
+
# Keys are content-derived (descriptor digests), so
|
|
62
|
+
# cross-fixture contamination is impossible.
|
|
63
|
+
@memo = {}
|
|
64
|
+
# `Analysis::Runner` walks files concurrently (file-
|
|
65
|
+
# level parallelism); the per-file workers share one
|
|
66
|
+
# Store. The monitor guards `@memo` + the counter
|
|
67
|
+
# hashes against concurrent writes. The Monitor is
|
|
68
|
+
# re-entrant so producer blocks can recursively
|
|
69
|
+
# consult the Store (e.g. one cache layer building on
|
|
70
|
+
# another) without dead-locking.
|
|
71
|
+
@monitor = Monitor.new
|
|
39
72
|
end
|
|
40
73
|
|
|
41
74
|
attr_reader :root
|
|
42
75
|
|
|
76
|
+
# @return [Boolean] whether this Store suppresses disk writes
|
|
77
|
+
# (`schema_version.txt`, entry creation). Reads are
|
|
78
|
+
# unaffected.
|
|
79
|
+
def read_only?
|
|
80
|
+
@read_only
|
|
81
|
+
end
|
|
82
|
+
|
|
43
83
|
# Returns a frozen snapshot of this Store's per-run hit / miss /
|
|
44
84
|
# write counters. The bookkeeping is in-memory only — every new
|
|
45
85
|
# `Store.new` starts at zero — so the counters reflect activity
|
|
@@ -49,8 +89,10 @@ module Rigor
|
|
|
49
89
|
#
|
|
50
90
|
# @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
|
|
51
91
|
def stats
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
@monitor.synchronize do
|
|
93
|
+
per_producer = @by_producer.transform_values { |counts| counts.dup.freeze }.freeze
|
|
94
|
+
{ hits: @hits, misses: @misses, writes: @writes, by_producer: per_producer }.freeze
|
|
95
|
+
end
|
|
54
96
|
end
|
|
55
97
|
|
|
56
98
|
# Walks the on-disk cache rooted at `root` and reports a
|
|
@@ -128,18 +170,30 @@ module Rigor
|
|
|
128
170
|
ensure_schema_version!
|
|
129
171
|
|
|
130
172
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
131
|
-
|
|
173
|
+
memo_key = [producer_id, key].freeze
|
|
174
|
+
memoed = @monitor.synchronize { @memo[memo_key] if @memo.key?(memo_key) }
|
|
175
|
+
unless memoed.nil?
|
|
176
|
+
@monitor.synchronize { record(:hits, producer_id) }
|
|
177
|
+
return memoed
|
|
178
|
+
end
|
|
132
179
|
|
|
180
|
+
path = entry_path(producer_id, key)
|
|
133
181
|
cached = read_entry(path, deserialize: deserialize)
|
|
134
182
|
unless cached.nil?
|
|
135
|
-
|
|
183
|
+
@monitor.synchronize do
|
|
184
|
+
record(:hits, producer_id)
|
|
185
|
+
@memo[memo_key] = cached.value
|
|
186
|
+
end
|
|
136
187
|
return cached.value
|
|
137
188
|
end
|
|
138
189
|
|
|
139
|
-
record(:misses, producer_id)
|
|
140
190
|
value = block.call
|
|
141
|
-
write_entry(path, descriptor, value, serialize: serialize)
|
|
142
|
-
|
|
191
|
+
write_entry(path, descriptor, value, serialize: serialize) unless @read_only
|
|
192
|
+
@monitor.synchronize do
|
|
193
|
+
record(:misses, producer_id)
|
|
194
|
+
record(:writes, producer_id) unless @read_only
|
|
195
|
+
@memo[memo_key] = value
|
|
196
|
+
end
|
|
143
197
|
value
|
|
144
198
|
end
|
|
145
199
|
|
|
@@ -269,6 +323,15 @@ module Rigor
|
|
|
269
323
|
end
|
|
270
324
|
|
|
271
325
|
def ensure_schema_version!
|
|
326
|
+
# Read-only stores never touch the cache root — no mkdir,
|
|
327
|
+
# no marker write, no destructive clear on schema
|
|
328
|
+
# mismatch. A stale or wrong-schema marker simply yields
|
|
329
|
+
# nothing back (entries read through the version check
|
|
330
|
+
# are content-keyed, so a write under the new schema
|
|
331
|
+
# never collides with a read under the old). The next
|
|
332
|
+
# writable run will repair the cache.
|
|
333
|
+
return if @read_only
|
|
334
|
+
|
|
272
335
|
FileUtils.mkdir_p(@root)
|
|
273
336
|
marker = File.join(@root, "schema_version.txt")
|
|
274
337
|
current = Descriptor::SCHEMA_VERSION.to_s
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Executes the `rigor lsp` command.
|
|
8
|
+
#
|
|
9
|
+
# See `docs/design/20260517-language-server.md` for the design.
|
|
10
|
+
# Slice 1 (this commit) ships the CLI subcommand entry point.
|
|
11
|
+
# The actual stdio JSON-RPC reader / writer is queued for slice 2;
|
|
12
|
+
# invoking `rigor lsp` at slice 1 returns immediately after
|
|
13
|
+
# validating the transport flag.
|
|
14
|
+
class LspCommand
|
|
15
|
+
USAGE = "Usage: rigor lsp [options]"
|
|
16
|
+
|
|
17
|
+
def initialize(argv:, out:, err:)
|
|
18
|
+
@argv = argv
|
|
19
|
+
@out = out
|
|
20
|
+
@err = err
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Integer] CLI exit status.
|
|
24
|
+
def run
|
|
25
|
+
options = parse_options
|
|
26
|
+
return CLI::EXIT_USAGE if options == :usage_error
|
|
27
|
+
|
|
28
|
+
transport = options.fetch(:transport)
|
|
29
|
+
unless transport == "stdio"
|
|
30
|
+
@err.puts("rigor lsp: unsupported transport: #{transport.inspect} (only `stdio` is supported in v1)")
|
|
31
|
+
return CLI::EXIT_USAGE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require_relative "../language_server"
|
|
35
|
+
require_relative "../configuration"
|
|
36
|
+
require "language_server-protocol"
|
|
37
|
+
|
|
38
|
+
# STDIN is read frame-by-frame via the gem's `Io::Reader`;
|
|
39
|
+
# STDOUT is wrapped in `SynchronizedWriter` so concurrent
|
|
40
|
+
# writes from the main dispatch thread + the Debouncer's
|
|
41
|
+
# async threads don't interleave frames. The Loop runs
|
|
42
|
+
# until either STDIN hits EOF or `server.exited?`; the
|
|
43
|
+
# process then exits with the server's recorded code
|
|
44
|
+
# (0 after a clean shutdown+exit, 1 otherwise).
|
|
45
|
+
writer = LanguageServer::SynchronizedWriter.new(
|
|
46
|
+
::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
|
|
47
|
+
)
|
|
48
|
+
server, loop_runner = build_server(writer: writer, config_path: options.fetch(:config))
|
|
49
|
+
loop_runner.run
|
|
50
|
+
server.exit_code || 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Builds the full collaborator graph from a fresh
|
|
56
|
+
# `Configuration` + `ProjectContext`. Returns `[server,
|
|
57
|
+
# loop]` so the caller drives the loop and reads
|
|
58
|
+
# `server.exit_code` for the process exit status.
|
|
59
|
+
def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
|
|
60
|
+
configuration = Configuration.load(config_path)
|
|
61
|
+
# ProjectContext caches Environment + Cache::Store across
|
|
62
|
+
# requests so hover / publish hit the warm path. Invalidated
|
|
63
|
+
# by `workspace/didChangeWatchedFiles` and
|
|
64
|
+
# `workspace/didChangeConfiguration`.
|
|
65
|
+
project_context = LanguageServer::ProjectContext.new(configuration: configuration)
|
|
66
|
+
# Single source of truth for buffer state — threaded to
|
|
67
|
+
# Server + all three providers.
|
|
68
|
+
buffer_table = LanguageServer::BufferTable.new
|
|
69
|
+
debouncer = LanguageServer::Debouncer.new
|
|
70
|
+
publisher = LanguageServer::DiagnosticPublisher.new(
|
|
71
|
+
writer: writer, buffer_table: buffer_table, project_context: project_context,
|
|
72
|
+
debouncer: debouncer, debounce_seconds: 0.2
|
|
73
|
+
)
|
|
74
|
+
server = LanguageServer::Server.new(
|
|
75
|
+
buffer_table: buffer_table,
|
|
76
|
+
publisher: publisher,
|
|
77
|
+
hover_provider: LanguageServer::HoverProvider.new(
|
|
78
|
+
buffer_table: buffer_table, project_context: project_context
|
|
79
|
+
),
|
|
80
|
+
document_symbol_provider: LanguageServer::DocumentSymbolProvider.new(
|
|
81
|
+
buffer_table: buffer_table, project_context: project_context
|
|
82
|
+
),
|
|
83
|
+
completion_provider: LanguageServer::CompletionProvider.new(
|
|
84
|
+
buffer_table: buffer_table, project_context: project_context
|
|
85
|
+
),
|
|
86
|
+
signature_help_provider: LanguageServer::SignatureHelpProvider.new(
|
|
87
|
+
buffer_table: buffer_table, project_context: project_context
|
|
88
|
+
),
|
|
89
|
+
folding_range_provider: LanguageServer::FoldingRangeProvider.new(
|
|
90
|
+
buffer_table: buffer_table, project_context: project_context
|
|
91
|
+
),
|
|
92
|
+
selection_range_provider: LanguageServer::SelectionRangeProvider.new(
|
|
93
|
+
buffer_table: buffer_table, project_context: project_context
|
|
94
|
+
),
|
|
95
|
+
project_context: project_context
|
|
96
|
+
)
|
|
97
|
+
loop_runner = LanguageServer::Loop.new(
|
|
98
|
+
reader: ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin),
|
|
99
|
+
writer: writer,
|
|
100
|
+
server: server
|
|
101
|
+
)
|
|
102
|
+
[server, loop_runner]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_options
|
|
106
|
+
options = { transport: "stdio", log: nil, config: nil }
|
|
107
|
+
|
|
108
|
+
parser = OptionParser.new do |opts|
|
|
109
|
+
opts.banner = USAGE
|
|
110
|
+
opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
|
|
111
|
+
options[:transport] = value
|
|
112
|
+
end
|
|
113
|
+
opts.on("--log=PATH", "Write LSP wire log + server debug to PATH (default: stderr)") do |value|
|
|
114
|
+
options[:log] = value
|
|
115
|
+
end
|
|
116
|
+
opts.on("--config=PATH", "Path to the Rigor configuration file") do |value|
|
|
117
|
+
options[:config] = value
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
parser.parse!(@argv)
|
|
121
|
+
options
|
|
122
|
+
rescue OptionParser::ParseError => e
|
|
123
|
+
@err.puts(e.message)
|
|
124
|
+
@err.puts(USAGE)
|
|
125
|
+
:usage_error
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
require "prism"
|
|
5
5
|
|
|
6
|
+
require_relative "../analysis/buffer_binding"
|
|
6
7
|
require_relative "../configuration"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../scope"
|
|
@@ -38,35 +39,73 @@ module Rigor
|
|
|
38
39
|
# @return [Integer] CLI exit status.
|
|
39
40
|
def run
|
|
40
41
|
options = parse_options
|
|
42
|
+
buffer = resolve_buffer_binding(options)
|
|
43
|
+
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
41
44
|
|
|
42
45
|
target = parse_position_argument(@argv)
|
|
43
46
|
return CLI::EXIT_USAGE if target.nil?
|
|
44
47
|
|
|
45
|
-
execute(target: target, options: options)
|
|
48
|
+
execute(target: target, options: options, buffer: buffer)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
private
|
|
49
52
|
|
|
50
53
|
def parse_options
|
|
51
|
-
options = { format: "text", trace: false, config: nil }
|
|
54
|
+
options = { format: "text", trace: false, config: nil, tmp_file: nil, instead_of: nil }
|
|
52
55
|
|
|
53
56
|
parser = OptionParser.new do |opts|
|
|
54
57
|
opts.banner = USAGE
|
|
55
58
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
56
59
|
opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
|
|
57
60
|
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
61
|
+
opts.on("--tmp-file=PATH",
|
|
62
|
+
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
63
|
+
options[:tmp_file] = value
|
|
64
|
+
end
|
|
65
|
+
opts.on("--instead-of=PATH",
|
|
66
|
+
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
67
|
+
options[:instead_of] = value
|
|
68
|
+
end
|
|
58
69
|
end
|
|
59
70
|
parser.parse!(@argv)
|
|
60
71
|
|
|
61
72
|
options
|
|
62
73
|
end
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
# Mirrors `Rigor::CLI#resolve_buffer_binding` (the `check`
|
|
76
|
+
# path). Returns nil / BufferBinding / :usage_error. The
|
|
77
|
+
# symbol return path lets the caller translate to
|
|
78
|
+
# `CLI::EXIT_USAGE` without raising.
|
|
79
|
+
def resolve_buffer_binding(options)
|
|
80
|
+
tmp = options[:tmp_file]
|
|
81
|
+
instead = options[:instead_of]
|
|
82
|
+
return nil if tmp.nil? && instead.nil?
|
|
83
|
+
|
|
84
|
+
if tmp.nil? || instead.nil?
|
|
85
|
+
@err.puts("--tmp-file and --instead-of must appear together")
|
|
86
|
+
return :usage_error
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless File.file?(tmp)
|
|
90
|
+
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
91
|
+
return :usage_error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Rigor::Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def execute(target:, options:, buffer: nil)
|
|
65
98
|
file, line, column = target
|
|
66
|
-
|
|
99
|
+
# Under editor mode the logical `file` may not exist on disk
|
|
100
|
+
# (user editing a new file); the runtime check is only that
|
|
101
|
+
# the BUFFER is readable, which `resolve_buffer_binding`
|
|
102
|
+
# has already enforced. For non-editor mode `file` must
|
|
103
|
+
# exist.
|
|
104
|
+
physical = buffer ? buffer.resolve(file) : file
|
|
105
|
+
return 1 unless file_exists?(buffer ? physical : file)
|
|
67
106
|
|
|
68
107
|
configuration = Configuration.load(options.fetch(:config))
|
|
69
|
-
source = File.read(
|
|
108
|
+
source = File.read(physical)
|
|
70
109
|
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
71
110
|
return 1 if parse_errors?(parse_result, file)
|
|
72
111
|
|