rigortype 0.2.9 → 0.3.0
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 +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../diagnostic"
|
|
4
|
+
require_relative "../severity_stamp"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Analysis
|
|
@@ -31,11 +32,16 @@ module Rigor
|
|
|
31
32
|
# @param cached_plugin_prepare_diagnostics [#call] reader returning the prepare-diagnostic snapshot.
|
|
32
33
|
# @param pre_eval_diagnostics_from_scanner [#call] reader returning the pre-eval scanner diagnostics.
|
|
33
34
|
# @param synthesized_namespaces_snapshot [#call] reader.
|
|
35
|
+
# @param quarantined_signatures_snapshot [#call] reader returning the `signature_paths:` files skipped
|
|
36
|
+
# because they do not parse (`[path, first_error_line]` pairs).
|
|
37
|
+
# @param env_build_failure_snapshot [#call] reader returning the total RBS env-build failure tuple
|
|
38
|
+
# (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
|
|
34
39
|
# @param conformance_results_snapshot [#call] reader.
|
|
35
40
|
def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
|
|
36
41
|
source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
|
|
37
42
|
pool_mode:, cached_plugin_prepare_diagnostics:,
|
|
38
43
|
pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
|
|
44
|
+
quarantined_signatures_snapshot:, env_build_failure_snapshot:,
|
|
39
45
|
conformance_results_snapshot:)
|
|
40
46
|
@configuration = configuration
|
|
41
47
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
@@ -47,6 +53,8 @@ module Rigor
|
|
|
47
53
|
@cached_plugin_prepare_diagnostics_reader = cached_plugin_prepare_diagnostics
|
|
48
54
|
@pre_eval_diagnostics_from_scanner_reader = pre_eval_diagnostics_from_scanner
|
|
49
55
|
@synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
|
|
56
|
+
@quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
|
|
57
|
+
@env_build_failure_snapshot_reader = env_build_failure_snapshot
|
|
50
58
|
@conformance_results_snapshot_reader = conformance_results_snapshot
|
|
51
59
|
end
|
|
52
60
|
|
|
@@ -73,6 +81,7 @@ module Rigor
|
|
|
73
81
|
dependency_source_budget_diagnostics +
|
|
74
82
|
dependency_source_config_conflict_diagnostics +
|
|
75
83
|
rbs_coverage_diagnostics +
|
|
84
|
+
rbs_inline_annotation_hint_diagnostics(expansion) +
|
|
76
85
|
expansion.fetch(:errors)
|
|
77
86
|
end
|
|
78
87
|
|
|
@@ -114,7 +123,7 @@ module Rigor
|
|
|
114
123
|
path: ".rigor.yml",
|
|
115
124
|
line: 1,
|
|
116
125
|
column: 1,
|
|
117
|
-
message: error
|
|
126
|
+
message: plugin_load_error_message(error),
|
|
118
127
|
severity: :error,
|
|
119
128
|
rule: "load-error",
|
|
120
129
|
source_family: :plugin_loader
|
|
@@ -122,6 +131,16 @@ module Rigor
|
|
|
122
131
|
end
|
|
123
132
|
end
|
|
124
133
|
|
|
134
|
+
# #194 slice 1 — when the require SUCCEEDED but configuration / instantiation then failed, the loader
|
|
135
|
+
# stamps the resolved file on the error; naming it here turns an engine↔plugin version skew (a stale
|
|
136
|
+
# installed `rigortype` gem shadowing a checkout's bundled plugin) into a one-line diagnosis. A
|
|
137
|
+
# require that failed outright carries no resolved path and keeps its original message.
|
|
138
|
+
def plugin_load_error_message(error)
|
|
139
|
+
return error.message if error.resolved_path.nil?
|
|
140
|
+
|
|
141
|
+
"#{error.message} (loaded from #{error.resolved_path})"
|
|
142
|
+
end
|
|
143
|
+
|
|
125
144
|
# ADR-10 § "Diagnostic prefix family" — surfaces gems listed in `dependencies.source_inference`
|
|
126
145
|
# that RubyGems could not resolve. The run continues; the gem simply contributes nothing this
|
|
127
146
|
# session, mirroring the plugin-load error envelope. Authored `:warning` because an unresolvable
|
|
@@ -224,12 +243,114 @@ module Rigor
|
|
|
224
243
|
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
225
244
|
end
|
|
226
245
|
|
|
246
|
+
# ADR-93 WD3 — the standalone residual. A bare `gem install rigortype` has no `rbs-inline` library,
|
|
247
|
+
# so the spec's "annotations are official type sources whenever present" cannot be satisfied: the
|
|
248
|
+
# annotated code reads `Dynamic[top]`. When the library is absent AND the project actually carries
|
|
249
|
+
# annotation-shaped comments, emit one `:info` routing the user to install it. Rigor never bundles it
|
|
250
|
+
# (ADR-0 zero-dep). Suppressed whenever the library resolves (auto-wire or an explicit `plugins:`
|
|
251
|
+
# entry then handles it — including the deliberate `enabled: false` opt-out, which resolves and so is
|
|
252
|
+
# never nagged) or an `rbs-inline` plugin is already active for another reason.
|
|
253
|
+
def rbs_inline_annotation_hint_diagnostics(expansion)
|
|
254
|
+
return [] if Configuration.rbs_inline_library_resolvable?
|
|
255
|
+
return [] if rbs_inline_plugin_active?
|
|
256
|
+
|
|
257
|
+
first = expansion.fetch(:files).find { |path| file_carries_inline_annotation?(path) }
|
|
258
|
+
return [] unless first
|
|
259
|
+
|
|
260
|
+
[build_rbs_inline_unsynthesized_diagnostic(first)]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def rbs_inline_plugin_active?
|
|
264
|
+
plugin_registry.plugins.any? { |plugin| plugin.manifest.id == "rbs-inline" }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A deliberately coarse routing scan — NOT the upstream annotation grammar, which is exactly what is
|
|
268
|
+
# unavailable here (that is the whole condition). It keys on the `# @rbs` block form and on a `#:`
|
|
269
|
+
# comment immediately followed by the start of an RBS type (`(`, `[`, `{`, `?`, a `Constant`, or an
|
|
270
|
+
# RBS lowercase base type), and never on an RDoc directive (`#:nodoc:` and friends read as a bare
|
|
271
|
+
# lowercase word closed by a colon), so a project with no real annotations stays silent. Reads the
|
|
272
|
+
# file directly (the analysis buffer is not threaded here) and early-exits on the first hit; this runs
|
|
273
|
+
# at most once per run and only in the library-absent standalone case, so the extra read is bounded.
|
|
274
|
+
INLINE_ANNOTATION_SHAPE = /
|
|
275
|
+
(?:^|\s)\#\s*@rbs\b
|
|
276
|
+
|
|
|
277
|
+
(?:^|\s)\#:[ \t]*(?:[\[({?A-Z]|(?:bool|void|nil|untyped|top|bot|self|instance|class)\b)
|
|
278
|
+
/x
|
|
279
|
+
private_constant :INLINE_ANNOTATION_SHAPE
|
|
280
|
+
|
|
281
|
+
def file_carries_inline_annotation?(path)
|
|
282
|
+
File.foreach(path) do |line|
|
|
283
|
+
return true if INLINE_ANNOTATION_SHAPE.match?(line)
|
|
284
|
+
end
|
|
285
|
+
false
|
|
286
|
+
rescue StandardError
|
|
287
|
+
false
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def build_rbs_inline_unsynthesized_diagnostic(sample_path)
|
|
291
|
+
Diagnostic.new(
|
|
292
|
+
path: ".rigor.yml",
|
|
293
|
+
line: 1,
|
|
294
|
+
column: 1,
|
|
295
|
+
message: "Inline rbs-inline annotations (`# @rbs …`, `#: <type>`) are present in your project " \
|
|
296
|
+
"(e.g. #{relative_signature_path(sample_path)}), but the `rbs-inline` library is not " \
|
|
297
|
+
"installed, so Rigor cannot read them and the annotated code stays `Dynamic[top]` — " \
|
|
298
|
+
"this run is quieter than your annotations intend, not cleaner. Install the " \
|
|
299
|
+
"`rbs-inline` gem (its dependency closure, `prism` + `rbs`, already ships with Rigor) " \
|
|
300
|
+
"and Rigor honours the annotations automatically (ADR-93); it is not bundled, keeping " \
|
|
301
|
+
"the core zero-dep (ADR-0).",
|
|
302
|
+
severity: :info,
|
|
303
|
+
rule: "rbs.coverage.inline-annotations-unsynthesized",
|
|
304
|
+
source_family: :builtin
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
|
|
227
308
|
# Robustness uplift companion (ADR-5) — when the project's `signature_paths:` RBS declared
|
|
228
309
|
# qualified names without their enclosing namespace, `RbsLoader` synthesizes the missing `module`s
|
|
229
310
|
# so the otherwise-inert signatures resolve. Surface a single `:info` diagnostic naming them so the
|
|
230
311
|
# user knows their sig set is malformed (`rbs validate` rejects it) and can fix it at the source.
|
|
231
312
|
# Authored `:info`: the analysis already succeeded; this is advisory, never a gate. Empty for a
|
|
232
313
|
# well-formed sig set.
|
|
314
|
+
# An unparseable `.rbs` under `signature_paths:` is QUARANTINED so the rest of the env survives
|
|
315
|
+
# (PR #50), which means the types it declares are silently absent — calls into them read
|
|
316
|
+
# `Dynamic[top]`, and the run gets *quieter*, not louder. The stderr banner alone never reached CI:
|
|
317
|
+
# it is not a diagnostic, so it is absent from `--format json` / SARIF / GitHub annotations / the LSP
|
|
318
|
+
# and cannot move the exit code. This puts it in the diagnostic stream where every channel sees it.
|
|
319
|
+
#
|
|
320
|
+
# Authored `:warning`, not `:error`: an existing green build must not turn red on upgrade, and the
|
|
321
|
+
# `rbs` gem's own parser moves across versions (ADR-79 keeps Rigor faithful to the project's `rbs`,
|
|
322
|
+
# so a version bump CAN newly reject a file that used to parse). Rejecting a broken sig set outright
|
|
323
|
+
# is a *new required discipline* — ADR-50 WD3 routes those through the bleeding-edge overlay, where
|
|
324
|
+
# the `reject-unparseable-signatures` feature promotes this rule to `:error` for anyone who opts in
|
|
325
|
+
# (and by default at the next major).
|
|
326
|
+
def rbs_quarantined_signature_diagnostics
|
|
327
|
+
quarantined = quarantined_signatures_snapshot
|
|
328
|
+
return [] if quarantined.empty?
|
|
329
|
+
|
|
330
|
+
[build_rbs_quarantined_signature_diagnostic(quarantined)]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# The twin of {#rbs_quarantined_signature_diagnostics}, one tier louder in consequence. Quarantine
|
|
334
|
+
# drops ONE unparseable file and keeps the rest of the env; a total build failure — typically a
|
|
335
|
+
# `signature_paths:` entry redeclaring a constant/class Rigor's bundled RBS already ships, which raises
|
|
336
|
+
# `RBS::DuplicatedDeclarationError` at resolve — collapses the WHOLE env to nil, so every type-of query
|
|
337
|
+
# degrades to `Dynamic[top]` and most rules stop firing: the run comes back EMPTY, which reads as clean.
|
|
338
|
+
# The stderr banner ({RbsLoader#warn_about_env_build_failure_once}) is not a diagnostic, so it never
|
|
339
|
+
# reached `--format json` / SARIF / CI annotations / the LSP; this puts it in the diagnostic stream so
|
|
340
|
+
# every channel sees it, and names the conflicting signature files off the raised error's `#decls`.
|
|
341
|
+
#
|
|
342
|
+
# Authored `:warning`, not `:error`, for the same reason as its quarantine twin: the conflict is
|
|
343
|
+
# *typically* between the user's `sig/` and Rigor's OWN bundled RBS, so an `:error` default would let a
|
|
344
|
+
# Rigor release turn a green build red with zero user change (AGENTS.md § FP discipline). The
|
|
345
|
+
# `reject-unparseable-signatures` bleeding-edge feature promotes it to `:error` for anyone who opts in
|
|
346
|
+
# (and by default at the next major).
|
|
347
|
+
def rbs_environment_build_failed_diagnostics
|
|
348
|
+
failure = env_build_failure_snapshot
|
|
349
|
+
return [] if failure.nil?
|
|
350
|
+
|
|
351
|
+
[build_rbs_environment_build_failed_diagnostic(failure)]
|
|
352
|
+
end
|
|
353
|
+
|
|
233
354
|
def rbs_synthesized_namespace_diagnostics
|
|
234
355
|
synthesized = synthesized_namespaces_snapshot
|
|
235
356
|
return [] if synthesized.nil? || synthesized.empty?
|
|
@@ -299,6 +420,54 @@ module Rigor
|
|
|
299
420
|
methods.size == 1 ? "required method" : "#{methods.size} required methods"
|
|
300
421
|
end
|
|
301
422
|
|
|
423
|
+
def build_rbs_quarantined_signature_diagnostic(quarantined)
|
|
424
|
+
sample_size = 5
|
|
425
|
+
sample = quarantined.first(sample_size).map { |path, _first_line| relative_signature_path(path) }
|
|
426
|
+
suffix = quarantined.size > sample_size ? ", and #{quarantined.size - sample_size} more" : ""
|
|
427
|
+
Diagnostic.new(
|
|
428
|
+
path: ".rigor.yml",
|
|
429
|
+
line: 1,
|
|
430
|
+
column: 1,
|
|
431
|
+
message: "#{quarantined.size} RBS file(s) under `signature_paths:` do not parse and were " \
|
|
432
|
+
"SKIPPED: #{sample.join(', ')}#{suffix}. The rest of your RBS environment still " \
|
|
433
|
+
"loaded, but the types those files declare are absent — calls into them read " \
|
|
434
|
+
"`Dynamic[top]`, so this run is quieter than it should be, not cleaner. Fix the " \
|
|
435
|
+
"parse error(s) (`rbs validate`) to restore that coverage.",
|
|
436
|
+
severity: :warning,
|
|
437
|
+
rule: "rbs.coverage.quarantined-signature",
|
|
438
|
+
source_family: :builtin
|
|
439
|
+
)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def build_rbs_environment_build_failed_diagnostic(failure)
|
|
443
|
+
error_class, first_line, buffers = failure
|
|
444
|
+
sample_size = 5
|
|
445
|
+
files = Array(buffers).map { |name| relative_signature_path(name.to_s) }
|
|
446
|
+
sample = files.first(sample_size)
|
|
447
|
+
suffix = files.size > sample_size ? ", and #{files.size - sample_size} more" : ""
|
|
448
|
+
conflicts = sample.empty? ? "" : " Conflicting signature file(s): #{sample.join(', ')}#{suffix}."
|
|
449
|
+
Diagnostic.new(
|
|
450
|
+
path: ".rigor.yml",
|
|
451
|
+
line: 1,
|
|
452
|
+
column: 1,
|
|
453
|
+
message: "The RBS environment failed to build (#{error_class}): #{first_line}.#{conflicts} " \
|
|
454
|
+
"A `signature_paths:` entry typically redeclares a constant or class that Rigor's " \
|
|
455
|
+
"bundled RBS already ships, which collapses the WHOLE environment to nil — every " \
|
|
456
|
+
"type-of query then reads `Dynamic[top]` and most diagnostics stop firing, so this " \
|
|
457
|
+
"run is EMPTY rather than clean. Remove the conflicting declaration from your `sig/` " \
|
|
458
|
+
"(`rbs validate`) to restore type coverage.",
|
|
459
|
+
severity: :warning,
|
|
460
|
+
rule: "rbs.coverage.environment-build-failed",
|
|
461
|
+
source_family: :builtin
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# The absolute path is what the loader records; the user thinks in project-relative terms.
|
|
466
|
+
def relative_signature_path(path)
|
|
467
|
+
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
468
|
+
path.start_with?(root) ? path.delete_prefix(root) : path
|
|
469
|
+
end
|
|
470
|
+
|
|
302
471
|
def build_rbs_synthesized_namespace_diagnostic(synthesized)
|
|
303
472
|
sample_size = 5
|
|
304
473
|
sample = synthesized.first(sample_size)
|
|
@@ -457,34 +626,10 @@ module Rigor
|
|
|
457
626
|
end
|
|
458
627
|
|
|
459
628
|
# ADR-8 § "Severity profile" — re-stamps each diagnostic's severity from the configured profile +
|
|
460
|
-
# per-rule overrides
|
|
461
|
-
#
|
|
629
|
+
# per-rule overrides, dropping any that resolve to `:off`. Delegates to the shared {SeverityStamp} so
|
|
630
|
+
# the ADR-87 WD4 boot-slimming hit path applies the identical final filter.
|
|
462
631
|
def apply_severity_profile(diagnostics)
|
|
463
|
-
diagnostics
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def stamp_severity(diagnostic)
|
|
467
|
-
return diagnostic if diagnostic.rule.nil?
|
|
468
|
-
|
|
469
|
-
resolved = Configuration::SeverityProfile.resolve(
|
|
470
|
-
rule: diagnostic.rule,
|
|
471
|
-
authored_severity: diagnostic.severity,
|
|
472
|
-
profile: @configuration.severity_profile,
|
|
473
|
-
overrides: @configuration.severity_overrides,
|
|
474
|
-
bleeding_edge_overrides: @configuration.bleeding_edge_severity_overrides
|
|
475
|
-
)
|
|
476
|
-
return nil if resolved == :off
|
|
477
|
-
return diagnostic if resolved == diagnostic.severity
|
|
478
|
-
|
|
479
|
-
Diagnostic.new(
|
|
480
|
-
path: diagnostic.path,
|
|
481
|
-
line: diagnostic.line,
|
|
482
|
-
column: diagnostic.column,
|
|
483
|
-
message: diagnostic.message,
|
|
484
|
-
severity: resolved,
|
|
485
|
-
rule: diagnostic.rule,
|
|
486
|
-
source_family: diagnostic.source_family
|
|
487
|
-
)
|
|
632
|
+
SeverityStamp.apply(diagnostics, @configuration)
|
|
488
633
|
end
|
|
489
634
|
|
|
490
635
|
private
|
|
@@ -513,6 +658,14 @@ module Rigor
|
|
|
513
658
|
@synthesized_namespaces_snapshot_reader.call
|
|
514
659
|
end
|
|
515
660
|
|
|
661
|
+
def quarantined_signatures_snapshot
|
|
662
|
+
@quarantined_signatures_snapshot_reader.call
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def env_build_failure_snapshot
|
|
666
|
+
@env_build_failure_snapshot_reader.call
|
|
667
|
+
end
|
|
668
|
+
|
|
516
669
|
def conformance_results_snapshot
|
|
517
670
|
@conformance_results_snapshot_reader.call
|
|
518
671
|
end
|
|
@@ -7,6 +7,7 @@ require_relative "../diagnostic"
|
|
|
7
7
|
require_relative "../worker_session"
|
|
8
8
|
require_relative "../run_stats"
|
|
9
9
|
require_relative "../../rbs_extended/conformance_checker"
|
|
10
|
+
require_relative "../../runtime/jit"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
12
13
|
module Analysis
|
|
@@ -50,11 +51,16 @@ module Rigor
|
|
|
50
51
|
boundary_cross_reporter:, source_rbs_synthesis_reporter:,
|
|
51
52
|
snapshots:, plugin_registry:, dependency_source_index:,
|
|
52
53
|
synthetic_method_index:, project_patched_methods:,
|
|
53
|
-
analyze_file:, project_scope_seed: -> { {} })
|
|
54
|
+
analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false)
|
|
54
55
|
@configuration = configuration
|
|
55
56
|
@cache_store = cache_store
|
|
56
57
|
@explain = explain
|
|
57
58
|
@workers = workers
|
|
59
|
+
# ADR-46 — the `--incremental` pool path. When true, pool-mode analysis is routed to the fork
|
|
60
|
+
# backend (the Ractor path lacks the cross-file seed and is off-default) and each worker records its
|
|
61
|
+
# slice's cross-file reads, marshalled back into `#collected_dependencies` for the runner's graph.
|
|
62
|
+
@record_dependencies = record_dependencies
|
|
63
|
+
@collected_dependencies = {}
|
|
58
64
|
@collect_stats = collect_stats
|
|
59
65
|
@buffer = buffer
|
|
60
66
|
@environment_override = environment_override
|
|
@@ -70,6 +76,11 @@ module Rigor
|
|
|
70
76
|
@analyze_file = analyze_file
|
|
71
77
|
end
|
|
72
78
|
|
|
79
|
+
# ADR-46 — the per-file cross-file read records the fork pool captured this run (empty unless
|
|
80
|
+
# `record_dependencies:` and a pool run occurred). The runner folds these into its `file_dependencies`
|
|
81
|
+
# so the incremental session's dependency graph is refreshed whether analysis ran sequential or pooled.
|
|
82
|
+
attr_reader :collected_dependencies
|
|
83
|
+
|
|
73
84
|
# ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
|
|
74
85
|
# silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
|
|
75
86
|
# cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
|
|
@@ -101,17 +112,7 @@ module Rigor
|
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
def analyze_files_sequentially(files, environment)
|
|
104
|
-
|
|
105
|
-
# #run can surface the malformed-RBS `:info` diagnostic without rebuilding the env. Gated on the
|
|
106
|
-
# project actually declaring `signature_paths:`: synthesis only matters for the project's own
|
|
107
|
-
# RBS, and `#synthesized_namespaces` forces the (otherwise-lazy) RBS env to build — doing so when
|
|
108
|
-
# there is no project sig set would warm `.rigor/cache` on a bare `--no-stats` run.
|
|
109
|
-
@snapshots.synthesized_namespaces =
|
|
110
|
-
project_signature_paths? ? (environment.rbs_loader&.synthesized_namespaces || []) : []
|
|
111
|
-
# `rigor:v1:conforms-to` lives only in the project's own `signature_paths:` RBS, so gate the scan
|
|
112
|
-
# the same way and reuse the already-built env (no extra RBS load).
|
|
113
|
-
@snapshots.conformance_results =
|
|
114
|
-
project_signature_paths? ? RbsExtended::ConformanceChecker.scan(environment.rbs_loader) : []
|
|
115
|
+
snapshot_project_signature_state(environment)
|
|
115
116
|
result = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
116
117
|
if @collect_stats
|
|
117
118
|
loader = environment.rbs_loader
|
|
@@ -121,6 +122,31 @@ module Rigor
|
|
|
121
122
|
result
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
# The whole-run findings about the project's OWN `signature_paths:` RBS — the namespaces the loader had
|
|
126
|
+
# to synthesize, the files it had to quarantine, the total env-build failure (if any), and the
|
|
127
|
+
# `rigor:v1:conforms-to` results. Snapshotted as small plain data (NOT the env) so `#run` can surface
|
|
128
|
+
# them as diagnostics without rebuilding it.
|
|
129
|
+
#
|
|
130
|
+
# All are gated on the project actually declaring `signature_paths:`, for one reason: each forces the
|
|
131
|
+
# (otherwise lazy) RBS env to build, and doing that when there is no project sig set would warm
|
|
132
|
+
# `.rigor/cache` on a bare `--no-stats` run. The loader has already memoised each answer, so reading
|
|
133
|
+
# them here is free.
|
|
134
|
+
def snapshot_project_signature_state(environment)
|
|
135
|
+
unless project_signature_paths?
|
|
136
|
+
@snapshots.synthesized_namespaces = []
|
|
137
|
+
@snapshots.quarantined_signatures = []
|
|
138
|
+
@snapshots.conformance_results = []
|
|
139
|
+
@snapshots.env_build_failure = nil
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
loader = environment.rbs_loader
|
|
144
|
+
@snapshots.synthesized_namespaces = loader&.synthesized_namespaces || []
|
|
145
|
+
@snapshots.quarantined_signatures = loader&.quarantined_signatures || []
|
|
146
|
+
@snapshots.env_build_failure = loader&.env_build_failure
|
|
147
|
+
@snapshots.conformance_results = RbsExtended::ConformanceChecker.scan(loader)
|
|
148
|
+
end
|
|
149
|
+
|
|
124
150
|
# Sequential-mode environment resolver. Returns the supplied `environment:` override (with the
|
|
125
151
|
# runner's fresh per-run reporter pair attached so dispatcher events route to THIS runner's
|
|
126
152
|
# diagnostics) when present; otherwise builds a fresh Environment per-call via
|
|
@@ -148,8 +174,19 @@ module Rigor
|
|
|
148
174
|
:sequential
|
|
149
175
|
end
|
|
150
176
|
|
|
151
|
-
# Routes pool-mode analysis to the selected backend.
|
|
177
|
+
# Routes pool-mode analysis to the selected backend. A `record_dependencies` (ADR-46 `--incremental`)
|
|
178
|
+
# run is pinned to the fork backend when `fork` is available: only the fork path marshals the
|
|
179
|
+
# per-worker dependency records back, and the Ractor path additionally lacks the cross-file
|
|
180
|
+
# `project_scope_seed` its worker would need to record complete edges. Without `fork` (Windows) a
|
|
181
|
+
# recording run degrades to sequential, which records correctly through `@analyze_file`.
|
|
152
182
|
def dispatch_pool(files)
|
|
183
|
+
if @record_dependencies
|
|
184
|
+
return analyze_files_in_fork_pool(files) if Process.respond_to?(:fork)
|
|
185
|
+
|
|
186
|
+
return analyze_files_sequentially_fallback(
|
|
187
|
+
files, reason: "incremental parallelism requires fork; recording sequentially"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
153
190
|
case pool_backend
|
|
154
191
|
when :ractor then analyze_files_in_pool(files)
|
|
155
192
|
when :fork then analyze_files_in_fork_pool(files)
|
|
@@ -314,7 +351,8 @@ module Rigor
|
|
|
314
351
|
synthetic_method_index: synthetic_method_index,
|
|
315
352
|
project_patched_methods: project_patched_methods,
|
|
316
353
|
project_scope_seed: project_scope_seed,
|
|
317
|
-
source_files: files
|
|
354
|
+
source_files: files,
|
|
355
|
+
record_dependencies: @record_dependencies
|
|
318
356
|
)
|
|
319
357
|
# Force the full RBS load on the parent so children copy-on-write inherit a warm Environment
|
|
320
358
|
# rather than each rebuilding it after the fork.
|
|
@@ -337,6 +375,9 @@ module Rigor
|
|
|
337
375
|
unless degraded.empty?
|
|
338
376
|
degraded.each { |path| results_by_path[path] = session.analyze(path) }
|
|
339
377
|
merge_worker_reporters(session.drain_reporters)
|
|
378
|
+
# The degraded slice was re-analysed on the parent session, so its dependency records live there,
|
|
379
|
+
# not in a child payload — fold them in alongside the successful children's.
|
|
380
|
+
@collected_dependencies.merge!(session.drain_dependencies) if @record_dependencies
|
|
340
381
|
end
|
|
341
382
|
|
|
342
383
|
diagnostics = Array(session.prepare_diagnostics) +
|
|
@@ -348,8 +389,19 @@ module Rigor
|
|
|
348
389
|
# copy-on-write-inherited session and writes the Marshal'd payload to `out_path`. `exit!` skips
|
|
349
390
|
# `at_exit` / stdio flush — the payload is already durable on disk by then.
|
|
350
391
|
def run_fork_worker(session, slice, out_path)
|
|
392
|
+
# Re-arm deferred YJIT in the child. The parent (`check` / `coverage`) armed `enable_after` on a
|
|
393
|
+
# background thread, but `fork` copies only the calling thread — the sleeping deadline thread does not
|
|
394
|
+
# survive into the child, so a worker forked before the deadline fires would otherwise run its whole
|
|
395
|
+
# (dominant) analysis slice un-JITted no matter how long it takes. If the parent already enabled YJIT
|
|
396
|
+
# before forking (a slow env build pushed the fork past the deadline), the child inherited the enabled
|
|
397
|
+
# state and this is a no-op; otherwise the child times its own slice and JITs the tail once it proves
|
|
398
|
+
# long enough — the same amortization contract, per worker. Parallel mode is opted into for large
|
|
399
|
+
# projects, so a worker slice reliably outlasts the deadline; a small-project worker finishes first and
|
|
400
|
+
# stays at no-YJIT parity, exactly as the sequential path would.
|
|
401
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
351
402
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
352
|
-
payload = { results: results, reporters: session.drain_reporters
|
|
403
|
+
payload = { results: results, reporters: session.drain_reporters,
|
|
404
|
+
dependencies: session.drain_dependencies }
|
|
353
405
|
File.binwrite(out_path, Marshal.dump(payload))
|
|
354
406
|
exit!(0)
|
|
355
407
|
rescue StandardError
|
|
@@ -362,6 +414,12 @@ module Rigor
|
|
|
362
414
|
loader = session.environment.rbs_loader
|
|
363
415
|
@snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
|
|
364
416
|
@snapshots.signature_paths = loader&.signature_paths || [].freeze
|
|
417
|
+
# The workers each quarantine the same broken file, but they report no diagnostics for it — the row is
|
|
418
|
+
# a whole-run one. Read it off the parent session's loader so a pooled run says exactly what a
|
|
419
|
+
# sequential one says. The same reasoning holds for a total env-build failure.
|
|
420
|
+
@snapshots.quarantined_signatures =
|
|
421
|
+
project_signature_paths? ? (loader&.quarantined_signatures || []) : []
|
|
422
|
+
@snapshots.env_build_failure = project_signature_paths? ? loader&.env_build_failure : nil
|
|
365
423
|
end
|
|
366
424
|
|
|
367
425
|
# Waits for every forked child, merges each successful payload into `results_by_path`, and returns
|
|
@@ -374,6 +432,7 @@ module Rigor
|
|
|
374
432
|
if payload
|
|
375
433
|
results_by_path.merge!(payload.fetch(:results))
|
|
376
434
|
merge_worker_reporters(payload.fetch(:reporters))
|
|
435
|
+
@collected_dependencies.merge!(payload.fetch(:dependencies, {})) if @record_dependencies
|
|
377
436
|
else
|
|
378
437
|
degraded.concat(child[:slice])
|
|
379
438
|
end
|
|
@@ -430,6 +489,9 @@ module Rigor
|
|
|
430
489
|
loader = environment.rbs_loader
|
|
431
490
|
@snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
|
|
432
491
|
@snapshots.signature_paths = loader&.signature_paths || [].freeze
|
|
492
|
+
@snapshots.quarantined_signatures =
|
|
493
|
+
project_signature_paths? ? (loader&.quarantined_signatures || []) : []
|
|
494
|
+
@snapshots.env_build_failure = project_signature_paths? ? loader&.env_build_failure : nil
|
|
433
495
|
diagnostics.unshift(
|
|
434
496
|
Diagnostic.new(
|
|
435
497
|
path: ".rigor.yml", line: 1, column: 1,
|
|
@@ -22,27 +22,17 @@ module Rigor
|
|
|
22
22
|
# predicate is injected (it keys on `@workers` / `@buffer`, owned by the Runner) so this collaborator
|
|
23
23
|
# never calls back into it.
|
|
24
24
|
class ProjectPrePasses
|
|
25
|
-
# Frozen bundle of the project-wide state the pre-passes produce
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# Frozen bundle of the EAGER (env-input) project-wide state the pre-passes produce — the slots whose
|
|
26
|
+
# products feed the RBS environment build. Mirrors the env-input ivars `apply_pre_passes_result`
|
|
27
|
+
# sets on the {Runner}. The cross-file discovery tables live in the separate {Discovery} bundle
|
|
28
|
+
# ({#discover}), deferred to the analysis path.
|
|
28
29
|
Result = Data.define(
|
|
29
30
|
:plugin_registry,
|
|
30
31
|
:dependency_source_index,
|
|
31
32
|
:cached_plugin_prepare_diagnostics,
|
|
32
33
|
:synthetic_method_index,
|
|
33
34
|
:project_patched_methods,
|
|
34
|
-
:pre_eval_diagnostics_from_scanner
|
|
35
|
-
:discovered_classes,
|
|
36
|
-
:discovered_def_nodes,
|
|
37
|
-
:discovered_singleton_def_nodes,
|
|
38
|
-
:discovered_def_sources,
|
|
39
|
-
:discovered_superclasses,
|
|
40
|
-
:discovered_includes,
|
|
41
|
-
:discovered_class_sources,
|
|
42
|
-
:discovered_method_visibilities,
|
|
43
|
-
:discovered_methods,
|
|
44
|
-
:data_member_layouts,
|
|
45
|
-
:struct_member_layouts
|
|
35
|
+
:pre_eval_diagnostics_from_scanner
|
|
46
36
|
)
|
|
47
37
|
|
|
48
38
|
# @param configuration [Rigor::Configuration]
|
|
@@ -58,10 +48,41 @@ module Rigor
|
|
|
58
48
|
@pool_mode_reader = pool_mode
|
|
59
49
|
end
|
|
60
50
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
51
|
+
# Frozen bundle of the cross-file discovery tables the two whole-project parse passes produce. Split
|
|
52
|
+
# out of {Result} so the {Runner} can defer these to the analysis (miss) path — a warm cache HIT never
|
|
53
|
+
# consumes them and so never pays the double parse (see {#discover} + `Runner#ensure_project_discovery`).
|
|
54
|
+
# The slot names mirror the discovery half of {Result} exactly.
|
|
55
|
+
Discovery = Data.define(
|
|
56
|
+
:discovered_classes,
|
|
57
|
+
:discovered_def_nodes,
|
|
58
|
+
:discovered_singleton_def_nodes,
|
|
59
|
+
:discovered_def_sources,
|
|
60
|
+
:discovered_singleton_def_sources,
|
|
61
|
+
:discovered_superclasses,
|
|
62
|
+
:discovered_includes,
|
|
63
|
+
:discovered_class_sources,
|
|
64
|
+
:discovered_method_visibilities,
|
|
65
|
+
:discovered_methods,
|
|
66
|
+
:data_member_layouts,
|
|
67
|
+
:struct_member_layouts
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Internal: drives every EAGER project-wide pre-pass — the ones whose products feed the RBS
|
|
71
|
+
# environment build (and therefore the cache key): plugin load + `#prepare`, the ADR-10
|
|
72
|
+
# dependency-source index, the ADR-16 synthetic-method scanner, and the ADR-17 project-patched
|
|
73
|
+
# scanner. The cross-file discovery tables are NOT built here (they are consumed only on the analysis
|
|
74
|
+
# path, so they defer to {#discover}); the {Result} carries `nil` for them, matching {#adopt_prebuilt}.
|
|
75
|
+
# Extracted so `#prepare_project_scan` and the prebuilt-less `#run` path share one implementation.
|
|
76
|
+
def run(expansion:)
|
|
77
|
+
# ADR-90 — resolve the target project's bundle for the plugin isolation layer BEFORE any plugin
|
|
78
|
+
# code runs: `#prepare` hooks (the rails-routes parse, the activerecord model index) already
|
|
79
|
+
# invoke `Plugin::Inflector`, and the `Environment.for_project` assignment of the same value
|
|
80
|
+
# happens only after the pre-passes. Without this, a standalone install degrades inflection for
|
|
81
|
+
# exactly the prepare-time checks even though the analyzed project's bundle carries activesupport.
|
|
82
|
+
Plugin::Isolation.target_bundle_root ||= Environment::BundleSigDiscovery.resolve_bundle_path(
|
|
83
|
+
bundle_path: @configuration.bundler_bundle_path,
|
|
84
|
+
auto_detect: @configuration.bundler_auto_detect
|
|
85
|
+
)&.to_s
|
|
65
86
|
plugin_registry = load_plugins
|
|
66
87
|
dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
67
88
|
# ADR-18 slice 3 — plugin prepare MUST run before the synthetic-method scanner so cross-plugin
|
|
@@ -91,31 +112,53 @@ module Rigor
|
|
|
91
112
|
pre_eval_outcome = Inference::ProjectPatchedScanner.scan(existing_pre_eval, buffer: @buffer)
|
|
92
113
|
project_patched_methods = pre_eval_outcome.registry
|
|
93
114
|
pre_eval_diagnostics_from_scanner = pre_eval_outcome.diagnostics
|
|
94
|
-
# Cross-file class discovery — walks every project file for `class Foo` / `module Bar`
|
|
95
|
-
# declarations so a `Foo.method_call` receiver in one file resolves a `class Foo` declared in a
|
|
96
|
-
# sibling file. Without this pre-pass each file's `discovered_classes` was per-file only, and
|
|
97
|
-
# lexical lookup fell back to stdlib `::Foo` for any user class shadowing a stdlib name (e.g.
|
|
98
|
-
# `Google::Cloud::Storage::File`). Cost is one extra parse pass over the project; small projects
|
|
99
|
-
# pay tens of ms, larger projects ~1s. Future optimisation can share parses with the existing
|
|
100
|
-
# scanner passes.
|
|
101
|
-
discovered_classes =
|
|
102
|
-
Inference::ScopeIndexer.discovered_classes_for_paths(expansion.fetch(:files), buffer: @buffer)
|
|
103
|
-
# ADR-24 slice 2 — cross-file def-node + class->superclass index so an implicit-self call inside a
|
|
104
|
-
# subclass resolves a superclass `def` declared in a sibling file. One extra parse pass over the
|
|
105
|
-
# project; shares the cost profile of the class-discovery pass above.
|
|
106
|
-
def_index =
|
|
107
|
-
Inference::ScopeIndexer.discovered_def_index_for_paths(expansion.fetch(:files), buffer: @buffer)
|
|
108
115
|
Result.new(
|
|
109
116
|
plugin_registry: plugin_registry,
|
|
110
117
|
dependency_source_index: dependency_source_index,
|
|
111
118
|
cached_plugin_prepare_diagnostics: cached_plugin_prepare_diagnostics,
|
|
112
119
|
synthetic_method_index: synthetic_method_index,
|
|
113
120
|
project_patched_methods: project_patched_methods,
|
|
114
|
-
pre_eval_diagnostics_from_scanner: pre_eval_diagnostics_from_scanner
|
|
115
|
-
|
|
121
|
+
pre_eval_diagnostics_from_scanner: pre_eval_diagnostics_from_scanner
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Internal: the DEFERRED cross-file discovery pre-pass, run lazily on the analysis (miss) path only —
|
|
126
|
+
# a warm cache HIT skips it entirely. Builds two whole-project tables in ONE walk:
|
|
127
|
+
# - Class discovery: every project file's `class Foo` / `module Bar` so a `Foo.method_call` receiver
|
|
128
|
+
# in one file resolves a `class Foo` declared in a sibling file (without it, lexical lookup fell
|
|
129
|
+
# back to stdlib `::Foo` for any user class shadowing a stdlib name).
|
|
130
|
+
# - ADR-24 slice 2 def-node / superclass / include index so an implicit-self call inside a subclass
|
|
131
|
+
# resolves a superclass `def` declared in a sibling file.
|
|
132
|
+
# Returns a frozen {Discovery} bundle; the {Runner} adopts it onto its discovery ivars. Both tables
|
|
133
|
+
# share ONE parse per file via `discovered_project_index_for_paths` — the file is parsed, both
|
|
134
|
+
# collectors walk the tree, and the AST is dropped before the next file (peak RSS stays flat).
|
|
135
|
+
def discover(expansion:)
|
|
136
|
+
build_discovery(
|
|
137
|
+
Inference::ScopeIndexer.discovered_project_index_for_paths(expansion.fetch(:files), buffer: @buffer)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ADR-85 WD2 — the seed-bundle twin of {#discover}. Rebuilds the discovery tables by folding the prior
|
|
142
|
+
# run's per-file bundles (re-walking only changed files) and returns `[Discovery, refreshed_bundles]`;
|
|
143
|
+
# the runner adopts the Discovery and exposes the bundles for the session to persist. On a cold run
|
|
144
|
+
# (`seed_bundles` empty) this is `#discover` plus a fresh bundle set — the discovery index is identical.
|
|
145
|
+
def discover_from_bundles(expansion:, seed_bundles:)
|
|
146
|
+
index = Inference::ScopeIndexer.discovered_project_index_incremental(
|
|
147
|
+
expansion.fetch(:files), seed_bundles: seed_bundles, buffer: @buffer
|
|
148
|
+
)
|
|
149
|
+
[build_discovery(index), index.fetch(:bundles)]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Builds the {Discovery} bundle from a `{ classes:, def_index: }` index hash (shared by the plain and
|
|
153
|
+
# seed-bundle discovery paths).
|
|
154
|
+
def build_discovery(index)
|
|
155
|
+
def_index = index.fetch(:def_index)
|
|
156
|
+
Discovery.new(
|
|
157
|
+
discovered_classes: index.fetch(:classes),
|
|
116
158
|
discovered_def_nodes: def_index.fetch(:def_nodes),
|
|
117
159
|
discovered_singleton_def_nodes: def_index.fetch(:singleton_def_nodes),
|
|
118
160
|
discovered_def_sources: def_index.fetch(:def_sources),
|
|
161
|
+
discovered_singleton_def_sources: def_index.fetch(:singleton_def_sources),
|
|
119
162
|
discovered_superclasses: def_index.fetch(:superclasses),
|
|
120
163
|
discovered_includes: def_index.fetch(:includes),
|
|
121
164
|
discovered_class_sources: def_index.fetch(:class_sources),
|
|
@@ -142,8 +185,8 @@ module Rigor
|
|
|
142
185
|
|
|
143
186
|
# Translates a prebuilt {ProjectScan} snapshot supplied to `Runner.new(prebuilt: ...)` into a
|
|
144
187
|
# {Result} the runner adopts the same way it adopts a fresh pre-pass run. The discovery tables are
|
|
145
|
-
# not part of the snapshot (the LSP path seeds an empty project scope),
|
|
146
|
-
# frozen-empty constructor defaults.
|
|
188
|
+
# not part of the snapshot (the LSP path seeds an empty project scope), and `Runner#ensure_project_discovery`
|
|
189
|
+
# is a no-op under `prebuilt`, so they stay at their frozen-empty constructor defaults.
|
|
147
190
|
def adopt_prebuilt(scan)
|
|
148
191
|
Result.new(
|
|
149
192
|
plugin_registry: scan.plugin_registry,
|
|
@@ -151,21 +194,24 @@ module Rigor
|
|
|
151
194
|
cached_plugin_prepare_diagnostics: scan.plugin_prepare_diagnostics,
|
|
152
195
|
synthetic_method_index: scan.synthetic_method_index,
|
|
153
196
|
project_patched_methods: scan.project_patched_methods,
|
|
154
|
-
pre_eval_diagnostics_from_scanner: scan.pre_eval_diagnostics
|
|
155
|
-
discovered_classes: nil,
|
|
156
|
-
discovered_def_nodes: nil,
|
|
157
|
-
discovered_singleton_def_nodes: nil,
|
|
158
|
-
discovered_def_sources: nil,
|
|
159
|
-
discovered_superclasses: nil,
|
|
160
|
-
discovered_includes: nil,
|
|
161
|
-
discovered_class_sources: nil,
|
|
162
|
-
discovered_method_visibilities: nil,
|
|
163
|
-
discovered_methods: nil,
|
|
164
|
-
data_member_layouts: nil,
|
|
165
|
-
struct_member_layouts: nil
|
|
197
|
+
pre_eval_diagnostics_from_scanner: scan.pre_eval_diagnostics
|
|
166
198
|
)
|
|
167
199
|
end
|
|
168
200
|
|
|
201
|
+
# ADR-88 WD1 — load the project's plugins and run every `#prepare` hook, returning the prepared
|
|
202
|
+
# {Rigor::Plugin::Registry} WITHOUT the synthetic-method / dependency-source / pre-eval scanners. The
|
|
203
|
+
# incremental fact-surface fingerprint probe ({Analysis::PluginFactFingerprint}) uses this to read the
|
|
204
|
+
# ADR-9 fact store + drive the ADR-60 producers without paying for the full pre-pass or building the RBS
|
|
205
|
+
# environment. `prepare` runs unconditionally here — the probe is always sequential (its `pool_mode?`
|
|
206
|
+
# reader returns false), so the pool-mode skip in {#run} does not apply. Prepare diagnostics are
|
|
207
|
+
# discarded: a plugin that raises in `#prepare` publishes no facts, so its surface is simply absent from
|
|
208
|
+
# the fingerprint, exactly as it would be absent from analysis.
|
|
209
|
+
def prepared_registry
|
|
210
|
+
registry = load_plugins
|
|
211
|
+
plugin_prepare_diagnostics(registry) unless registry.empty?
|
|
212
|
+
registry
|
|
213
|
+
end
|
|
214
|
+
|
|
169
215
|
# Returns the per-run shared `Plugin::FactStore` instance. All loaded plugins share this store
|
|
170
216
|
# through their respective `Plugin::Services` (the same instance is threaded by
|
|
171
217
|
# `Plugin::Loader.load`). Returns `nil` when no plugins are loaded.
|