rigortype 0.4.2 → 0.4.3
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 +1 -1
- data/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -24,18 +24,31 @@ module Rigor
|
|
|
24
24
|
# `class_decl_paths` (a frozen `Hash<String, String>` populated once per environment by the RBS loader;
|
|
25
25
|
# ~1000-2000 entries × one `String#start_with?`).
|
|
26
26
|
class RunStats
|
|
27
|
-
attr_reader :wall_seconds,
|
|
27
|
+
attr_reader :wall_seconds,
|
|
28
28
|
:target_files,
|
|
29
29
|
:rbs_classes_total, :rbs_classes_project_sig, :rbs_classes_bundled,
|
|
30
30
|
:gem_walk_classes, :gem_walk_gems, :rbs_attribution_available
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# The live probe a run's summary uses: reads this process's RSS when first asked. The probe is a constant
|
|
33
|
+
# lambda, so the RunStats it rides on never captures the Runner (and its analysis state) through `Result`.
|
|
34
|
+
CURRENT_PROCESS_PEAK_RSS = Ractor.make_shareable(-> { RunStats.peak_rss_bytes })
|
|
35
|
+
|
|
36
|
+
# Issue #1529 — the peak RSS is read on the first {#peak_rss_bytes} call, not when the summary is built. A run
|
|
37
|
+
# that prints no summary (`--incremental` recheck, `--no-stats`) therefore never forks `ps` or reads `/proc`.
|
|
38
|
+
# The read stays in this process: the summary is built and printed by the process that ran the analysis.
|
|
39
|
+
#
|
|
40
|
+
# @param peak_rss_bytes — a value to report directly (specs, or a caller that already read it). Ignored when
|
|
41
|
+
# `peak_rss_probe:` is given.
|
|
42
|
+
# @param peak_rss_probe — a zero-argument callable returning the RSS in bytes, called at most once, lazily.
|
|
43
|
+
def initialize(wall_seconds:, # rubocop:disable Metrics/ParameterLists
|
|
33
44
|
target_files:,
|
|
34
45
|
rbs_classes_total:, rbs_classes_project_sig:, rbs_classes_bundled:,
|
|
35
46
|
gem_walk_classes:, gem_walk_gems:,
|
|
36
|
-
rbs_attribution_available: true)
|
|
47
|
+
rbs_attribution_available: true, peak_rss_bytes: nil, peak_rss_probe: nil)
|
|
37
48
|
@wall_seconds = wall_seconds
|
|
38
|
-
@
|
|
49
|
+
@peak_rss_probe = peak_rss_probe || -> { peak_rss_bytes }
|
|
50
|
+
# A frozen RunStats cannot memoise into an ivar, so the first read lands in this (unfrozen) Hash.
|
|
51
|
+
@peak_rss_memo = {}
|
|
39
52
|
@target_files = target_files
|
|
40
53
|
@rbs_classes_total = rbs_classes_total
|
|
41
54
|
@rbs_classes_project_sig = rbs_classes_project_sig
|
|
@@ -46,6 +59,15 @@ module Rigor
|
|
|
46
59
|
freeze
|
|
47
60
|
end
|
|
48
61
|
|
|
62
|
+
# The peak RSS in bytes, read once on first call and memoised (nil when no route answers). The check command
|
|
63
|
+
# forces the first call straight after the analysis (#1529), before `--coverage` or any output, so the figure
|
|
64
|
+
# covers the analysis alone. On Linux it is the kernel's VmHWM high-water mark, so a later read is never below
|
|
65
|
+
# the run-end value. On macOS it is `ps`'s current RSS, so the reading is the process as it stood at that
|
|
66
|
+
# first call, not a high-water mark.
|
|
67
|
+
def peak_rss_bytes
|
|
68
|
+
@peak_rss_memo.fetch(:value) { @peak_rss_memo[:value] = @peak_rss_probe.call }
|
|
69
|
+
end
|
|
70
|
+
|
|
49
71
|
# Reports the process's resident set size in bytes. Source ordering: `/proc/self/status` (Linux — reads
|
|
50
72
|
# `VmHWM:`, the peak RSS the kernel records) first; otherwise `ps -o rss= -p <pid>` (macOS / BSD —
|
|
51
73
|
# reports CURRENT RSS, the closest universally-available proxy). Returns nil when neither route works
|
|
@@ -146,7 +168,7 @@ module Rigor
|
|
|
146
168
|
end
|
|
147
169
|
out.puts("#{prefix}Process")
|
|
148
170
|
out.puts("#{prefix} Wall time: #{Kernel.format('%.2fs', @wall_seconds)}")
|
|
149
|
-
out.puts("#{prefix} Memory peak: #{format_bytes(
|
|
171
|
+
out.puts("#{prefix} Memory peak: #{format_bytes(peak_rss_bytes)}")
|
|
150
172
|
end
|
|
151
173
|
|
|
152
174
|
def to_h
|
|
@@ -159,7 +181,7 @@ module Rigor
|
|
|
159
181
|
gem_walk_classes: @gem_walk_classes,
|
|
160
182
|
gem_walk_gems: @gem_walk_gems,
|
|
161
183
|
wall_seconds: @wall_seconds,
|
|
162
|
-
peak_rss_bytes:
|
|
184
|
+
peak_rss_bytes: peak_rss_bytes
|
|
163
185
|
}
|
|
164
186
|
end
|
|
165
187
|
|
|
@@ -128,7 +128,32 @@ module Rigor
|
|
|
128
128
|
source_family: :builtin
|
|
129
129
|
)
|
|
130
130
|
end
|
|
131
|
-
|
|
131
|
+
scanned = Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
|
|
132
|
+
suppress_pre_eval(not_found + scanned)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The ordinary suppression pipeline for the `pre-eval.*` rows: the `# rigor:disable` /
|
|
136
|
+
# `# rigor:disable-file` comments of the file a row is positioned in (a `pre_eval:` file need not be
|
|
137
|
+
# under `paths:`, so its comments are read here rather than by per-file analysis) and the project's
|
|
138
|
+
# `disable:` list. A row positioned at `.rigor.yml` has no Ruby comments (`.yml` is skipped) and answers to
|
|
139
|
+
# `disable:` only.
|
|
140
|
+
# Stream order is the output contract, so survivors are picked out of the original sequence.
|
|
141
|
+
def suppress_pre_eval(diagnostics)
|
|
142
|
+
kept = diagnostics.group_by(&:path).flat_map do |path, group|
|
|
143
|
+
CheckRules.filter_suppressed(
|
|
144
|
+
group, comments: pre_eval_comments(path), disabled_rules: @configuration.disabled_rules
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
kept_ids = kept.to_set(&:object_id)
|
|
148
|
+
diagnostics.select { |diagnostic| kept_ids.include?(diagnostic.object_id) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def pre_eval_comments(path)
|
|
152
|
+
return [].freeze unless File.file?(path) && !path.end_with?(".rbs", ".yml", ".yaml")
|
|
153
|
+
|
|
154
|
+
Prism.parse_file(path).comments
|
|
155
|
+
rescue StandardError
|
|
156
|
+
[].freeze
|
|
132
157
|
end
|
|
133
158
|
|
|
134
159
|
def diagnostic_from_hash(hash)
|
|
@@ -46,7 +46,7 @@ module Rigor
|
|
|
46
46
|
synthetic_method_index:, project_patched_methods:,
|
|
47
47
|
analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false,
|
|
48
48
|
restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty },
|
|
49
|
-
feature_sources: -> { {} })
|
|
49
|
+
feature_sources: -> { {} }, plugin_prepare: -> { [[], []] })
|
|
50
50
|
@configuration = configuration
|
|
51
51
|
@cache_store = cache_store
|
|
52
52
|
@explain = explain
|
|
@@ -84,6 +84,10 @@ module Rigor
|
|
|
84
84
|
# inherits the same compiled bytes.
|
|
85
85
|
@template_units_reader = template_units
|
|
86
86
|
@analyze_file = analyze_file
|
|
87
|
+
# Issue #1541 — `() -> [rows, disclosure records]`, a `#prepare` run on a throwaway registry. Read
|
|
88
|
+
# only by {#serve_empty_closure}: a pooled run's `#prepare` happens inside its workers, and an
|
|
89
|
+
# empty closure dispatches none.
|
|
90
|
+
@plugin_prepare = plugin_prepare
|
|
87
91
|
# Issues #796 / #794 — the previous full run's run-level rows, handed down by
|
|
88
92
|
# {Analysis::IncrementalSession} on a NARROWED run only (a recheck's closure, a
|
|
89
93
|
# `--verify-incremental` partition). A full run leaves this nil and derives everything itself.
|
|
@@ -146,10 +150,8 @@ module Rigor
|
|
|
146
150
|
# (`expansion.fetch(:files)`), independent of any `analyze_only` narrowing of `files`; see
|
|
147
151
|
# {#serve_empty_closure} for what an empty `files` does with it.
|
|
148
152
|
def analyze_files(files, environment: nil, project_files: nil)
|
|
149
|
-
if files.empty?
|
|
150
|
-
|
|
151
|
-
return []
|
|
152
|
-
end
|
|
153
|
+
return serve_empty_closure(environment, project_files) if files.empty?
|
|
154
|
+
|
|
153
155
|
# Issue #784 / #793 — `files` is what this run ANALYSES; `source_files` is what its environment
|
|
154
156
|
# is BUILT over, and the two are the same only on a full run. A narrowed run (`analyze_only`: an
|
|
155
157
|
# incremental closure, a `--verify-incremental` partition) used to build its environment over the
|
|
@@ -199,7 +201,16 @@ module Rigor
|
|
|
199
201
|
# whether anyone could have demanded anything at all: with no files, nobody could have, and an
|
|
200
202
|
# empty project keeps paying no env build. Keyed on the project's files rather than on
|
|
201
203
|
# `analyze_only`, because a recheck over an EMPTY project narrows to `Set[]`, which is non-nil.
|
|
204
|
+
#
|
|
205
|
+
# Issue #1541 — and, in pool mode, its `prepare` rows. A pooled run's `#prepare` happens inside the
|
|
206
|
+
# workers and its rows reach the stream through their first payload, so the coordinator-side
|
|
207
|
+
# pre-pass skips it ({ProjectPrePasses#run}) and {DiagnosticAggregator#pre_file_diagnostics} omits
|
|
208
|
+
# the cached rows; with no worker dispatched the row appeared nowhere, and a plugin whose `#prepare`
|
|
209
|
+
# raises turned a red project green on a nothing-changed `--incremental --workers N` recheck. They
|
|
210
|
+
# are returned as the run's per-file stream head, where the pool path puts them (`.rigor.yml`).
|
|
211
|
+
# Sequential, whose pre-pass already ran `#prepare`, returns no rows here.
|
|
202
212
|
def serve_empty_closure(environment, project_files)
|
|
213
|
+
prepare_rows = pool_mode? ? pooled_prepare_rows : []
|
|
203
214
|
env = environment || @environment_override
|
|
204
215
|
# Resolved over the project's OWN file list — not `[]`, which would drop every plugin-synthesized
|
|
205
216
|
# virtual RBS (`Environment.collect_virtual_rbs` short-circuits on an empty list) and scan a
|
|
@@ -217,9 +228,16 @@ module Rigor
|
|
|
217
228
|
# `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
|
|
218
229
|
# an environment, and force nothing when they did not.
|
|
219
230
|
record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
|
|
220
|
-
|
|
231
|
+
record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed_restored_run_level_rows?
|
|
232
|
+
prepare_rows
|
|
233
|
+
end
|
|
221
234
|
|
|
222
|
-
|
|
235
|
+
# Issue #1541 — the pooled empty closure's `#prepare`: the rows go to the caller, the run-scoped
|
|
236
|
+
# registrations into the table the workers' payloads would have filled.
|
|
237
|
+
def pooled_prepare_rows
|
|
238
|
+
rows, records = @plugin_prepare.call
|
|
239
|
+
merge_run_disclosures(records)
|
|
240
|
+
Array(rows)
|
|
223
241
|
end
|
|
224
242
|
|
|
225
243
|
# Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
|
|
@@ -700,8 +718,9 @@ module Rigor
|
|
|
700
718
|
# Issue #1051, known exception: run-scoped disclosures a plugin registered from `#prepare` are NOT
|
|
701
719
|
# recovered here — nor, since #1060, a positioned `#emit_once` batch registered there, which rides
|
|
702
720
|
# the same table. There is no session to drain — this backend builds none on the coordinator — and
|
|
703
|
-
# `#prepare`
|
|
704
|
-
#
|
|
721
|
+
# `#prepare` does not run on the coordinator-side registry under pool mode (only an empty closure
|
|
722
|
+
# runs it, on a throwaway registry; see {#serve_empty_closure}), so a prepare-time disclosure lives
|
|
723
|
+
# only inside the Ractor that died with it. A disclosure registered
|
|
705
724
|
# from `#diagnostics_for_file` IS recovered, because the re-analysis below runs the coordinator-side
|
|
706
725
|
# plugin instances that {DiagnosticAggregator#plugin_run_disclosure_diagnostics} reads directly.
|
|
707
726
|
# Left as is: recovering the rest means running `#prepare` on the coordinator registry, which would
|
|
@@ -59,7 +59,7 @@ module Rigor
|
|
|
59
59
|
:unpositioned_mixins, :discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
|
|
60
60
|
:discovered_method_visibilities, :discovered_methods, :discovered_parameter_envelopes, :data_member_layouts,
|
|
61
61
|
:struct_member_layouts, :discovered_deferred_ranges, :discovered_refinements, :siblings,
|
|
62
|
-
:discovered_global_write_census
|
|
62
|
+
:discovered_global_write_census, :singleton_class_sources
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Internal: drives every EAGER project-wide pre-pass — the ones whose products feed the RBS
|
|
@@ -175,6 +175,8 @@ module Rigor
|
|
|
175
175
|
# object's `extend`s reach `Narrowing` from a sibling file the way its `include`s already do.
|
|
176
176
|
discovered_extends: def_index.fetch(:extends), unpositioned_mixins: def_index.fetch(:unpositioned_mixins),
|
|
177
177
|
discovered_class_sources: def_index.fetch(:class_sources),
|
|
178
|
+
# Issue #1752 — the files opening `class << Const`, for the class-declaration diff alone.
|
|
179
|
+
singleton_class_sources: def_index.fetch(:singleton_class_sources, {}),
|
|
178
180
|
# Issue #644 — the cross-file value-constant publication table and its write attribution.
|
|
179
181
|
constant_values: def_index.fetch(:constant_values), constant_sources: def_index.fetch(:constant_sources),
|
|
180
182
|
constant_writes: def_index.fetch(:constant_writes),
|
|
@@ -251,6 +253,24 @@ module Rigor
|
|
|
251
253
|
registry
|
|
252
254
|
end
|
|
253
255
|
|
|
256
|
+
# Issue #1541 — what a pooled run's workers would have produced from `#prepare`, for the coordinator's
|
|
257
|
+
# empty-closure branch, which dispatches none: the `#prepare` error rows and the run-scoped
|
|
258
|
+
# registrations (`#disclose_once` / `#emit_once`) made there. It runs on a THROWAWAY registry, so
|
|
259
|
+
# nothing `#prepare` records on a plugin instance (a trust refusal, say) reaches the coordinator-side
|
|
260
|
+
# instances {DiagnosticAggregator} reads — a pooled cold run never shows those either.
|
|
261
|
+
# Returns the rows first, then the disclosure records.
|
|
262
|
+
def prepare_throwaway_registry
|
|
263
|
+
registry = load_plugins
|
|
264
|
+
rows = plugin_prepare_diagnostics(registry)
|
|
265
|
+
records = registry.plugins.flat_map do |plugin|
|
|
266
|
+
plugin_id = safe_plugin_id(plugin)
|
|
267
|
+
Array(plugin.run_disclosure_records).map { |record| record.merge(plugin_id: plugin_id) }
|
|
268
|
+
rescue StandardError
|
|
269
|
+
[]
|
|
270
|
+
end
|
|
271
|
+
[rows, records]
|
|
272
|
+
end
|
|
273
|
+
|
|
254
274
|
# Returns the per-run shared `Plugin::FactStore` instance. All loaded plugins share this store
|
|
255
275
|
# through their respective `Plugin::Services` (the same instance is threaded by
|
|
256
276
|
# `Plugin::Loader.load`). Returns `nil` when no plugins are loaded.
|
|
@@ -297,8 +297,13 @@ module Rigor
|
|
|
297
297
|
record_dependencies: false, record_self_calls: false, analyze_only: nil,
|
|
298
298
|
seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
|
|
299
299
|
discovery_seed: nil, no_tolerated_effects: false,
|
|
300
|
-
restored_run_level_rows: nil, served_per_file_diagnostics: nil)
|
|
300
|
+
restored_run_level_rows: nil, served_per_file_diagnostics: nil, on_file_indexed: nil)
|
|
301
301
|
@configuration = configuration
|
|
302
|
+
# ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
|
|
303
|
+
# index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
|
|
304
|
+
# sees what `check` saw. A pooled file is indexed in a worker and never reaches it; the incremental session
|
|
305
|
+
# analyses forced targets with `workers: 0` for that reason.
|
|
306
|
+
@on_file_indexed = on_file_indexed
|
|
302
307
|
# Issues #796 / #794 — the previous full run's run-level rows, supplied only by
|
|
303
308
|
# {IncrementalSession} on a narrowed run; see {PoolCoordinator#replay_restored_run_level_rows}.
|
|
304
309
|
@restored_run_level_rows = restored_run_level_rows
|
|
@@ -431,6 +436,7 @@ module Rigor
|
|
|
431
436
|
# Issue #1367 — the project's `Inference::GlobalWriteCensus`, which the `global.*` write rules read.
|
|
432
437
|
@project_discovered_global_write_census = Set.new.freeze
|
|
433
438
|
@project_discovered_class_sources = {}.freeze
|
|
439
|
+
@project_singleton_class_sources = {}.freeze
|
|
434
440
|
# Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
|
|
435
441
|
# literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
|
|
436
442
|
# every per-file scope so `FOO = :sym` in one file reads as `:sym` in another; the second seeds
|
|
@@ -648,8 +654,8 @@ module Rigor
|
|
|
648
654
|
# subclasses (`subclass_of:<last segment>`).
|
|
649
655
|
def class_declarations
|
|
650
656
|
result = Hash.new { |hash, key| hash[key] = Set.new }
|
|
651
|
-
@project_discovered_class_sources.each do |
|
|
652
|
-
files.each { |file| result[file] << class_name }
|
|
657
|
+
[@project_discovered_class_sources, @project_singleton_class_sources].each do |sources|
|
|
658
|
+
sources.each { |class_name, files| files.each { |file| result[file] << class_name } }
|
|
653
659
|
end
|
|
654
660
|
Incremental.add_superclass_edges(result, @project_discovered_class_sources, @project_discovered_superclasses)
|
|
655
661
|
result.transform_values(&:freeze).freeze
|
|
@@ -1748,6 +1754,8 @@ module Rigor
|
|
|
1748
1754
|
@project_discovered_prepends = discovery.discovered_prepends
|
|
1749
1755
|
@project_discovered_extends = discovery.discovered_extends
|
|
1750
1756
|
@project_unpositioned_mixins = discovery.unpositioned_mixins
|
|
1757
|
+
# Issue #1752 — the files that open `class << Const`, read only by {#class_declarations}.
|
|
1758
|
+
@project_singleton_class_sources = discovery.singleton_class_sources
|
|
1751
1759
|
end
|
|
1752
1760
|
|
|
1753
1761
|
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
@@ -1804,10 +1812,8 @@ module Rigor
|
|
|
1804
1812
|
# the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
|
|
1805
1813
|
# and every file was a discovered-not-analysed dependency of the run's cache entry.
|
|
1806
1814
|
#
|
|
1807
|
-
# The widening decision itself
|
|
1808
|
-
#
|
|
1809
|
-
# excludes (a relative `lib/gen/**` pattern never matches `/abs/lib/gen/x.rb`), and treating the two as the
|
|
1810
|
-
# same root would drop those files from discovery.
|
|
1815
|
+
# The widening decision itself compares the paths as spelled. `exclude:` patterns apply to both spellings
|
|
1816
|
+
# alike (#1576), so the configured absolute `lib` and the argument `lib` exclude the same files.
|
|
1811
1817
|
def project_discovery_expansion(paths, expansion)
|
|
1812
1818
|
return nil unless widen_discovery_to_project?(paths)
|
|
1813
1819
|
|
|
@@ -2017,7 +2023,8 @@ module Rigor
|
|
|
2017
2023
|
template_units: -> { template_units },
|
|
2018
2024
|
analyze_file: ->(path, environment) { analyze_file(path, environment) },
|
|
2019
2025
|
restored_run_level_rows: @restored_run_level_rows,
|
|
2020
|
-
feature_sources: -> { required_feature_sources }
|
|
2026
|
+
feature_sources: -> { required_feature_sources },
|
|
2027
|
+
plugin_prepare: -> { @pre_passes.prepare_throwaway_registry }
|
|
2021
2028
|
)
|
|
2022
2029
|
@diagnostic_aggregator = DiagnosticAggregator.new(
|
|
2023
2030
|
configuration: @configuration,
|
|
@@ -2063,7 +2070,8 @@ module Rigor
|
|
|
2063
2070
|
)
|
|
2064
2071
|
RunStats.new(
|
|
2065
2072
|
wall_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_started_at,
|
|
2066
|
-
|
|
2073
|
+
# Issue #1529 — read lazily on the summary's first use, so a run that prints none never forks for it.
|
|
2074
|
+
peak_rss_probe: RunStats::CURRENT_PROCESS_PEAK_RSS,
|
|
2067
2075
|
# #392 — the template units count too: they are files this run parsed and typed, and a stats line
|
|
2068
2076
|
# that omitted them would under-report exactly the work a views-heavy project added.
|
|
2069
2077
|
target_files: expansion.fetch(:files).size + template_units.paths.size,
|
|
@@ -2176,7 +2184,8 @@ module Rigor
|
|
|
2176
2184
|
bad = []
|
|
2177
2185
|
Array(paths).each do |path|
|
|
2178
2186
|
if File.directory?(path)
|
|
2179
|
-
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns
|
|
2187
|
+
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns,
|
|
2188
|
+
base: @configuration.exclude_base))
|
|
2180
2189
|
# Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
|
|
2181
2190
|
# on-disk presence — `parse_source` reads bytes from the buffer's physical path. Common case: LSP
|
|
2182
2191
|
# client editing a brand-new file.
|
|
@@ -2591,10 +2600,11 @@ module Rigor
|
|
|
2591
2600
|
# the rule is active or `record_self_calls:` opted in.
|
|
2592
2601
|
index = nil
|
|
2593
2602
|
self_call_record = with_self_call_recording(path) do
|
|
2594
|
-
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
2603
|
+
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope, widen_clauses: true)
|
|
2595
2604
|
end
|
|
2596
2605
|
self_call_misses = self_call_record ? self_call_record.calls : []
|
|
2597
2606
|
diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
2607
|
+
notify_file_indexed(path, parse_result, scope, index)
|
|
2598
2608
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
2599
2609
|
rescue Errno::ENOENT => e
|
|
2600
2610
|
[
|
|
@@ -2618,6 +2628,14 @@ module Rigor
|
|
|
2618
2628
|
]
|
|
2619
2629
|
end
|
|
2620
2630
|
|
|
2631
|
+
# ADR-113 WD2 — the `on_file_indexed` observer. It reads; an error inside it never changes this file's
|
|
2632
|
+
# diagnostics, so it is contained here rather than reaching {#analyze_file_body}'s crash row.
|
|
2633
|
+
def notify_file_indexed(path, parse_result, scope, index)
|
|
2634
|
+
@on_file_indexed&.call(path, parse_result, scope, index)
|
|
2635
|
+
rescue StandardError
|
|
2636
|
+
nil
|
|
2637
|
+
end
|
|
2638
|
+
|
|
2621
2639
|
# ADR-53 B4 — the built-in node collectors and the plugin node rules share ONE traversal of the file.
|
|
2622
2640
|
# The collectors are built here (they need the completed `index`) and populated by the converged
|
|
2623
2641
|
# plugin walk; `node_results` carries the per-plugin node-rule output. Both the built-in `diagnose`
|
|
@@ -228,7 +228,7 @@ module Rigor
|
|
|
228
228
|
scope = @template_units.seed(
|
|
229
229
|
seed_project_scope(Scope.empty(environment: @environment, source_path: path)), path
|
|
230
230
|
)
|
|
231
|
-
index = Inference::ScopeIndexer.index(root, default_scope: scope)
|
|
231
|
+
index = Inference::ScopeIndexer.index(root, default_scope: scope, widen_clauses: true)
|
|
232
232
|
# ADR-53 B4 — built-in collectors + plugin node rules share one walk.
|
|
233
233
|
node_collectors = CheckRules.build_node_collectors(path, index, root)
|
|
234
234
|
node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
|
|
@@ -59,6 +59,19 @@ module Rigor
|
|
|
59
59
|
# `apps/` and `tool/` are not loaded by an analysis and stay out.
|
|
60
60
|
SOURCE_DIRECTORIES = %w[lib plugins].freeze
|
|
61
61
|
|
|
62
|
+
# Engine DATA the analysis reads at runtime, relative to the gem root (issue #1492). A data edit moves
|
|
63
|
+
# diagnostics as surely as a `lib/` edit, yet the caches that key only on `signature_paths` plus this
|
|
64
|
+
# digest (the `--incremental` snapshot fingerprint, `Protection::MutationCache`) see nothing else.
|
|
65
|
+
#
|
|
66
|
+
# - YAML: `data/builtins` (`MethodCatalog::DATA_ROOT`) and `data/effects` (`Effects::Catalog`,
|
|
67
|
+
# `Effects::Registry`).
|
|
68
|
+
# - RBS: every `.rbs` under `data/` (`core_overlay`, `vendored_gem_sigs`, `capability_roles`,
|
|
69
|
+
# `gem_overlay`) plus each bundled plugin's `plugins/*/sig`. `RbsDescriptor` digests the same files
|
|
70
|
+
# into the run-result and env caches, but those are the only two consumers that do.
|
|
71
|
+
#
|
|
72
|
+
# Tooling-only trees (`data/oss-sweep`) are not read by an analysis and stay out.
|
|
73
|
+
DATA_GLOBS = %w[data/builtins/**/*.yml data/effects/**/*.yml data/**/*.rbs plugins/*/sig/**/*.rbs].freeze
|
|
74
|
+
|
|
62
75
|
# The one directory whose absence means "this is not an engine tree". `plugins/` may legitimately be
|
|
63
76
|
# missing (a slimmed install contributes nothing); a missing `lib/` would silently shrink the digest
|
|
64
77
|
# to whatever else happened to be there, which is exactly the weakening this module forbids.
|
|
@@ -153,8 +166,9 @@ module Rigor
|
|
|
153
166
|
!File.exist?(File.join(root, ".git"))
|
|
154
167
|
end
|
|
155
168
|
|
|
156
|
-
# A SHA-256 over every engine `.rb` file: its ROOT-RELATIVE
|
|
157
|
-
# re-cloning the checkout) followed by its bytes, in sorted path
|
|
169
|
+
# A SHA-256 over every engine `.rb` file and {DATA_GLOBS} data file: its ROOT-RELATIVE
|
|
170
|
+
# path (so the digest survives moving or re-cloning the checkout) followed by its bytes, in sorted path
|
|
171
|
+
# order.
|
|
158
172
|
#
|
|
159
173
|
# The walk itself is not memoised — {.process_identity} is where a production caller gets the
|
|
160
174
|
# once-per-process value, and this stays the computation so a spec can point it at another tree.
|
|
@@ -178,12 +192,12 @@ module Rigor
|
|
|
178
192
|
raise Unavailable, "engine source under #{root} could not be read: #{e.message}"
|
|
179
193
|
end
|
|
180
194
|
|
|
181
|
-
# Sorted absolute paths of every engine `.rb` file
|
|
195
|
+
# Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
|
|
196
|
+
# the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
|
|
182
197
|
def source_files(root)
|
|
183
|
-
SOURCE_DIRECTORIES.flat_map
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
end.sort
|
|
198
|
+
sources = SOURCE_DIRECTORIES.flat_map { |relative| Dir.glob(File.join(root, relative, "**", "*.rb")) }
|
|
199
|
+
data = DATA_GLOBS.flat_map { |pattern| Dir.glob(File.join(root, pattern)) }
|
|
200
|
+
(sources + data).uniq.sort
|
|
187
201
|
end
|
|
188
202
|
end
|
|
189
203
|
end
|
|
@@ -260,7 +260,11 @@ module Rigor
|
|
|
260
260
|
# {EngineSource.process_identity} closes it, and answers nil for a version-pinned tree — which adds no
|
|
261
261
|
# part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
|
|
262
262
|
# survive the upgrade untouched.
|
|
263
|
-
|
|
263
|
+
#
|
|
264
|
+
# Issue #1533 — `explain:` is the run's `--explain` flag. It adds `:info` rows to the per-file diagnostics
|
|
265
|
+
# the snapshot caches, so a snapshot written without it holds none and one written with it holds rows a
|
|
266
|
+
# plain run must not print. No part for the default, so every plain run's fingerprint is as it was.
|
|
267
|
+
def self.fingerprint(configuration:, roots:, explain: false)
|
|
264
268
|
bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
|
|
265
269
|
parts = [
|
|
266
270
|
"engine:#{Rigor::VERSION}:#{SCHEMA}",
|
|
@@ -279,14 +283,11 @@ module Rigor
|
|
|
279
283
|
# dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
|
|
280
284
|
# files, the set the run's environment scans; no part when nothing is required, so every other
|
|
281
285
|
# project's fingerprint is as it was.
|
|
282
|
-
features =
|
|
283
|
-
configuration, Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns)
|
|
284
|
-
)
|
|
286
|
+
features = required_features(configuration, roots)
|
|
285
287
|
parts << "required-features:#{features.join(',')}" unless features.empty?
|
|
286
288
|
# ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
|
|
287
289
|
# wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
|
|
288
|
-
|
|
289
|
-
parts << "shadow-harness:#{shadow}" if shadow
|
|
290
|
+
parts.concat(run_mode_parts(explain))
|
|
290
291
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
291
292
|
rescue StandardError
|
|
292
293
|
# {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
|
|
@@ -297,6 +298,12 @@ module Rigor
|
|
|
297
298
|
nil
|
|
298
299
|
end
|
|
299
300
|
|
|
301
|
+
def self.required_features(configuration, roots)
|
|
302
|
+
files = Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns,
|
|
303
|
+
base: configuration.exclude_base)
|
|
304
|
+
Environment::RequiredFeatures.for_configuration(configuration, files)
|
|
305
|
+
end
|
|
306
|
+
|
|
300
307
|
# ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
|
|
301
308
|
# constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
|
|
302
309
|
# No part for a project that lists no gem, so its fingerprint is what it was.
|
|
@@ -314,6 +321,15 @@ module Rigor
|
|
|
314
321
|
end
|
|
315
322
|
private_class_method :digest_file_if_present
|
|
316
323
|
|
|
324
|
+
# The parts naming how the run was asked to behave rather than what it reads: the ADR-116 WD5 shadow harness
|
|
325
|
+
# (the snapshot's rows are only as checked as the run that wrote them; nil with the harness off) and
|
|
326
|
+
# `--explain` (#1533).
|
|
327
|
+
def self.run_mode_parts(explain)
|
|
328
|
+
shadow = Analysis::ShadowHarness.cache_identity
|
|
329
|
+
[("shadow-harness:#{shadow}" if shadow), ("explain:true" if explain)].compact
|
|
330
|
+
end
|
|
331
|
+
private_class_method :run_mode_parts
|
|
332
|
+
|
|
317
333
|
# Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
|
|
318
334
|
# even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
|
|
319
335
|
def self.digest_pre_eval(paths)
|
|
@@ -346,8 +362,20 @@ module Rigor
|
|
|
346
362
|
end
|
|
347
363
|
private_class_method :digest_signature_paths
|
|
348
364
|
|
|
349
|
-
|
|
350
|
-
|
|
365
|
+
# Issue #1867 — one snapshot FILE per fingerprint mode. A run whose fingerprint carries run-mode parts
|
|
366
|
+
# (`--explain`, the ADR-116 shadow harness) keeps its own file so it neither reads nor overwrites the
|
|
367
|
+
# plain run's warm snapshot, and the reverse. The plain file name is unchanged. The fingerprint still
|
|
368
|
+
# carries the mode parts, so the files never cross-serve.
|
|
369
|
+
def initialize(root:, explain: false)
|
|
370
|
+
@path = File.join(root.to_s, "incremental", self.class.file_name(explain: explain))
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# `snapshot.bin` for a plain run; `snapshot-<mode digest>.bin` when {run_mode_parts} is non-empty.
|
|
374
|
+
def self.file_name(explain: false)
|
|
375
|
+
parts = run_mode_parts(explain)
|
|
376
|
+
return "snapshot.bin" if parts.empty?
|
|
377
|
+
|
|
378
|
+
"snapshot-#{Digest::SHA256.hexdigest(parts.join("\x00"))[0, 12]}.bin"
|
|
351
379
|
end
|
|
352
380
|
|
|
353
381
|
attr_reader :path
|