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
|
@@ -12,10 +12,13 @@ require_relative "../type/combinator"
|
|
|
12
12
|
require_relative "../inference/coverage_scanner"
|
|
13
13
|
require_relative "../inference/scope_indexer"
|
|
14
14
|
require_relative "../inference/synthetic_method_scanner"
|
|
15
|
+
require_relative "../inference/project_patched_scanner"
|
|
15
16
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
17
|
+
require_relative "buffer_binding"
|
|
16
18
|
require_relative "check_rules"
|
|
17
19
|
require_relative "dependency_source_inference"
|
|
18
20
|
require_relative "diagnostic"
|
|
21
|
+
require_relative "project_scan"
|
|
19
22
|
require_relative "result"
|
|
20
23
|
require_relative "run_stats"
|
|
21
24
|
require_relative "worker_session"
|
|
@@ -52,21 +55,63 @@ module Rigor
|
|
|
52
55
|
# Set to false to skip the stats summary entirely; the
|
|
53
56
|
# CLI's `--no-stats` threads `false` through to keep
|
|
54
57
|
# trivial-fixture runs from warming `.rigor/cache`.
|
|
55
|
-
|
|
58
|
+
# @param prebuilt [Rigor::Analysis::ProjectScan, nil] when
|
|
59
|
+
# supplied, the runner adopts the pre-built plugin
|
|
60
|
+
# registry / dependency-source index / scanner outputs
|
|
61
|
+
# from the snapshot and skips the per-call pre-passes
|
|
62
|
+
# that produce them. Used by long-lived integrations
|
|
63
|
+
# (`Rigor::LanguageServer::ProjectContext`) to keep
|
|
64
|
+
# per-buffer requests fast — scanners walk the project
|
|
65
|
+
# once per generation rather than once per request, and
|
|
66
|
+
# plugin `#prepare` runs once per generation rather than
|
|
67
|
+
# once per request. Watched-file invalidation is the
|
|
68
|
+
# owner's responsibility; the runner trusts the snapshot
|
|
69
|
+
# it was given.
|
|
70
|
+
# @param environment [Rigor::Environment, nil] opt-in
|
|
71
|
+
# Environment override. When supplied, sequential mode uses
|
|
72
|
+
# the provided env instance in `#analyze_files` instead of
|
|
73
|
+
# building a fresh one via `Environment.for_project`, and
|
|
74
|
+
# attaches the runner's per-run reporter pair onto the
|
|
75
|
+
# env's mutable `Reporters` slot via
|
|
76
|
+
# `Environment#attach_reporters!`. Long-lived consumers
|
|
77
|
+
# (LSP `ProjectContext`) pass a shared env so per-publish
|
|
78
|
+
# work doesn't repeat the `Environment.for_project` build
|
|
79
|
+
# (bundler / lockfile / collection discovery, RbsLoader
|
|
80
|
+
# construction). Pool mode ignores the override — each
|
|
81
|
+
# worker continues to build its own Environment.
|
|
82
|
+
def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists
|
|
56
83
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
57
|
-
plugin_requirer: nil, workers: 0, collect_stats: true
|
|
84
|
+
plugin_requirer: nil, workers: 0, collect_stats: true,
|
|
85
|
+
buffer: nil, prebuilt: nil, environment: nil)
|
|
58
86
|
@configuration = configuration
|
|
59
87
|
@explain = explain
|
|
60
|
-
@cache_store = cache_store
|
|
88
|
+
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
61
89
|
@plugin_requirer = plugin_requirer
|
|
62
90
|
@workers = workers
|
|
63
91
|
@collect_stats = collect_stats
|
|
92
|
+
@buffer = buffer
|
|
93
|
+
@prebuilt = prebuilt
|
|
94
|
+
@environment_override = environment
|
|
64
95
|
@plugin_registry = Plugin::Registry::EMPTY
|
|
65
96
|
@dependency_source_index = DependencySourceInference::Index::EMPTY
|
|
66
97
|
@rbs_extended_reporter = RbsExtended::Reporter.new
|
|
67
98
|
@boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
|
|
99
|
+
# `#run` resets these for each invocation; pre-seed them to
|
|
100
|
+
# empty containers so `build_run_stats` / `pre_file_diagnostics`
|
|
101
|
+
# (private, called only from `#run`) can read them without
|
|
102
|
+
# nil-guards.
|
|
103
|
+
@class_decl_paths_snapshot = {}.freeze
|
|
104
|
+
@signature_paths_snapshot = [].freeze
|
|
105
|
+
@cached_plugin_prepare_diagnostics = [].freeze
|
|
106
|
+
@project_discovered_classes = {}.freeze
|
|
68
107
|
end
|
|
69
108
|
|
|
109
|
+
# ADR-pending editor mode — present when the runner is wired
|
|
110
|
+
# for the `--tmp-file` / `--instead-of` buffer-binding shape
|
|
111
|
+
# (`docs/design/20260516-editor-mode.md`). Nil for normal
|
|
112
|
+
# project runs.
|
|
113
|
+
attr_reader :buffer
|
|
114
|
+
|
|
70
115
|
# Walks every Ruby file under `paths`, parses it, builds a
|
|
71
116
|
# per-node scope index through
|
|
72
117
|
# `Rigor::Inference::ScopeIndexer`, and runs the
|
|
@@ -84,23 +129,18 @@ module Rigor
|
|
|
84
129
|
target_ruby_error = validate_target_ruby
|
|
85
130
|
return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
|
|
86
131
|
|
|
87
|
-
@plugin_registry = load_plugins
|
|
88
|
-
@dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
89
132
|
expansion = expand_paths(paths)
|
|
90
133
|
@class_decl_paths_snapshot = {}.freeze
|
|
91
134
|
@signature_paths_snapshot = []
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
paths: expansion.fetch(:files),
|
|
99
|
-
environment: nil
|
|
100
|
-
)
|
|
135
|
+
|
|
136
|
+
if @prebuilt
|
|
137
|
+
adopt_prebuilt_project_scan(@prebuilt)
|
|
138
|
+
else
|
|
139
|
+
run_project_pre_passes(expansion: expansion)
|
|
140
|
+
end
|
|
101
141
|
|
|
102
142
|
diagnostics = pre_file_diagnostics(expansion)
|
|
103
|
-
diagnostics += analyze_files(expansion
|
|
143
|
+
diagnostics += analyze_files(target_files(expansion))
|
|
104
144
|
diagnostics += rbs_extended_reporter_diagnostics
|
|
105
145
|
diagnostics += boundary_cross_diagnostics
|
|
106
146
|
|
|
@@ -110,6 +150,113 @@ module Rigor
|
|
|
110
150
|
)
|
|
111
151
|
end
|
|
112
152
|
|
|
153
|
+
# Runs every project-wide pre-pass (`load_plugins` +
|
|
154
|
+
# `plugin#prepare` + dependency-source builder +
|
|
155
|
+
# synthetic-method scanner + project-patched scanner)
|
|
156
|
+
# exactly once, then returns a frozen
|
|
157
|
+
# {Rigor::Analysis::ProjectScan} snapshot.
|
|
158
|
+
#
|
|
159
|
+
# Long-lived integrations (`Rigor::LanguageServer::ProjectContext`)
|
|
160
|
+
# call this once per project-state generation and feed the
|
|
161
|
+
# snapshot back into `Runner.new(prebuilt: scan)` for every
|
|
162
|
+
# subsequent per-buffer publish. The cold pre-pass cost is
|
|
163
|
+
# paid once per generation rather than once per keystroke.
|
|
164
|
+
#
|
|
165
|
+
# Notes for callers:
|
|
166
|
+
# - The runner this method is called on may be a "build only"
|
|
167
|
+
# instance — `@buffer` is typically nil so the scanners
|
|
168
|
+
# observe on-disk bytes for the full project. Callers that
|
|
169
|
+
# want pre-passes to see a particular buffer's edits should
|
|
170
|
+
# build the runner WITH `buffer:` set.
|
|
171
|
+
# - The returned ProjectScan is frozen and shareable; the
|
|
172
|
+
# underlying `plugin_registry` is the same object that ran
|
|
173
|
+
# `#prepare`, so the per-plugin `services.fact_store` is
|
|
174
|
+
# already populated for subsequent dispatch use.
|
|
175
|
+
def prepare_project_scan(paths: @configuration.paths)
|
|
176
|
+
expansion = expand_paths(paths)
|
|
177
|
+
run_project_pre_passes(expansion: expansion)
|
|
178
|
+
ProjectScan.new(
|
|
179
|
+
plugin_registry: @plugin_registry,
|
|
180
|
+
dependency_source_index: @dependency_source_index,
|
|
181
|
+
synthetic_method_index: @synthetic_method_index,
|
|
182
|
+
project_patched_methods: @project_patched_methods,
|
|
183
|
+
plugin_prepare_diagnostics: @cached_plugin_prepare_diagnostics.dup.freeze,
|
|
184
|
+
pre_eval_diagnostics: @pre_eval_diagnostics_from_scanner.dup.freeze
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Internal: drives every project-wide pre-pass and stores
|
|
189
|
+
# the results on instance variables in the order the
|
|
190
|
+
# downstream `#run` body expects. Extracted so
|
|
191
|
+
# `#prepare_project_scan` and the prebuilt-less `#run` path
|
|
192
|
+
# share one implementation.
|
|
193
|
+
def run_project_pre_passes(expansion:)
|
|
194
|
+
@plugin_registry = load_plugins
|
|
195
|
+
@dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
196
|
+
# ADR-18 slice 3 — plugin prepare MUST run before the
|
|
197
|
+
# synthetic-method scanner so cross-plugin facts
|
|
198
|
+
# (`:dry_type_aliases` etc.) are already published when
|
|
199
|
+
# the scanner resolves Tier C `returns_from_arg:`
|
|
200
|
+
# lookups. The diagnostics produced by prepare are
|
|
201
|
+
# captured here so `pre_file_diagnostics` can re-emit
|
|
202
|
+
# them in the existing order without invoking prepare
|
|
203
|
+
# twice. Pool mode still re-runs prepare per worker
|
|
204
|
+
# (workers don't see this early invocation), preserving
|
|
205
|
+
# the existing Phase 4b contract.
|
|
206
|
+
@cached_plugin_prepare_diagnostics =
|
|
207
|
+
pool_mode? ? [] : plugin_prepare_diagnostics
|
|
208
|
+
# ADR-16 slice 2b — Tier C pre-pass. Built once per run
|
|
209
|
+
# against the resolved file set + the loaded plugin
|
|
210
|
+
# registry's `heredoc_templates` so synthetic methods are
|
|
211
|
+
# visible cross-file when per-file inference dispatches.
|
|
212
|
+
@synthetic_method_index = Inference::SyntheticMethodScanner.scan(
|
|
213
|
+
plugin_registry: @plugin_registry,
|
|
214
|
+
paths: expansion.fetch(:files),
|
|
215
|
+
environment: nil,
|
|
216
|
+
fact_store: shared_fact_store,
|
|
217
|
+
buffer: @buffer
|
|
218
|
+
)
|
|
219
|
+
# ADR-17 slice 2 — pre-eval pre-pass. Built once per run
|
|
220
|
+
# from the `pre_eval:` entries that exist on disk
|
|
221
|
+
# (slice-1's `pre-eval.file-not-found` `:error` already
|
|
222
|
+
# surfaced any missing entries; the scanner skips them
|
|
223
|
+
# here). The resulting {ProjectPatchedMethods} registry
|
|
224
|
+
# is consulted by the dispatcher tier between plugins
|
|
225
|
+
# and dependency-source inference so project-side
|
|
226
|
+
# patches resolve cross-file.
|
|
227
|
+
existing_pre_eval = @configuration.pre_eval.select { |path| File.file?(path) }
|
|
228
|
+
pre_eval_outcome = Inference::ProjectPatchedScanner.scan(existing_pre_eval, buffer: @buffer)
|
|
229
|
+
@project_patched_methods = pre_eval_outcome.registry
|
|
230
|
+
@pre_eval_diagnostics_from_scanner = pre_eval_outcome.diagnostics
|
|
231
|
+
# Cross-file class discovery — walks every project file
|
|
232
|
+
# for `class Foo` / `module Bar` declarations so a
|
|
233
|
+
# `Foo.method_call` receiver in one file resolves a
|
|
234
|
+
# `class Foo` declared in a sibling file. Without this
|
|
235
|
+
# pre-pass each file's `discovered_classes` was per-file
|
|
236
|
+
# only, and lexical lookup fell back to stdlib `::Foo`
|
|
237
|
+
# for any user class shadowing a stdlib name (e.g.
|
|
238
|
+
# `Google::Cloud::Storage::File`). Cost is one extra
|
|
239
|
+
# parse pass over the project; small projects pay
|
|
240
|
+
# tens of ms, larger projects ~1s. Future optimisation
|
|
241
|
+
# can share parses with the existing scanner passes.
|
|
242
|
+
@project_discovered_classes =
|
|
243
|
+
Inference::ScopeIndexer.discovered_classes_for_paths(expansion.fetch(:files), buffer: @buffer)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Internal: adopts a frozen {ProjectScan} snapshot supplied
|
|
247
|
+
# to `Runner.new(prebuilt: ...)` by storing each slot on
|
|
248
|
+
# the runner's ivar surface, mirroring what
|
|
249
|
+
# `run_project_pre_passes` would have produced.
|
|
250
|
+
def adopt_prebuilt_project_scan(scan)
|
|
251
|
+
@plugin_registry = scan.plugin_registry
|
|
252
|
+
@dependency_source_index = scan.dependency_source_index
|
|
253
|
+
@synthetic_method_index = scan.synthetic_method_index
|
|
254
|
+
@project_patched_methods = scan.project_patched_methods
|
|
255
|
+
@cached_plugin_prepare_diagnostics = scan.plugin_prepare_diagnostics
|
|
256
|
+
@pre_eval_diagnostics_from_scanner = scan.pre_eval_diagnostics
|
|
257
|
+
end
|
|
258
|
+
private :run_project_pre_passes, :adopt_prebuilt_project_scan
|
|
259
|
+
|
|
113
260
|
# ADR-15 Phase 4b — routes per-file analysis to either the
|
|
114
261
|
# sequential coordinator-side Environment (legacy path,
|
|
115
262
|
# default) or a Ractor worker pool built around
|
|
@@ -133,7 +280,7 @@ module Rigor
|
|
|
133
280
|
if pool_mode?
|
|
134
281
|
analyze_files_in_pool(files)
|
|
135
282
|
else
|
|
136
|
-
environment =
|
|
283
|
+
environment = resolve_sequential_environment
|
|
137
284
|
result = files.flat_map { |path| analyze_file(path, environment) }
|
|
138
285
|
if @collect_stats
|
|
139
286
|
loader = environment.rbs_loader
|
|
@@ -144,6 +291,23 @@ module Rigor
|
|
|
144
291
|
end
|
|
145
292
|
end
|
|
146
293
|
|
|
294
|
+
# Sequential-mode environment resolver. Returns the supplied
|
|
295
|
+
# `environment:` override (with the runner's fresh per-run
|
|
296
|
+
# reporter pair attached so dispatcher events route to THIS
|
|
297
|
+
# runner's diagnostics) when present; otherwise builds a
|
|
298
|
+
# fresh Environment per-call via {#build_runner_environment}
|
|
299
|
+
# — preserving the pre-override behaviour bit-for-bit.
|
|
300
|
+
def resolve_sequential_environment
|
|
301
|
+
return build_runner_environment unless @environment_override
|
|
302
|
+
|
|
303
|
+
@environment_override.attach_reporters!(
|
|
304
|
+
rbs_extended_reporter: @rbs_extended_reporter,
|
|
305
|
+
boundary_cross_reporter: @boundary_cross_reporter
|
|
306
|
+
)
|
|
307
|
+
@environment_override
|
|
308
|
+
end
|
|
309
|
+
private :resolve_sequential_environment
|
|
310
|
+
|
|
147
311
|
# Pre-file diagnostic streams that fire once per run rather
|
|
148
312
|
# than per analyzed file: plugin load / prepare envelopes,
|
|
149
313
|
# the ADR-10 dependency-source resolution surface, and the
|
|
@@ -162,9 +326,15 @@ module Rigor
|
|
|
162
326
|
# against the coordinator-side plugin instances (which
|
|
163
327
|
# the pool path never consults for per-file analysis).
|
|
164
328
|
def pre_file_diagnostics(expansion)
|
|
165
|
-
|
|
329
|
+
# ADR-18 slice 3 — prepare diagnostics are captured
|
|
330
|
+
# earlier in #run (before the synthetic-method scanner)
|
|
331
|
+
# so cross-plugin facts are available to the scanner.
|
|
332
|
+
# We re-surface the captured diagnostics here so the
|
|
333
|
+
# existing pre_file_diagnostics ordering is preserved.
|
|
334
|
+
prepare = pool_mode? ? [] : @cached_plugin_prepare_diagnostics
|
|
166
335
|
plugin_load_diagnostics +
|
|
167
336
|
prepare +
|
|
337
|
+
pre_eval_diagnostics +
|
|
168
338
|
dependency_source_diagnostics +
|
|
169
339
|
dependency_source_budget_diagnostics +
|
|
170
340
|
dependency_source_config_conflict_diagnostics +
|
|
@@ -172,6 +342,53 @@ module Rigor
|
|
|
172
342
|
expansion.fetch(:errors)
|
|
173
343
|
end
|
|
174
344
|
|
|
345
|
+
# Returns the per-run shared `Plugin::FactStore` instance.
|
|
346
|
+
# All loaded plugins share this store through their
|
|
347
|
+
# respective `Plugin::Services` (the same instance is
|
|
348
|
+
# threaded by `Plugin::Loader.load`). Returns `nil` when
|
|
349
|
+
# no plugins are loaded.
|
|
350
|
+
def shared_fact_store
|
|
351
|
+
return nil if @plugin_registry.nil? || @plugin_registry.empty?
|
|
352
|
+
|
|
353
|
+
@plugin_registry.plugins.first&.services&.fact_store
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# ADR-17 slice 1 — surface a `:error` diagnostic for each
|
|
357
|
+
# `pre_eval:` entry whose resolved path doesn't exist on
|
|
358
|
+
# disk. Loud failure mode (`:error`, not `:warning`):
|
|
359
|
+
# a missing pre_eval path is a configuration mistake the
|
|
360
|
+
# user must fix before analysis is meaningful.
|
|
361
|
+
#
|
|
362
|
+
# Slice 2 adds the `:warning` `pre-eval.parse-error`
|
|
363
|
+
# stream from the pre-pass scanner — accumulated as
|
|
364
|
+
# `@pre_eval_diagnostics_from_scanner` during {#run} and
|
|
365
|
+
# merged here so both diagnostics flow through the same
|
|
366
|
+
# severity / ordering pipeline.
|
|
367
|
+
def pre_eval_diagnostics
|
|
368
|
+
not_found = @configuration.pre_eval.filter_map do |path|
|
|
369
|
+
next if File.file?(path)
|
|
370
|
+
|
|
371
|
+
Diagnostic.new(
|
|
372
|
+
path: ".rigor.yml", line: 1, column: 1,
|
|
373
|
+
message: "pre_eval entry not found: #{path.inspect}. " \
|
|
374
|
+
"Pre-evaluation requires the file to exist on disk; remove the entry " \
|
|
375
|
+
"or create the file before re-running analysis.",
|
|
376
|
+
severity: :error,
|
|
377
|
+
rule: "pre-eval.file-not-found",
|
|
378
|
+
source_family: :builtin
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
not_found + Array(@pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def diagnostic_from_hash(hash)
|
|
385
|
+
Diagnostic.new(
|
|
386
|
+
path: hash.fetch(:path), line: hash.fetch(:line), column: hash.fetch(:column),
|
|
387
|
+
message: hash.fetch(:message), severity: hash.fetch(:severity),
|
|
388
|
+
rule: hash.fetch(:rule), source_family: :builtin
|
|
389
|
+
)
|
|
390
|
+
end
|
|
391
|
+
|
|
175
392
|
# `target_ruby` flows through to Prism's `version:` option.
|
|
176
393
|
# Prism enforces the supported range and raises
|
|
177
394
|
# `ArgumentError` for versions it does not recognise. Run a
|
|
@@ -193,8 +410,54 @@ module Rigor
|
|
|
193
410
|
|
|
194
411
|
private
|
|
195
412
|
|
|
413
|
+
# Editor mode § "Scope choice — option A". Under
|
|
414
|
+
# `buffer:` non-nil the per-file analysis emits diagnostics
|
|
415
|
+
# ONLY for the buffer's logical path; the rest of `paths:`
|
|
416
|
+
# is consumed by the project-wide pre-passes (synthetic
|
|
417
|
+
# methods, project-patched methods, plugin facts) but
|
|
418
|
+
# contributes no per-file diagnostics. This is the v1 cut
|
|
419
|
+
# before a per-file diagnostic cache exists; option B (full
|
|
420
|
+
# project + incremental cache) is queued.
|
|
421
|
+
#
|
|
422
|
+
# The buffer's logical path is added to the file list even
|
|
423
|
+
# if it's not under `paths:` — per design § "Failure
|
|
424
|
+
# envelope": "--instead-of=Y with Y not under any paths:
|
|
425
|
+
# directory → treated as a valid logical identity for the
|
|
426
|
+
# buffer".
|
|
427
|
+
def target_files(expansion)
|
|
428
|
+
files = expansion.fetch(:files)
|
|
429
|
+
return files if @buffer.nil?
|
|
430
|
+
|
|
431
|
+
[@buffer.logical_path]
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Editor mode (`buffer:` non-nil) auto-flips the cache store
|
|
435
|
+
# to `read_only: true` so multiple debounced editor invocations
|
|
436
|
+
# against the same project don't churn the on-disk cache or
|
|
437
|
+
# race on schema-version writes. Cache reads continue
|
|
438
|
+
# unchanged; misses still run the producer block but the
|
|
439
|
+
# result is not persisted. Per design doc § "Cache behaviour".
|
|
440
|
+
def enforce_read_only_cache(cache_store, buffer)
|
|
441
|
+
return cache_store if buffer.nil?
|
|
442
|
+
return cache_store if cache_store.nil?
|
|
443
|
+
return cache_store if cache_store.read_only?
|
|
444
|
+
|
|
445
|
+
Cache::Store.new(root: cache_store.root, read_only: true)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`.
|
|
449
|
+
# Editor mode (`buffer:` non-nil) silently overrides pool
|
|
450
|
+
# mode to sequential: per design § "Ractor pool mode", the
|
|
451
|
+
# pool's warm-up cost dominates one-file wall time, so the
|
|
452
|
+
# pool gains nothing on a per-buffer invocation. The override
|
|
453
|
+
# is part of the contract — not a degradation diagnostic —
|
|
454
|
+
# because `--workers=N` is a project-scale knob and editor
|
|
455
|
+
# mode is per-buffer; the conflict resolves toward the more
|
|
456
|
+
# specific axis.
|
|
196
457
|
def pool_mode?
|
|
197
|
-
@workers.is_a?(Integer) && @workers.positive?
|
|
458
|
+
return false unless @workers.is_a?(Integer) && @workers.positive?
|
|
459
|
+
|
|
460
|
+
@buffer.nil?
|
|
198
461
|
end
|
|
199
462
|
|
|
200
463
|
# Coordinator-side Environment used by the sequential code
|
|
@@ -214,7 +477,8 @@ module Rigor
|
|
|
214
477
|
bundler_lockfile: @configuration.bundler_lockfile,
|
|
215
478
|
rbs_collection_lockfile: @configuration.rbs_collection_lockfile,
|
|
216
479
|
rbs_collection_auto_detect: @configuration.rbs_collection_auto_detect,
|
|
217
|
-
synthetic_method_index: @synthetic_method_index
|
|
480
|
+
synthetic_method_index: @synthetic_method_index,
|
|
481
|
+
project_patched_methods: @project_patched_methods
|
|
218
482
|
)
|
|
219
483
|
end
|
|
220
484
|
|
|
@@ -345,7 +609,7 @@ module Rigor
|
|
|
345
609
|
# Wall + RSS are single syscalls; total cost is bounded
|
|
346
610
|
# by the snapshot size (~1000-2000 entries).
|
|
347
611
|
def build_run_stats(wall_started_at:, expansion:)
|
|
348
|
-
snapshot = @class_decl_paths_snapshot
|
|
612
|
+
snapshot = @class_decl_paths_snapshot
|
|
349
613
|
project_sig, bundled = RunStats.partition_classes(
|
|
350
614
|
class_decl_paths: snapshot,
|
|
351
615
|
signature_paths: @signature_paths_snapshot
|
|
@@ -895,7 +1159,11 @@ module Rigor
|
|
|
895
1159
|
Array(paths).each do |path|
|
|
896
1160
|
if File.directory?(path)
|
|
897
1161
|
files.concat(reject_excluded(Dir.glob(File.join(path, RUBY_GLOB))))
|
|
898
|
-
|
|
1162
|
+
# Editor-mode bypass: the buffer's logical path is treated
|
|
1163
|
+
# as a real `.rb` file regardless of on-disk presence —
|
|
1164
|
+
# `parse_source` reads bytes from the buffer's physical
|
|
1165
|
+
# path. Common case: LSP client editing a brand-new file.
|
|
1166
|
+
elsif accept_as_ruby_file?(path)
|
|
899
1167
|
files << path
|
|
900
1168
|
elsif File.exist?(path)
|
|
901
1169
|
errors << path_error(path, "not a Ruby file (expected `.rb` or a directory)")
|
|
@@ -906,6 +1174,11 @@ module Rigor
|
|
|
906
1174
|
{ files: files, errors: errors }
|
|
907
1175
|
end
|
|
908
1176
|
|
|
1177
|
+
def accept_as_ruby_file?(path)
|
|
1178
|
+
(File.file?(path) && path.end_with?(".rb")) ||
|
|
1179
|
+
(@buffer && path == @buffer.logical_path)
|
|
1180
|
+
end
|
|
1181
|
+
|
|
909
1182
|
# `Configuration#exclude_patterns` is a list of glob patterns
|
|
910
1183
|
# checked against each globbed path via `File.fnmatch?` (without
|
|
911
1184
|
# `FNM_PATHNAME`, so `**` and `*` both span path separators —
|
|
@@ -935,11 +1208,25 @@ module Rigor
|
|
|
935
1208
|
)
|
|
936
1209
|
end
|
|
937
1210
|
|
|
1211
|
+
# Reads + parses the source at `path`. Under editor mode
|
|
1212
|
+
# (`@buffer` set) reads bytes from `@buffer.physical_path`
|
|
1213
|
+
# when `path` matches the logical binding, then parses with
|
|
1214
|
+
# `filepath: path` so Prism's location data carries the
|
|
1215
|
+
# LOGICAL path. Non-binding paths go through the cheaper
|
|
1216
|
+
# `Prism.parse_file` codepath unchanged.
|
|
1217
|
+
def parse_source(path)
|
|
1218
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
1219
|
+
return Prism.parse_file(physical, version: @configuration.target_ruby) if physical == path
|
|
1220
|
+
|
|
1221
|
+
Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
|
|
1222
|
+
end
|
|
1223
|
+
|
|
938
1224
|
def analyze_file(path, environment) # rubocop:disable Metrics/MethodLength
|
|
939
|
-
parse_result =
|
|
1225
|
+
parse_result = parse_source(path)
|
|
940
1226
|
return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
|
|
941
1227
|
|
|
942
1228
|
scope = Scope.empty(environment: environment, source_path: path)
|
|
1229
|
+
scope = scope.with_discovered_classes(@project_discovered_classes) unless @project_discovered_classes.empty?
|
|
943
1230
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
944
1231
|
diagnostics = CheckRules.diagnose(
|
|
945
1232
|
path: path,
|
|
@@ -88,10 +88,11 @@ module Rigor
|
|
|
88
88
|
# directly-unrecognised node, mirroring
|
|
89
89
|
# {Rigor::Analysis::Runner#explain_diagnostics}.
|
|
90
90
|
def initialize(configuration:, cache_store: nil, # rubocop:disable Metrics/MethodLength
|
|
91
|
-
plugin_blueprints: [], explain: false)
|
|
91
|
+
plugin_blueprints: [], explain: false, buffer: nil)
|
|
92
92
|
@configuration = configuration
|
|
93
93
|
@cache_store = cache_store
|
|
94
94
|
@explain = explain
|
|
95
|
+
@buffer = buffer
|
|
95
96
|
|
|
96
97
|
# NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths`
|
|
97
98
|
# is process-global state. Writing it from a non-main
|
|
@@ -137,7 +138,7 @@ module Rigor
|
|
|
137
138
|
# profile re-stamping is intentionally NOT applied — that
|
|
138
139
|
# is a per-run aggregate concern handled by the caller.
|
|
139
140
|
def analyze(path)
|
|
140
|
-
parse_result =
|
|
141
|
+
parse_result = parse_source(path)
|
|
141
142
|
return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
|
|
142
143
|
|
|
143
144
|
scope = Scope.empty(environment: @environment, source_path: path)
|
|
@@ -174,6 +175,17 @@ module Rigor
|
|
|
174
175
|
|
|
175
176
|
private
|
|
176
177
|
|
|
178
|
+
# See {Runner#parse_source}. Same contract: if `@buffer`
|
|
179
|
+
# binds `path` to a physical file, read the physical bytes
|
|
180
|
+
# but stamp the parse buffer's `filepath:` as the LOGICAL
|
|
181
|
+
# path so downstream diagnostics carry the logical path.
|
|
182
|
+
def parse_source(path)
|
|
183
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
184
|
+
return Prism.parse_file(physical, version: @configuration.target_ruby) if physical == path
|
|
185
|
+
|
|
186
|
+
Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
|
|
187
|
+
end
|
|
188
|
+
|
|
177
189
|
# Mirrors {Runner#build_trust_policy}. Workers under Phase
|
|
178
190
|
# 4b will need the same trust derivation, and the
|
|
179
191
|
# configuration is already shareable, so deriving it inside
|