rigortype 0.4.2 → 0.4.4
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- 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 +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- 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 +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- 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/mcp_command.rb +1 -1
- 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/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- 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/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- 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 +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- 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/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +21 -1
|
@@ -8,6 +8,7 @@ require_relative "../../inference/synthetic_method_scanner"
|
|
|
8
8
|
require_relative "../../inference/project_patched_scanner"
|
|
9
9
|
require_relative "../dependency_source_inference"
|
|
10
10
|
require_relative "../project_scan"
|
|
11
|
+
require_relative "../worker_session"
|
|
11
12
|
require_relative "../template_units"
|
|
12
13
|
require_relative "../template_unit_collector"
|
|
13
14
|
|
|
@@ -59,7 +60,7 @@ module Rigor
|
|
|
59
60
|
:unpositioned_mixins, :discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
|
|
60
61
|
:discovered_method_visibilities, :discovered_methods, :discovered_parameter_envelopes, :data_member_layouts,
|
|
61
62
|
:struct_member_layouts, :discovered_deferred_ranges, :discovered_refinements, :siblings,
|
|
62
|
-
:discovered_global_write_census
|
|
63
|
+
:discovered_global_write_census, :singleton_class_sources
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
# Internal: drives every EAGER project-wide pre-pass — the ones whose products feed the RBS
|
|
@@ -175,6 +176,8 @@ module Rigor
|
|
|
175
176
|
# object's `extend`s reach `Narrowing` from a sibling file the way its `include`s already do.
|
|
176
177
|
discovered_extends: def_index.fetch(:extends), unpositioned_mixins: def_index.fetch(:unpositioned_mixins),
|
|
177
178
|
discovered_class_sources: def_index.fetch(:class_sources),
|
|
179
|
+
# Issue #1752 — the files opening `class << Const`, for the class-declaration diff alone.
|
|
180
|
+
singleton_class_sources: def_index.fetch(:singleton_class_sources, {}),
|
|
178
181
|
# Issue #644 — the cross-file value-constant publication table and its write attribution.
|
|
179
182
|
constant_values: def_index.fetch(:constant_values), constant_sources: def_index.fetch(:constant_sources),
|
|
180
183
|
constant_writes: def_index.fetch(:constant_writes),
|
|
@@ -251,6 +254,35 @@ module Rigor
|
|
|
251
254
|
registry
|
|
252
255
|
end
|
|
253
256
|
|
|
257
|
+
# Issue #1541 — what a pooled run's workers would have produced from `#prepare`, for the coordinator's
|
|
258
|
+
# empty-closure branch, which dispatches none: the `#prepare` error rows and the run-scoped
|
|
259
|
+
# registrations (`#disclose_once` / `#emit_once`) made there. It runs on a THROWAWAY registry, so
|
|
260
|
+
# nothing `#prepare` records on a plugin instance reaches the coordinator-side instances
|
|
261
|
+
# {DiagnosticAggregator} reads; issue #1900 — the trust refusals are therefore handed back too, in
|
|
262
|
+
# the shape {Analysis::WorkerSession#drain_trust_refusals} gives a pooled cold run's.
|
|
263
|
+
# Returns the rows first, then the disclosure records, then the refusals.
|
|
264
|
+
def prepare_throwaway_registry
|
|
265
|
+
registry = load_plugins
|
|
266
|
+
# Issue #1900 — `#init` already ran (in `load_plugins`) and its refusals are the coordinator-side
|
|
267
|
+
# registry's to report, so only what `#prepare` refuses from here on is handed back.
|
|
268
|
+
baseline = WorkerSession.refusal_counts(registry)
|
|
269
|
+
rows = plugin_prepare_diagnostics(registry)
|
|
270
|
+
records = registry.plugins.flat_map do |plugin|
|
|
271
|
+
plugin_id = safe_plugin_id(plugin)
|
|
272
|
+
Array(plugin.run_disclosure_records).map { |record| record.merge(plugin_id: plugin_id) }
|
|
273
|
+
rescue StandardError
|
|
274
|
+
[]
|
|
275
|
+
end
|
|
276
|
+
[rows, records, trust_refusals(registry, baseline)]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Issue #1900 — `#prepare` on `plugin_registry` itself, returning its error rows. For the pool
|
|
280
|
+
# coordinator's sequential fallback, which analyses on the coordinator-side registry that {#run}
|
|
281
|
+
# left unprepared because the run is in pool mode.
|
|
282
|
+
def prepare_registry(plugin_registry)
|
|
283
|
+
plugin_prepare_diagnostics(plugin_registry)
|
|
284
|
+
end
|
|
285
|
+
|
|
254
286
|
# Returns the per-run shared `Plugin::FactStore` instance. All loaded plugins share this store
|
|
255
287
|
# through their respective `Plugin::Services` (the same instance is threaded by
|
|
256
288
|
# `Plugin::Loader.load`). Returns `nil` when no plugins are loaded.
|
|
@@ -262,6 +294,19 @@ module Rigor
|
|
|
262
294
|
|
|
263
295
|
private
|
|
264
296
|
|
|
297
|
+
def trust_refusals(registry, baseline)
|
|
298
|
+
registry.plugins.filter_map do |plugin|
|
|
299
|
+
summary = plugin.io_boundary.refusal_summary
|
|
300
|
+
next nil if summary.nil?
|
|
301
|
+
|
|
302
|
+
plugin_id = safe_plugin_id(plugin)
|
|
303
|
+
delta = summary[:count] - baseline.fetch(plugin_id, 0)
|
|
304
|
+
summary.merge(count: delta, plugin_id: plugin_id) if delta.positive?
|
|
305
|
+
rescue StandardError
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
265
310
|
def pool_mode?
|
|
266
311
|
@pool_mode_reader.call
|
|
267
312
|
end
|
|
@@ -297,8 +297,14 @@ 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, served_effect_collections: nil,
|
|
301
|
+
on_file_indexed: nil)
|
|
301
302
|
@configuration = configuration
|
|
303
|
+
# ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
|
|
304
|
+
# index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
|
|
305
|
+
# sees what `check` saw. A pooled file is indexed in a worker and never reaches it; the incremental session
|
|
306
|
+
# analyses forced targets with `workers: 0` for that reason.
|
|
307
|
+
@on_file_indexed = on_file_indexed
|
|
302
308
|
# Issues #796 / #794 — the previous full run's run-level rows, supplied only by
|
|
303
309
|
# {IncrementalSession} on a narrowed run; see {PoolCoordinator#replay_restored_run_level_rows}.
|
|
304
310
|
@restored_run_level_rows = restored_run_level_rows
|
|
@@ -306,6 +312,10 @@ module Rigor
|
|
|
306
312
|
# narrowed run does not analyse, spliced into this run's per-file stream by
|
|
307
313
|
# {#interleave_served_diagnostics}. Empty on every other run.
|
|
308
314
|
@served_per_file_diagnostics = served_per_file_diagnostics || {}
|
|
315
|
+
# Issue #1898 — `{path => FileCollection}` the {IncrementalSession} serves for the same unanalysed files, so the
|
|
316
|
+
# effect graph this run closes (and the envelope pass judges) spans the whole project, not just the closure.
|
|
317
|
+
# Never part of {#effect_collections_by_path}, which stays what THIS run collected. Empty on every other run.
|
|
318
|
+
@served_effect_collections = served_effect_collections || {}
|
|
309
319
|
@explain = explain
|
|
310
320
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
311
321
|
@plugin_requirer = plugin_requirer
|
|
@@ -431,6 +441,7 @@ module Rigor
|
|
|
431
441
|
# Issue #1367 — the project's `Inference::GlobalWriteCensus`, which the `global.*` write rules read.
|
|
432
442
|
@project_discovered_global_write_census = Set.new.freeze
|
|
433
443
|
@project_discovered_class_sources = {}.freeze
|
|
444
|
+
@project_singleton_class_sources = {}.freeze
|
|
434
445
|
# Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
|
|
435
446
|
# literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
|
|
436
447
|
# every per-file scope so `FOO = :sym` in one file reads as `:sym` in another; the second seeds
|
|
@@ -648,8 +659,8 @@ module Rigor
|
|
|
648
659
|
# subclasses (`subclass_of:<last segment>`).
|
|
649
660
|
def class_declarations
|
|
650
661
|
result = Hash.new { |hash, key| hash[key] = Set.new }
|
|
651
|
-
@project_discovered_class_sources.each do |
|
|
652
|
-
files.each { |file| result[file] << class_name }
|
|
662
|
+
[@project_discovered_class_sources, @project_singleton_class_sources].each do |sources|
|
|
663
|
+
sources.each { |class_name, files| files.each { |file| result[file] << class_name } }
|
|
653
664
|
end
|
|
654
665
|
Incremental.add_superclass_edges(result, @project_discovered_class_sources, @project_discovered_superclasses)
|
|
655
666
|
result.transform_values(&:freeze).freeze
|
|
@@ -845,7 +856,8 @@ module Rigor
|
|
|
845
856
|
analysis = serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
846
857
|
computed = false
|
|
847
858
|
diagnostics = @cache_store.fetch_or_validate(
|
|
848
|
-
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID,
|
|
859
|
+
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID,
|
|
860
|
+
key_descriptor: RunCacheKey.with_roots(key_descriptor, expansion.fetch(:roots)),
|
|
849
861
|
generation_cap: RunCacheKey::GENERATION_CAP
|
|
850
862
|
) do
|
|
851
863
|
computed = true
|
|
@@ -1374,7 +1386,8 @@ module Rigor
|
|
|
1374
1386
|
|
|
1375
1387
|
def build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
1376
1388
|
entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
|
|
1377
|
-
pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files
|
|
1389
|
+
pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files +
|
|
1390
|
+
root_presence_entries(expansion)
|
|
1378
1391
|
# #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
|
|
1379
1392
|
# `.rbs` files that existed while the run read them, so without these a signature file written after
|
|
1380
1393
|
# the run left the slot validating fresh and the warm run answered without it.
|
|
@@ -1385,6 +1398,20 @@ module Rigor
|
|
|
1385
1398
|
Cache::Descriptor.new(files: entries, globs: globs.uniq)
|
|
1386
1399
|
end
|
|
1387
1400
|
|
|
1401
|
+
# #1962 / #1555 — a kind row per path the run's answer depends on the presence and kind of, not only its
|
|
1402
|
+
# bytes. An analysis root: its path errors (`no such file or directory`, `not a Ruby file`) answer to whether
|
|
1403
|
+
# it exists and whether it is a directory or a file, while what a directory root holds is the analysed-file
|
|
1404
|
+
# set the key carries. A `pre_eval:` entry: `pre-eval.file-not-found` answers to whether it is a file (one
|
|
1405
|
+
# that is has its `:stat` row from {#pre_eval_file_entries}). A project signature root, configured or the
|
|
1406
|
+
# auto-detected `sig`: the loader reads a directory and a file differently, and #979's listing rows cover a
|
|
1407
|
+
# file appearing under a root that existed, not the root appearing. A kind row rather than an existence row,
|
|
1408
|
+
# because a directory replaced by a file of the same name leaves an existence row fresh. The `--incremental`
|
|
1409
|
+
# slot records existence rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}).
|
|
1410
|
+
def root_presence_entries(expansion)
|
|
1411
|
+
(expansion.fetch(:roots) + @configuration.pre_eval + signature_root_candidates)
|
|
1412
|
+
.uniq.map { |path| Cache::Descriptor::FileEntry.kind(path: path) }
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1388
1415
|
# Every file a plugin read or probed through its `IoBoundary` (#577 / #613), and every directory it listed
|
|
1389
1416
|
# (#954), as the boundaries stand when asked.
|
|
1390
1417
|
def plugin_boundary_rows
|
|
@@ -1574,7 +1601,7 @@ module Rigor
|
|
|
1574
1601
|
end
|
|
1575
1602
|
end
|
|
1576
1603
|
end
|
|
1577
|
-
private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries,
|
|
1604
|
+
private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries, :root_presence_entries,
|
|
1578
1605
|
:analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
|
|
1579
1606
|
:unanalysed_pre_eval_entries, :signature_rows, :within?
|
|
1580
1607
|
|
|
@@ -1748,6 +1775,8 @@ module Rigor
|
|
|
1748
1775
|
@project_discovered_prepends = discovery.discovered_prepends
|
|
1749
1776
|
@project_discovered_extends = discovery.discovered_extends
|
|
1750
1777
|
@project_unpositioned_mixins = discovery.unpositioned_mixins
|
|
1778
|
+
# Issue #1752 — the files that open `class << Const`, read only by {#class_declarations}.
|
|
1779
|
+
@project_singleton_class_sources = discovery.singleton_class_sources
|
|
1751
1780
|
end
|
|
1752
1781
|
|
|
1753
1782
|
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
@@ -1804,10 +1833,8 @@ module Rigor
|
|
|
1804
1833
|
# the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
|
|
1805
1834
|
# and every file was a discovered-not-analysed dependency of the run's cache entry.
|
|
1806
1835
|
#
|
|
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.
|
|
1836
|
+
# The widening decision itself compares the paths as spelled. `exclude:` patterns apply to both spellings
|
|
1837
|
+
# alike (#1576), so the configured absolute `lib` and the argument `lib` exclude the same files.
|
|
1811
1838
|
def project_discovery_expansion(paths, expansion)
|
|
1812
1839
|
return nil unless widen_discovery_to_project?(paths)
|
|
1813
1840
|
|
|
@@ -1901,8 +1928,10 @@ module Rigor
|
|
|
1901
1928
|
# The run's per-file effect collections in sorted path order — the sequential half merged with the
|
|
1902
1929
|
# pool's, exactly as {#file_dependencies} reconciles its two halves. Sorted, because the fold below
|
|
1903
1930
|
# it must not depend on pool-completion order.
|
|
1931
|
+
# Issue #1898 — a narrowed incremental run adds the collections its session served for the files it did not
|
|
1932
|
+
# analyse, under this run's own for any path both name.
|
|
1904
1933
|
def effect_collections
|
|
1905
|
-
effect_collections_by_path.sort_by { |path, _| path.to_s }.map(&:last)
|
|
1934
|
+
@served_effect_collections.merge(effect_collections_by_path).sort_by { |path, _| path.to_s }.map(&:last)
|
|
1906
1935
|
end
|
|
1907
1936
|
|
|
1908
1937
|
# Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
|
|
@@ -2017,7 +2046,9 @@ module Rigor
|
|
|
2017
2046
|
template_units: -> { template_units },
|
|
2018
2047
|
analyze_file: ->(path, environment) { analyze_file(path, environment) },
|
|
2019
2048
|
restored_run_level_rows: @restored_run_level_rows,
|
|
2020
|
-
feature_sources: -> { required_feature_sources }
|
|
2049
|
+
feature_sources: -> { required_feature_sources },
|
|
2050
|
+
plugin_prepare: -> { @pre_passes.prepare_throwaway_registry },
|
|
2051
|
+
prepare_coordinator_plugins: -> { @pre_passes.prepare_registry(@plugin_registry) }
|
|
2021
2052
|
)
|
|
2022
2053
|
@diagnostic_aggregator = DiagnosticAggregator.new(
|
|
2023
2054
|
configuration: @configuration,
|
|
@@ -2037,7 +2068,8 @@ module Rigor
|
|
|
2037
2068
|
definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
|
|
2038
2069
|
hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
|
|
2039
2070
|
conformance_results_snapshot: -> { @snapshots.conformance_results },
|
|
2040
|
-
pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures }
|
|
2071
|
+
pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures },
|
|
2072
|
+
pooled_trust_refusals: -> { @pool_coordinator.collected_trust_refusals }
|
|
2041
2073
|
)
|
|
2042
2074
|
end
|
|
2043
2075
|
|
|
@@ -2063,7 +2095,8 @@ module Rigor
|
|
|
2063
2095
|
)
|
|
2064
2096
|
RunStats.new(
|
|
2065
2097
|
wall_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_started_at,
|
|
2066
|
-
|
|
2098
|
+
# Issue #1529 — read lazily on the summary's first use, so a run that prints none never forks for it.
|
|
2099
|
+
peak_rss_probe: RunStats::CURRENT_PROCESS_PEAK_RSS,
|
|
2067
2100
|
# #392 — the template units count too: they are files this run parsed and typed, and a stats line
|
|
2068
2101
|
# that omitted them would under-report exactly the work a views-heavy project added.
|
|
2069
2102
|
target_files: expansion.fetch(:files).size + template_units.paths.size,
|
|
@@ -2176,7 +2209,8 @@ module Rigor
|
|
|
2176
2209
|
bad = []
|
|
2177
2210
|
Array(paths).each do |path|
|
|
2178
2211
|
if File.directory?(path)
|
|
2179
|
-
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns
|
|
2212
|
+
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns,
|
|
2213
|
+
base: @configuration.exclude_base))
|
|
2180
2214
|
# Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
|
|
2181
2215
|
# on-disk presence — `parse_source` reads bytes from the buffer's physical path. Common case: LSP
|
|
2182
2216
|
# client editing a brand-new file.
|
|
@@ -2188,7 +2222,7 @@ module Rigor
|
|
|
2188
2222
|
bad << [path, "no such file or directory"]
|
|
2189
2223
|
end
|
|
2190
2224
|
end
|
|
2191
|
-
{ files: files, errors: path_expansion_errors(bad, any_files: files.any?) }
|
|
2225
|
+
{ files: files, roots: Array(paths).map(&:to_s), errors: path_expansion_errors(bad, any_files: files.any?) }
|
|
2192
2226
|
end
|
|
2193
2227
|
|
|
2194
2228
|
# A bad path *among valid ones* is a warn-and-skip — the run still does useful work, so `rigor check
|
|
@@ -2591,10 +2625,11 @@ module Rigor
|
|
|
2591
2625
|
# the rule is active or `record_self_calls:` opted in.
|
|
2592
2626
|
index = nil
|
|
2593
2627
|
self_call_record = with_self_call_recording(path) do
|
|
2594
|
-
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
2628
|
+
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope, widen_clauses: true)
|
|
2595
2629
|
end
|
|
2596
2630
|
self_call_misses = self_call_record ? self_call_record.calls : []
|
|
2597
2631
|
diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
|
|
2632
|
+
notify_file_indexed(path, parse_result, scope, index)
|
|
2598
2633
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
2599
2634
|
rescue Errno::ENOENT => e
|
|
2600
2635
|
[
|
|
@@ -2618,6 +2653,14 @@ module Rigor
|
|
|
2618
2653
|
]
|
|
2619
2654
|
end
|
|
2620
2655
|
|
|
2656
|
+
# ADR-113 WD2 — the `on_file_indexed` observer. It reads; an error inside it never changes this file's
|
|
2657
|
+
# diagnostics, so it is contained here rather than reaching {#analyze_file_body}'s crash row.
|
|
2658
|
+
def notify_file_indexed(path, parse_result, scope, index)
|
|
2659
|
+
@on_file_indexed&.call(path, parse_result, scope, index)
|
|
2660
|
+
rescue StandardError
|
|
2661
|
+
nil
|
|
2662
|
+
end
|
|
2663
|
+
|
|
2621
2664
|
# ADR-53 B4 — the built-in node collectors and the plugin node rules share ONE traversal of the file.
|
|
2622
2665
|
# The collectors are built here (they need the completed `index`) and populated by the converged
|
|
2623
2666
|
# plugin walk; `node_results` carries the per-plugin node-rule output. Both the built-in `diagnose`
|
|
@@ -2636,7 +2679,8 @@ module Rigor
|
|
|
2636
2679
|
node_collectors: node_collectors,
|
|
2637
2680
|
stated_runtime_ruby: @configuration.stated_runtime_ruby
|
|
2638
2681
|
)
|
|
2639
|
-
|
|
2682
|
+
plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
2683
|
+
diagnostics + CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
|
|
2640
2684
|
end
|
|
2641
2685
|
|
|
2642
2686
|
# ADR-24 slice 4a — runs `block` (the typing pass) with the self-call recorder active when either
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "check_rules/rule_ids"
|
|
3
4
|
require_relative "diagnostic"
|
|
4
5
|
require_relative "../configuration/severity_profile"
|
|
5
6
|
|
|
@@ -9,14 +10,46 @@ module Rigor
|
|
|
9
10
|
# overrides and drops any diagnostic resolving to `:off`. Rules emit with their authored severity; the
|
|
10
11
|
# profile is the final filter.
|
|
11
12
|
#
|
|
13
|
+
# Issue #1895 — the `.rigor.yml` `disable:` list is applied here too, ahead of the profile, with the same
|
|
14
|
+
# token expansion and match per-file analysis uses ({CheckRules.config_disabled?}), but only to a built-in
|
|
15
|
+
# check-rule row (see {.check_rule_row?}): the rows the diagnostic policy already says `disable:` reaches. Most
|
|
16
|
+
# are filtered where they were produced; this catches the ones that are not (the `effect.annotations-unchecked`
|
|
17
|
+
# residual under `disable: [all]`) on every run shape (sequential, pool, `--incremental`, a warm cache hit).
|
|
18
|
+
# Issue #1953 — a plugin's `plugin.<id>.*` row answers here to its qualified id (`plugin.<id>.<rule>`) or to
|
|
19
|
+
# `plugin.<id>`, never to a bare id or `all`. The run-level rows outside the check-rule catalogue
|
|
20
|
+
# (`rbs.coverage.*`, `dynamic.dependency-source.*`, the plugin loader, `plugin_trust.*`, `pool-degraded`) keep
|
|
21
|
+
# answering to `severity_overrides:` only, by any spelling.
|
|
22
|
+
#
|
|
12
23
|
# Extracted (ADR-87 WD4) so the miss path ({Runner::DiagnosticAggregator}) and the boot-slimming hit path
|
|
13
24
|
# ({RunCacheProbe}, which never loads the aggregator) apply the IDENTICAL final filter — a warm HIT's
|
|
14
25
|
# served diagnostics are byte-identical to what a full run would print.
|
|
15
26
|
module SeverityStamp
|
|
16
27
|
module_function
|
|
17
28
|
|
|
29
|
+
CHECK_RULES = CheckRules::ALL_RULES.to_set.freeze
|
|
30
|
+
private_constant :CHECK_RULES
|
|
31
|
+
|
|
18
32
|
def apply(diagnostics, configuration)
|
|
19
|
-
|
|
33
|
+
disabled = CheckRules.expand_rule_tokens(configuration.disabled_rules)
|
|
34
|
+
diagnostics.filter_map do |diagnostic|
|
|
35
|
+
next nil if config_disabled_row?(diagnostic, disabled)
|
|
36
|
+
|
|
37
|
+
stamp(diagnostic, configuration)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A built-in check-rule row answers to the expanded list by its id or `all`; a plugin's `plugin.<id>.*` row
|
|
42
|
+
# by its qualified id or `plugin.<id>` only ({CheckRules.plugin_row_disabled?}, issue #1953). Every other
|
|
43
|
+
# row is out of the list's reach.
|
|
44
|
+
def config_disabled_row?(diagnostic, disabled)
|
|
45
|
+
return CheckRules.config_disabled?(diagnostic.rule, disabled) if check_rule_row?(diagnostic)
|
|
46
|
+
|
|
47
|
+
CheckRules.plugin_row_disabled?(diagnostic, disabled)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A built-in check-rule row: its id is in the CheckRules catalogue and no plugin emitted it.
|
|
51
|
+
def check_rule_row?(diagnostic)
|
|
52
|
+
diagnostic.source_family == Diagnostic::DEFAULT_SOURCE_FAMILY && CHECK_RULES.include?(diagnostic.rule)
|
|
20
53
|
end
|
|
21
54
|
|
|
22
55
|
def stamp(diagnostic, configuration)
|
|
@@ -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)
|
|
@@ -241,7 +241,8 @@ module Rigor
|
|
|
241
241
|
node_collectors: node_collectors,
|
|
242
242
|
stated_runtime_ruby: @configuration.stated_runtime_ruby
|
|
243
243
|
)
|
|
244
|
-
|
|
244
|
+
plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
245
|
+
diagnostics += CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
|
|
245
246
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
246
247
|
rescue Errno::ENOENT => e
|
|
247
248
|
[analyzer_error(path, e.message)]
|
|
@@ -336,6 +337,45 @@ module Rigor
|
|
|
336
337
|
end
|
|
337
338
|
end
|
|
338
339
|
|
|
340
|
+
# Issue #1900 — `{plugin id => refused-read count}` over `registry`'s boundaries right now: the baseline
|
|
341
|
+
# a later drain subtracts, so it reports only the refusals made after the baseline was taken.
|
|
342
|
+
def self.refusal_counts(registry)
|
|
343
|
+
registry.plugins.to_h do |plugin|
|
|
344
|
+
summary = begin
|
|
345
|
+
plugin.io_boundary.refusal_summary
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
[safe_plugin_id(plugin), summary ? summary[:count] : 0]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def self.safe_plugin_id(plugin)
|
|
354
|
+
plugin.manifest.id
|
|
355
|
+
rescue StandardError
|
|
356
|
+
plugin.class.to_s
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Issue #1900 — the {Plugin::IoBoundary} trust-policy refusals this session's plugin instances made
|
|
360
|
+
# since the previous drain, one `{plugin_id:, count:, first_path:, nearest_root:}` per plugin that hit
|
|
361
|
+
# any, in registry order. Unlike {#drain_run_disclosures} each drain reports only what is new — the
|
|
362
|
+
# boundary keeps its running total and this session remembers how much of it was already reported —
|
|
363
|
+
# because the coordinator SUMS the drains: a fork child inherits the parent's `#prepare` refusals,
|
|
364
|
+
# and reporting them again would count them once per worker. `first_path` is the boundary's first
|
|
365
|
+
# refusal overall, which is the delta's own first path whenever nothing was reported before it, and
|
|
366
|
+
# the coordinator keeps the first-merged path otherwise. It is not baselined after `#init`, so a plugin
|
|
367
|
+
# whose `#init` refuses differently here than on the coordinator registry can have this session's
|
|
368
|
+
# `#init` path named. Marshal-clean and Ractor-copyable.
|
|
369
|
+
def drain_trust_refusals
|
|
370
|
+
return [] if @plugin_registry.empty?
|
|
371
|
+
|
|
372
|
+
@plugin_registry.plugins.filter_map do |plugin|
|
|
373
|
+
trust_refusal_delta(plugin)
|
|
374
|
+
rescue StandardError
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
339
379
|
private
|
|
340
380
|
|
|
341
381
|
# Mirrors {Runner#seed_project_scope}: applies the cross-file pre-pass discovery tables the
|
|
@@ -402,6 +442,10 @@ module Rigor
|
|
|
402
442
|
def run_plugin_prepare
|
|
403
443
|
return [] if @plugin_registry.empty?
|
|
404
444
|
|
|
445
|
+
# Issue #1900 — per plugin id, how much of its boundary's count is already reported. Starts at what
|
|
446
|
+
# `#init` refused while the registry was materialised: `#init` runs on the coordinator-side registry
|
|
447
|
+
# too, which reports those reads itself, so counting them here as well would double them.
|
|
448
|
+
@reported_refusal_counts = self.class.refusal_counts(@plugin_registry)
|
|
405
449
|
@plugin_registry.plugins.flat_map do |plugin|
|
|
406
450
|
plugin.prepare(plugin.services)
|
|
407
451
|
[]
|
|
@@ -490,6 +534,18 @@ module Rigor
|
|
|
490
534
|
)
|
|
491
535
|
end
|
|
492
536
|
|
|
537
|
+
def trust_refusal_delta(plugin)
|
|
538
|
+
summary = plugin.io_boundary.refusal_summary
|
|
539
|
+
return nil if summary.nil?
|
|
540
|
+
|
|
541
|
+
plugin_id = safe_plugin_id(plugin)
|
|
542
|
+
delta = summary[:count] - @reported_refusal_counts.fetch(plugin_id, 0)
|
|
543
|
+
return nil unless delta.positive?
|
|
544
|
+
|
|
545
|
+
@reported_refusal_counts[plugin_id] = summary[:count]
|
|
546
|
+
summary.merge(count: delta, plugin_id: plugin_id)
|
|
547
|
+
end
|
|
548
|
+
|
|
493
549
|
def safe_plugin_id(plugin)
|
|
494
550
|
plugin.manifest.id
|
|
495
551
|
rescue StandardError
|
|
@@ -82,7 +82,12 @@ module Rigor
|
|
|
82
82
|
# an unchanged file would fold as "no possible fact" where a cold walk records one.
|
|
83
83
|
# v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
|
|
84
84
|
# carries it empty, so an unchanged file's conditional visibility would fold as certain.
|
|
85
|
-
|
|
85
|
+
# v19: #1555 / #1962 — the run-result dependency descriptor now carries a `:kind` row per analysis root,
|
|
86
|
+
# `pre_eval:` entry and project signature root (configured, or the auto-detected `sig`), so one that
|
|
87
|
+
# appears, vanishes or changes kind invalidates the warm entry. A
|
|
88
|
+
# pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
|
|
89
|
+
# entries must read as misses once and rebuild carrying the rows.
|
|
90
|
+
SCHEMA_VERSION = 19
|
|
86
91
|
|
|
87
92
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
88
93
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -96,7 +101,7 @@ module Rigor
|
|
|
96
101
|
# It MUST NOT be placed in a descriptor used as a cache KEY (its value carries machine-local,
|
|
97
102
|
# per-run-nondeterministic stat data); {RbsDescriptor.build}'s env-cache `files` therefore stay
|
|
98
103
|
# `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
|
|
99
|
-
VALID_COMPARATORS = %i[digest stat mtime exists].freeze
|
|
104
|
+
VALID_COMPARATORS = %i[digest stat mtime kind exists].freeze
|
|
100
105
|
|
|
101
106
|
# The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
|
|
102
107
|
# {Descriptor#file_entry_fresh?} compares the live answer against.
|
|
@@ -163,6 +168,32 @@ module Rigor
|
|
|
163
168
|
new(path: path, comparator: :exists, value: PRESENT)
|
|
164
169
|
end
|
|
165
170
|
|
|
171
|
+
# The four `:kind` values, in the order {Analysis::Runner}'s path expansion asks: a directory, a regular
|
|
172
|
+
# file, something else that exists (a FIFO, a socket), nothing (a missing path or a dangling symlink).
|
|
173
|
+
# Each predicate follows symlinks, as the expansion's do.
|
|
174
|
+
KIND_DIRECTORY = "directory"
|
|
175
|
+
KIND_FILE = "file"
|
|
176
|
+
KIND_OTHER = "other"
|
|
177
|
+
KIND_ABSENT = "absent"
|
|
178
|
+
|
|
179
|
+
# #1962 — the kind row: fresh while `path` is still the same kind of thing, stale once it appears,
|
|
180
|
+
# vanishes, or a directory and a file trade places. It is the row for an analysis root, whose
|
|
181
|
+
# diagnostics (`no such file or directory`, `not a Ruby file`) answer to exactly that and to nothing
|
|
182
|
+
# else about the path, and for a `pre_eval:` entry and a signature root: an existence row would read a
|
|
183
|
+
# directory replaced by a file of the same name as fresh (ADR-45 WD1b's bound), and what the path holds
|
|
184
|
+
# has rows of its own (the analysed-file set the key carries, a signature root's `.rbs` rows).
|
|
185
|
+
def self.kind(path:)
|
|
186
|
+
new(path: path, comparator: :kind, value: kind_of(path))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.kind_of(path)
|
|
190
|
+
if File.directory?(path) then KIND_DIRECTORY
|
|
191
|
+
elsif File.file?(path) then KIND_FILE
|
|
192
|
+
elsif File.exist?(path) then KIND_OTHER
|
|
193
|
+
else KIND_ABSENT
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
166
197
|
# @return whether this row records the path as absent (see {.absent}).
|
|
167
198
|
def absent?
|
|
168
199
|
comparator == :exists && value == ABSENT
|
|
@@ -413,10 +444,11 @@ module Rigor
|
|
|
413
444
|
|
|
414
445
|
# File-comparator strictness ordering. `:stat` is strictest (it carries the content digest AND a stat
|
|
415
446
|
# tuple, so it validates content-authoritatively while short-circuiting on an unmoved stat); `:digest` is
|
|
416
|
-
# deterministic across machines; `:mtime` is cheaper but local; `:
|
|
417
|
-
# `:
|
|
418
|
-
#
|
|
419
|
-
|
|
447
|
+
# deterministic across machines; `:mtime` is cheaper but local; `:kind` answers what the path is and
|
|
448
|
+
# `:exists` only whether it is there, the weakest signal. Ranking `:stat` and `:digest` apart guarantees a
|
|
449
|
+
# path contributed under both never raises a value {Conflict} (their `value` strings differ by
|
|
450
|
+
# construction) — the stricter one wins and its value is used.
|
|
451
|
+
COMPARATOR_STRICTNESS = { stat: 4, digest: 3, mtime: 2, kind: 1, exists: 0 }.freeze
|
|
420
452
|
private_constant :COMPARATOR_STRICTNESS
|
|
421
453
|
|
|
422
454
|
# Composes any number of descriptors into a single descriptor whose slots are the union of the inputs'
|
|
@@ -505,6 +537,8 @@ module Rigor
|
|
|
505
537
|
FileDigest.stat_fresh?(entry.path, entry.value)
|
|
506
538
|
when :mtime
|
|
507
539
|
File.exist?(entry.path) && File.mtime(entry.path).to_i.to_s == entry.value
|
|
540
|
+
when :kind
|
|
541
|
+
FileEntry.kind_of(entry.path) == entry.value
|
|
508
542
|
when :exists
|
|
509
543
|
File.exist?(entry.path).to_s == entry.value
|
|
510
544
|
else
|
|
@@ -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
|