rigortype 0.4.0 → 0.4.1
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 +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -297,11 +297,15 @@ 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)
|
|
300
|
+
restored_run_level_rows: nil, served_per_file_diagnostics: nil)
|
|
301
301
|
@configuration = configuration
|
|
302
302
|
# Issues #796 / #794 — the previous full run's run-level rows, supplied only by
|
|
303
303
|
# {IncrementalSession} on a narrowed run; see {PoolCoordinator#replay_restored_run_level_rows}.
|
|
304
304
|
@restored_run_level_rows = restored_run_level_rows
|
|
305
|
+
# Issue #1524 — `{path => rows}` the {IncrementalSession} serves from its per-file cache for the files a
|
|
306
|
+
# narrowed run does not analyse, spliced into this run's per-file stream by
|
|
307
|
+
# {#interleave_served_diagnostics}. Empty on every other run.
|
|
308
|
+
@served_per_file_diagnostics = served_per_file_diagnostics || {}
|
|
305
309
|
@explain = explain
|
|
306
310
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
307
311
|
@plugin_requirer = plugin_requirer
|
|
@@ -353,6 +357,12 @@ module Rigor
|
|
|
353
357
|
@cached_effect_ancestry = nil
|
|
354
358
|
@cached_collections_loader = nil
|
|
355
359
|
@run_dependency_descriptor = nil
|
|
360
|
+
# ADR-45 WD2 (#1507) — the plugin reads each file's analysis made (`{path => [row]}`), and the boundary
|
|
361
|
+
# rows as they stood around the per-file loop; see {#incremental_slot_rows}. Recorded only by a
|
|
362
|
+
# dependency-recording (incremental) run.
|
|
363
|
+
@analysis_reads = {}
|
|
364
|
+
@pre_analysis_plugin_rows = nil
|
|
365
|
+
@post_analysis_plugin_rows = nil
|
|
356
366
|
# Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
|
|
357
367
|
# See `self_undefined_rule_active?`.
|
|
358
368
|
@self_undefined_rule_active = nil
|
|
@@ -414,6 +424,7 @@ module Rigor
|
|
|
414
424
|
@project_discovered_prepends = {}.freeze
|
|
415
425
|
# Issue #898 — the singleton-side twin of the include table (`extend M` / `extend self`).
|
|
416
426
|
@project_discovered_extends = {}.freeze
|
|
427
|
+
@project_unpositioned_mixins = {}.freeze
|
|
417
428
|
@project_discovered_deferred_ranges = {}.freeze
|
|
418
429
|
# Issue #1120 — `{refined class => {method => [refining modules]}}`, every `refine X do … end` body.
|
|
419
430
|
@project_discovered_refinements = {}.freeze
|
|
@@ -433,6 +444,9 @@ module Rigor
|
|
|
433
444
|
@constant_shadower_index = nil
|
|
434
445
|
@project_discovered_method_visibilities = {}.freeze
|
|
435
446
|
@project_discovered_methods = {}.freeze
|
|
447
|
+
# ADR-119 WD1 — the `possible_*` / `contested_*` siblings of the paired members, ONE Hash keyed by sibling
|
|
448
|
+
# name so no adoption or seed path names a sibling and none can drop one.
|
|
449
|
+
@project_discovery_siblings = Scope::DiscoveryIndex::EMPTY_SIBLINGS
|
|
436
450
|
# Issue #992 — the cross-file parameter-envelope table `call.wrong-arity` reads for an undeclared `def`.
|
|
437
451
|
@project_discovered_parameter_envelopes = {}.freeze
|
|
438
452
|
@project_data_member_layouts = {}.freeze
|
|
@@ -508,6 +522,9 @@ module Rigor
|
|
|
508
522
|
# #788 — the per-file reader is assigned only on the analysis (miss) path; reset it here so a run the
|
|
509
523
|
# ADR-45 result cache serves does not answer with the previous run's rows.
|
|
510
524
|
@per_file_diagnostics = [].freeze
|
|
525
|
+
@analysis_reads = {}
|
|
526
|
+
@pre_analysis_plugin_rows = nil
|
|
527
|
+
@post_analysis_plugin_rows = nil
|
|
511
528
|
# ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
|
|
512
529
|
# of THIS run share one memo bucket while entries from any earlier run in this process (stale after
|
|
513
530
|
# an edit) become unreachable.
|
|
@@ -828,6 +845,11 @@ module Rigor
|
|
|
828
845
|
# then hit — the stats it would otherwise suppress were all gathered.
|
|
829
846
|
@run_served_from_cache = !computed && analysis.nil?
|
|
830
847
|
diagnostics
|
|
848
|
+
rescue Inference::DeclarationWalk::ContractError
|
|
849
|
+
# A broken walk contract in the project pre-pass (a shadow-harness divergence, an unknown variant) is
|
|
850
|
+
# not a cache failure: the fallback below would re-run the analysis with the pre-pass marked done and
|
|
851
|
+
# no project index, and report the resulting false positives instead of the failure.
|
|
852
|
+
raise
|
|
831
853
|
rescue StandardError
|
|
832
854
|
# The result cache must never break a run. If anything in the cache path fails, fall back to a
|
|
833
855
|
# direct, uncached analysis.
|
|
@@ -1175,19 +1197,55 @@ module Rigor
|
|
|
1175
1197
|
# `#per_file_diagnostics`: the `analyze_files` return, stamped with the same severity profile the run's
|
|
1176
1198
|
# own stream gets (`SeverityStamp` drops `:off` rows and re-stamps overrides, and the per-file cache never
|
|
1177
1199
|
# re-stamps), sliced to the rows positioned at the targets. Returns the RAW return for the run's stream,
|
|
1178
|
-
# which is stamped once, at the end of `#run_analysis
|
|
1200
|
+
# which is stamped once, at the end of `#run_analysis` — on a narrowed incremental run with the served
|
|
1201
|
+
# files' rows spliced in (#1524), which `#per_file_diagnostics` never carries.
|
|
1179
1202
|
def analyze_targets(targets, environment:, project_files:)
|
|
1180
1203
|
raw = template_units.remap(
|
|
1181
1204
|
@pool_coordinator.analyze_files(targets, environment: environment, project_files: project_files)
|
|
1182
1205
|
)
|
|
1206
|
+
# ADR-45 WD2 (#1507) — the boundaries as the per-file loop left them; with the snapshot the first
|
|
1207
|
+
# analysed file took, the rows {#incremental_slot_rows} checks every analysis-time read against.
|
|
1208
|
+
@post_analysis_plugin_rows = plugin_boundary_rows if @record_dependencies
|
|
1183
1209
|
analysed = targets.to_set
|
|
1184
1210
|
@per_file_diagnostics = @diagnostic_aggregator.apply_severity_profile(raw)
|
|
1185
1211
|
.select { |diagnostic| analysed.include?(diagnostic.path) }
|
|
1186
1212
|
.freeze
|
|
1187
|
-
raw
|
|
1213
|
+
interleave_served_diagnostics(raw, analysed, project_files | targets)
|
|
1188
1214
|
end
|
|
1189
1215
|
private :analyze_targets
|
|
1190
1216
|
|
|
1217
|
+
# Issue #1524 — splices the rows {IncrementalSession} serves from its per-file cache into this narrowed
|
|
1218
|
+
# run's per-file stream, each file's rows where a full run of the same tree lists them. A full run
|
|
1219
|
+
# analyses `order` (the project's files, then any buffer and template-unit targets) one file after
|
|
1220
|
+
# another and a narrowed run a subsequence of it, so a served file's rows go just before the first row
|
|
1221
|
+
# positioned at an analysed file later in `order`, or at the end when there is none. Only such a row
|
|
1222
|
+
# marks a place, so the rows this run produced keep their relative order and a pool backend's
|
|
1223
|
+
# `.rigor.yml` rows stay at the head. Splicing here rather than into the finished result keeps the
|
|
1224
|
+
# run-level streams on either side of the per-file stream, where a full run has them. The served rows
|
|
1225
|
+
# are already severity-resolved and the run's final stamp leaves them unchanged: a resolved severity
|
|
1226
|
+
# resolves to itself, and a row the profile turns `:off` is never cached.
|
|
1227
|
+
def interleave_served_diagnostics(raw, analysed, order)
|
|
1228
|
+
served = @served_per_file_diagnostics
|
|
1229
|
+
return raw if served.empty?
|
|
1230
|
+
|
|
1231
|
+
rank = order.each_with_index.to_h
|
|
1232
|
+
# A served path the expansion no longer lists (a file deleted between the session's listing and this
|
|
1233
|
+
# run) keeps its rows, after every other file's, as the concatenation this replaced did.
|
|
1234
|
+
known, unknown = served.keys.partition { |path| rank.key?(path) }
|
|
1235
|
+
pending = known.sort_by { |path| rank.fetch(path) }
|
|
1236
|
+
merged = []
|
|
1237
|
+
raw.each do |diagnostic|
|
|
1238
|
+
# `order` lists every target, so an analysed path always has a rank; one that did not would only
|
|
1239
|
+
# mark no place, since a misplaced row must never abort the run (ADR-5).
|
|
1240
|
+
position = analysed.include?(diagnostic.path) ? rank.fetch(diagnostic.path, -1) : -1
|
|
1241
|
+
merged.concat(served.fetch(pending.shift)) while pending.any? && rank.fetch(pending.first) < position
|
|
1242
|
+
merged << diagnostic
|
|
1243
|
+
end
|
|
1244
|
+
(pending + unknown).each { |path| merged.concat(served.fetch(path)) }
|
|
1245
|
+
merged
|
|
1246
|
+
end
|
|
1247
|
+
private :interleave_served_diagnostics
|
|
1248
|
+
|
|
1191
1249
|
# ADR-67 WD6a — the check-walk parameter-inference pre-pass. Populates `@project_param_inferred_types`
|
|
1192
1250
|
# (read by `project_scope_seed_tables`) with the call-site union of every undeclared parameter, running
|
|
1193
1251
|
# ONE round (a single hop of call-site → param typing; the protection scan's three-round fixpoint stays a
|
|
@@ -1310,25 +1368,205 @@ module Rigor
|
|
|
1310
1368
|
# `.rbs` files that existed while the run read them, so without these a signature file written after
|
|
1311
1369
|
# the run left the slot validating fresh and the warm run answered without it.
|
|
1312
1370
|
globs = rbs_descriptor.globs + template_unit_glob_entries
|
|
1371
|
+
boundary = plugin_boundary_rows
|
|
1372
|
+
entries.concat(boundary.files)
|
|
1373
|
+
globs.concat(boundary.globs).concat(producer_watch_glob_entries)
|
|
1374
|
+
Cache::Descriptor.new(files: entries, globs: globs.uniq)
|
|
1375
|
+
end
|
|
1376
|
+
|
|
1377
|
+
# Every file a plugin read or probed through its `IoBoundary` (#577 / #613), and every directory it listed
|
|
1378
|
+
# (#954), as the boundaries stand when asked.
|
|
1379
|
+
def plugin_boundary_rows
|
|
1380
|
+
files = []
|
|
1381
|
+
globs = []
|
|
1313
1382
|
@plugin_registry.plugins.each do |plugin|
|
|
1314
1383
|
# Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
|
|
1315
1384
|
# are frozen after the run, and a plugin that never built a boundary read no files through it,
|
|
1316
1385
|
# so it contributes no dependencies.
|
|
1317
1386
|
boundary = plugin.instance_variable_get(:@io_boundary)
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1387
|
+
next unless boundary
|
|
1388
|
+
|
|
1389
|
+
# #629 — a boundary carries GlobEntry listing rows too (`IoBoundary#list_directory`), and they
|
|
1390
|
+
# are the only edge back to a file the run decided was ABSENT from a directory it consulted.
|
|
1391
|
+
boundary_descriptor = boundary.cache_descriptor
|
|
1392
|
+
files.concat(boundary_descriptor.files)
|
|
1393
|
+
globs.concat(boundary_descriptor.globs)
|
|
1394
|
+
end
|
|
1395
|
+
Cache::Descriptor.new(files: files, globs: globs)
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
# One glob row per producer `watch:` pattern (ADR-60 WD3), recomputed against the tree as it stands.
|
|
1399
|
+
def producer_watch_glob_entries
|
|
1400
|
+
@plugin_registry.plugins.flat_map do |plugin|
|
|
1401
|
+
plugin.class.producers.each_value.flat_map do |prod|
|
|
1402
|
+
prod[:watch] ? plugin.send(:watch_glob_entries, prod[:watch]) : []
|
|
1324
1403
|
end
|
|
1325
|
-
|
|
1326
|
-
|
|
1404
|
+
end
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
# ADR-45 WD2 (#1507) — the rows `rigor check --incremental`'s run-result slot records beside the analysed
|
|
1408
|
+
# files (whose digests {IncrementalSession} holds itself). Every run reads these inputs again and re-derives
|
|
1409
|
+
# its answer from them; they are split by when the row was taken, which decides whether the session's
|
|
1410
|
+
# {IncrementalRunSlot::WriteGuard} must vouch for it:
|
|
1411
|
+
#
|
|
1412
|
+
# - `observed` — every plugin row (`#prepare`, producers, template synthesis, and this run's per-file reads
|
|
1413
|
+
# too), recorded by the {Plugin::IoBoundary} as the plugin read. It includes each producer's `watch:`
|
|
1414
|
+
# globs, which a producer replays into its boundary whether it computed or was served (#1558). A save
|
|
1415
|
+
# after the read leaves the row stale, so no guard is needed.
|
|
1416
|
+
# - `derived` — rows taken when the run ENDS: the template files and their claimed globs (#392, re-analysed
|
|
1417
|
+
# every run), and one existence row per analysis root and per `pre_eval:` entry. A run's path errors (`no
|
|
1418
|
+
# such file or directory`, `pre-eval.file-not-found`) answer to those last rows; the plain slot records
|
|
1419
|
+
# neither, and the incremental path regenerates the errors every run, so a slot that served one past the
|
|
1420
|
+
# edit that retracts it would print what no run of that tree prints. Existence rows carry ADR-45 WD1b's
|
|
1421
|
+
# bound: a directory replaced by a file of the same name stays fresh.
|
|
1422
|
+
# - `by_file` — `{path => Descriptor}`, the plugin reads each file's analysis made
|
|
1423
|
+
# ({Plugin::IoBoundary.attributing}). A narrowed run analyses only its closure, so the session carries the
|
|
1424
|
+
# other files' reads forward from the previous slot.
|
|
1425
|
+
#
|
|
1426
|
+
# The template rows stand in for the `template-units` slot the plain key carries, which only a run can
|
|
1427
|
+
# compute. That digest is a function of each unit's compiled Ruby (the template's bytes, and whatever else
|
|
1428
|
+
# the transform read, through its boundary), its transform (the plugin, whose identity and configuration
|
|
1429
|
+
# the key carries) and the synthesis version (the engine's): each of those has a row or a key slot.
|
|
1430
|
+
#
|
|
1431
|
+
# The inputs a recheck does NOT re-derive are {#baseline_dependency_rows}. nil when this run cannot vouch for
|
|
1432
|
+
# its rows — a pool run, whose workers' reads never reach this process; a run that recorded no dependencies,
|
|
1433
|
+
# whose analysis no session owns; or a run in which a boundary row changed during the per-file loop that no
|
|
1434
|
+
# file's analysis was credited with, such as a read from a thread the plugin started.
|
|
1435
|
+
IncrementalSlotRows = Data.define(:observed, :derived, :by_file)
|
|
1436
|
+
|
|
1437
|
+
# @param roots — the analysis roots the run was given.
|
|
1438
|
+
def incremental_slot_rows(roots:)
|
|
1439
|
+
return nil if pool_mode? || !@record_dependencies
|
|
1440
|
+
return nil unless analysis_reads_attributed?
|
|
1441
|
+
|
|
1442
|
+
IncrementalSlotRows.new(
|
|
1443
|
+
observed: plugin_boundary_rows,
|
|
1444
|
+
derived: Cache::Descriptor.new(
|
|
1445
|
+
files: template_unit_file_entries + existence_entries(Array(roots) + @configuration.pre_eval),
|
|
1446
|
+
globs: template_unit_glob_entries
|
|
1447
|
+
),
|
|
1448
|
+
by_file: @analysis_reads.transform_values do |reads|
|
|
1449
|
+
Cache::Descriptor.new(
|
|
1450
|
+
files: reads.grep(Cache::Descriptor::FileEntry).uniq,
|
|
1451
|
+
globs: reads.grep(Cache::Descriptor::GlobEntry).uniq
|
|
1452
|
+
)
|
|
1327
1453
|
end
|
|
1454
|
+
)
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
# Whether every boundary row that appeared or changed during the per-file loop is one some file's analysis
|
|
1458
|
+
# was credited with. A row outside every file's attribution would ride this run's `run` rows only, and the
|
|
1459
|
+
# next narrowed run — which does not repeat the read — would drop it.
|
|
1460
|
+
def analysis_reads_attributed?
|
|
1461
|
+
post = @post_analysis_plugin_rows || plugin_boundary_rows
|
|
1462
|
+
pre = @pre_analysis_plugin_rows || post
|
|
1463
|
+
credited = @analysis_reads.each_value.flat_map(&:itself).to_set
|
|
1464
|
+
(post.files - pre.files).all? { |row| credited.include?(row) } &&
|
|
1465
|
+
(post.globs - pre.globs).all? { |row| credited.include?(row) }
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
# ADR-45 WD2 (#1507) — the rows a FULL run records for the inputs an incremental recheck does not re-derive,
|
|
1469
|
+
# which the session therefore carries unchanged from the full run that recorded them to every later slot:
|
|
1470
|
+
#
|
|
1471
|
+
# - the signature tree the plain slot records (`RbsDescriptor.file_entries` / `.glob_entries` over the loader
|
|
1472
|
+
# roots this run's analysis resolved), and an existence row per signature root, configured or the
|
|
1473
|
+
# auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty, and the snapshot
|
|
1474
|
+
# fingerprint digests the same roots (configured, or the auto-detected `sig`), so a change to one drops the
|
|
1475
|
+
# snapshot and forces a full run rather than reaching a recheck;
|
|
1476
|
+
# - the discovered-not-analysed files (#684) and a listing row per discovery root — a narrowed recheck does
|
|
1477
|
+
# not widen discovery at all;
|
|
1478
|
+
# - every `pre_eval:` file outside `files` (a stat row, or an absence row) — the session tracks only analysed
|
|
1479
|
+
# files, and one that is analysed is guarded by its own row.
|
|
1480
|
+
#
|
|
1481
|
+
# Recomputing any of these on a recheck would vouch for the tree the recheck saw while its answer still
|
|
1482
|
+
# carries rows computed before the change, so a change to one keeps the probe declining until the next full
|
|
1483
|
+
# run. nil when this process analysed no file itself — a pool run, or a run with no files.
|
|
1484
|
+
#
|
|
1485
|
+
# Split by what identifies the input. `pinned` holds the signature files outside the project's own signature
|
|
1486
|
+
# roots, which the slot's key identifies already: the engine's own (`data/`, a bundled plugin's `sig/`, a
|
|
1487
|
+
# gem overlay) by the engine slot, a gem's `sig/` and an `rbs collection` directory by the lockfiles. `owned`
|
|
1488
|
+
# holds the rest, which only their rows identify. The session's write guard refuses an `owned` row whose
|
|
1489
|
+
# change time it cannot read, and passes over a `pinned` one.
|
|
1490
|
+
BaselineRows = Data.define(:owned, :pinned)
|
|
1491
|
+
|
|
1492
|
+
SignatureRoots = Struct.new(:signature_paths)
|
|
1493
|
+
private_constant :SignatureRoots
|
|
1494
|
+
|
|
1495
|
+
# @param files — the run's analysed-path set.
|
|
1496
|
+
def baseline_dependency_rows(files:)
|
|
1497
|
+
return nil if pool_mode? || !@collect_stats || @analyzed_files.empty?
|
|
1498
|
+
|
|
1499
|
+
own, pinned = signature_rows
|
|
1500
|
+
BaselineRows.new(
|
|
1501
|
+
owned: Cache::Descriptor.new(
|
|
1502
|
+
files: own.files + existence_entries(signature_root_candidates) +
|
|
1503
|
+
discovery_file_entries({ files: files }) + unanalysed_pre_eval_entries(files),
|
|
1504
|
+
globs: (own.globs + discovery_glob_entries).uniq
|
|
1505
|
+
),
|
|
1506
|
+
pinned: pinned
|
|
1507
|
+
)
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
# The signature tree's rows, the project's own and the rest.
|
|
1511
|
+
def signature_rows
|
|
1512
|
+
roots = SignatureRoots.new(@snapshots.signature_paths)
|
|
1513
|
+
own = signature_root_candidates.map { |root| File.join(File.absolute_path(root.to_s), "") }
|
|
1514
|
+
own_files, other_files = Cache::RbsDescriptor.file_entries(roots, comparator: :stat)
|
|
1515
|
+
.partition { |row| within?(row.path, own) }
|
|
1516
|
+
own_globs, other_globs = Cache::RbsDescriptor.glob_entries(roots).partition { |row| within?(row.root, own) }
|
|
1517
|
+
[Cache::Descriptor.new(files: own_files, globs: own_globs.uniq),
|
|
1518
|
+
Cache::Descriptor.new(files: other_files, globs: other_globs.uniq)]
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
def within?(path, roots)
|
|
1522
|
+
directory = File.join(File.absolute_path(path.to_s), "")
|
|
1523
|
+
roots.any? { |root| directory.start_with?(root) }
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
# The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
|
|
1527
|
+
# `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
|
|
1528
|
+
# signature rows' listing rows cover a file appearing under a root that existed; only an existence row
|
|
1529
|
+
# covers the root itself appearing.
|
|
1530
|
+
def signature_root_candidates
|
|
1531
|
+
@configuration.signature_paths || ["sig"]
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
# One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
|
|
1535
|
+
# as well as an edit to one that existed.
|
|
1536
|
+
def discovery_glob_entries
|
|
1537
|
+
return [] if @project_discovery_expansion.nil?
|
|
1538
|
+
|
|
1539
|
+
@configuration.paths.select { |root| File.directory?(root) }.map do |root|
|
|
1540
|
+
Cache::Descriptor::GlobEntry.compute(root: root, pattern: PathExpansion::RUBY_GLOB, mode: :names)
|
|
1328
1541
|
end
|
|
1329
|
-
Cache::Descriptor.new(files: entries, globs: globs.uniq)
|
|
1330
1542
|
end
|
|
1331
1543
|
|
|
1544
|
+
def unanalysed_pre_eval_entries(files)
|
|
1545
|
+
analysed = files.to_set { |path| File.absolute_path(path) }
|
|
1546
|
+
outside = @configuration.pre_eval.reject { |path| analysed.include?(File.absolute_path(path)) }
|
|
1547
|
+
outside.map do |path|
|
|
1548
|
+
if File.file?(path)
|
|
1549
|
+
Cache::Descriptor::FileEntry.stat(path: path, digest: Cache::FileDigest.hexdigest(path))
|
|
1550
|
+
else
|
|
1551
|
+
existence_entries([path]).first
|
|
1552
|
+
end
|
|
1553
|
+
end
|
|
1554
|
+
end
|
|
1555
|
+
|
|
1556
|
+
def existence_entries(paths)
|
|
1557
|
+
paths.uniq.map do |path|
|
|
1558
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
1559
|
+
if File.exist?(physical)
|
|
1560
|
+
Cache::Descriptor::FileEntry.present(path: physical)
|
|
1561
|
+
else
|
|
1562
|
+
Cache::Descriptor::FileEntry.absent(path: physical)
|
|
1563
|
+
end
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries,
|
|
1567
|
+
:analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
|
|
1568
|
+
:unanalysed_pre_eval_entries, :signature_rows, :within?
|
|
1569
|
+
|
|
1332
1570
|
# Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
|
|
1333
1571
|
# are a function of them (that is the entire point: the method whose absence would have been reported
|
|
1334
1572
|
# is defined in one of them), so the run-result cache must invalidate when one changes. Empty on every
|
|
@@ -1483,8 +1721,10 @@ module Rigor
|
|
|
1483
1721
|
end
|
|
1484
1722
|
|
|
1485
1723
|
# The tables only check rules read beyond dispatch — the issue #1120 refinements and the issue #1367
|
|
1486
|
-
# global-write census
|
|
1724
|
+
# global-write census — and the ADR-119 WD1 siblings of the paired members. Extracted to keep
|
|
1725
|
+
# {#apply_discovery_result} under its ABC budget.
|
|
1487
1726
|
def apply_discovery_call_surface_tables(discovery)
|
|
1727
|
+
@project_discovery_siblings = discovery.siblings
|
|
1488
1728
|
@project_discovered_refinements = discovery.discovered_refinements
|
|
1489
1729
|
@project_discovered_global_write_census = discovery.discovered_global_write_census
|
|
1490
1730
|
end
|
|
@@ -1496,6 +1736,7 @@ module Rigor
|
|
|
1496
1736
|
@project_discovered_includes = discovery.discovered_includes
|
|
1497
1737
|
@project_discovered_prepends = discovery.discovered_prepends
|
|
1498
1738
|
@project_discovered_extends = discovery.discovered_extends
|
|
1739
|
+
@project_unpositioned_mixins = discovery.unpositioned_mixins
|
|
1499
1740
|
end
|
|
1500
1741
|
|
|
1501
1742
|
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
@@ -1512,7 +1753,7 @@ module Rigor
|
|
|
1512
1753
|
|
|
1513
1754
|
@project_discovery_done = true
|
|
1514
1755
|
# Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
|
|
1515
|
-
expansion = @project_discovery_expansion || expansion
|
|
1756
|
+
expansion = @project_discovery_expansion || once_each(expansion)
|
|
1516
1757
|
if @collect_seed_bundles
|
|
1517
1758
|
# ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
|
|
1518
1759
|
# capture the refreshed bundle set for the session to persist.
|
|
@@ -1546,11 +1787,42 @@ module Rigor
|
|
|
1546
1787
|
# Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
|
|
1547
1788
|
# seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
|
|
1548
1789
|
# ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
|
|
1790
|
+
#
|
|
1791
|
+
# Issue #1556 — the widened set holds each FILE once. `.rigor.yml`'s `paths:` load as absolute paths and a
|
|
1792
|
+
# path argument usually does not, so `rigor check lib` widened discovery over both spellings of every file:
|
|
1793
|
+
# the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
|
|
1794
|
+
# and every file was a discovered-not-analysed dependency of the run's cache entry.
|
|
1795
|
+
#
|
|
1796
|
+
# The widening decision itself still compares the paths as spelled. `exclude:` patterns match a path as it
|
|
1797
|
+
# is spelled, so until #1576 is fixed the configured absolute `lib` can discover files the argument `lib`
|
|
1798
|
+
# excludes (a relative `lib/gen/**` pattern never matches `/abs/lib/gen/x.rb`), and treating the two as the
|
|
1799
|
+
# same root would drop those files from discovery.
|
|
1549
1800
|
def project_discovery_expansion(paths, expansion)
|
|
1550
1801
|
return nil unless widen_discovery_to_project?(paths)
|
|
1551
1802
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1803
|
+
analysed = expansion.fetch(:files)
|
|
1804
|
+
files = discovery_files(expand_paths(@configuration.paths | paths).fetch(:files), analysed)
|
|
1805
|
+
# Overlapping arguments (`lib lib/user.rb`) name an analysed file twice; the widened set names it once.
|
|
1806
|
+
files.size > analysed.uniq { |path| File.absolute_path(path) }.size ? { files: files, errors: [] } : nil
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
# The expansion with each file once, first spelling kept: overlapping arguments (`lib ./lib`) that do not
|
|
1810
|
+
# widen discovery still name a file twice, and the pre-pass would take every constant in it for one written
|
|
1811
|
+
# twice (#1556). Symlinked aliases (`src -> lib`) are not folded; that needs a realpath and stays out of scope.
|
|
1812
|
+
def once_each(expansion)
|
|
1813
|
+
files = expansion.fetch(:files)
|
|
1814
|
+
unique = files.uniq { |path| File.absolute_path(path) }
|
|
1815
|
+
unique.size == files.size ? expansion : expansion.merge(files: unique)
|
|
1816
|
+
end
|
|
1817
|
+
|
|
1818
|
+
# Each file once, in project order, under the spelling the analysis uses when it is analysed.
|
|
1819
|
+
def discovery_files(project_files, analysed_files)
|
|
1820
|
+
spelling = analysed_files.to_h { |path| [File.absolute_path(path), path] }
|
|
1821
|
+
seen = Set.new
|
|
1822
|
+
project_files.filter_map do |path|
|
|
1823
|
+
absolute = File.absolute_path(path)
|
|
1824
|
+
spelling.fetch(absolute, path) if seen.add?(absolute)
|
|
1825
|
+
end
|
|
1554
1826
|
end
|
|
1555
1827
|
|
|
1556
1828
|
def widen_discovery_to_project?(paths)
|
|
@@ -1569,7 +1841,7 @@ module Rigor
|
|
|
1569
1841
|
|
|
1570
1842
|
private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
|
|
1571
1843
|
:apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
|
|
1572
|
-
:project_discovery_expansion, :widen_discovery_to_project?
|
|
1844
|
+
:project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project?
|
|
1573
1845
|
|
|
1574
1846
|
# Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
|
|
1575
1847
|
# `version:`. Prism exposes no version list, so the floor is found empirically — only when a
|
|
@@ -1832,7 +2104,7 @@ module Rigor
|
|
|
1832
2104
|
end
|
|
1833
2105
|
|
|
1834
2106
|
def shadow_rule_walk?
|
|
1835
|
-
|
|
2107
|
+
ShadowHarness.enabled?
|
|
1836
2108
|
end
|
|
1837
2109
|
|
|
1838
2110
|
def collect_plugin_diagnostics(plugin, path, root, scope, node_result)
|
|
@@ -1977,13 +2249,9 @@ module Rigor
|
|
|
1977
2249
|
# bucket identically to the sequential path.
|
|
1978
2250
|
tables[:run_generation] = @run_generation if @run_generation
|
|
1979
2251
|
tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
|
|
1980
|
-
tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
|
|
1981
2252
|
unless @project_discovered_def_nestings.empty?
|
|
1982
2253
|
tables[:discovered_def_nestings] = @project_discovered_def_nestings
|
|
1983
2254
|
end
|
|
1984
|
-
unless @project_discovered_singleton_def_nodes.empty?
|
|
1985
|
-
tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
|
|
1986
|
-
end
|
|
1987
2255
|
seed_def_source_tables(tables)
|
|
1988
2256
|
unless @project_discovered_superclasses.empty?
|
|
1989
2257
|
tables[:discovered_superclasses] = @project_discovered_superclasses
|
|
@@ -1992,27 +2260,41 @@ module Rigor
|
|
|
1992
2260
|
tables[:discovered_header_nestings] = @project_discovered_header_nestings
|
|
1993
2261
|
end
|
|
1994
2262
|
seed_mixin_tables(tables)
|
|
1995
|
-
|
|
1996
|
-
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
1997
|
-
end
|
|
1998
|
-
tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
|
|
1999
|
-
unless @project_discovered_deferred_ranges.empty?
|
|
2000
|
-
tables[:discovered_deferred_ranges] = @project_discovered_deferred_ranges
|
|
2001
|
-
end
|
|
2263
|
+
seed_paired_tables(tables)
|
|
2002
2264
|
seed_call_surface_tables(tables)
|
|
2003
2265
|
seed_opt_in_pre_pass_tables(tables)
|
|
2004
2266
|
seed_member_layout_tables(tables)
|
|
2005
2267
|
seed_dependency_attribution_tables(tables)
|
|
2006
|
-
|
|
2268
|
+
# Only a base seed can bring in half a pair (`seed_paired_tables` writes both halves), so a run without
|
|
2269
|
+
# one skips the copy this costs per file.
|
|
2270
|
+
@discovery_seed ? Scope::DiscoveryIndex.compact_pairs(tables) : tables
|
|
2271
|
+
end
|
|
2272
|
+
|
|
2273
|
+
# ADR-119 WD1 — the members `Scope::DiscoveryIndex::SIBLINGS` pairs, each with its sibling. A pair rides the
|
|
2274
|
+
# seed when either half is non-empty and is left out when both are, which keeps an `discovery_seed:` base's
|
|
2275
|
+
# table for a pair this run computed nothing for. {Scope::DiscoveryIndex#with} takes the two together, and
|
|
2276
|
+
# `compact_pairs` completes a half pair a base seed brought in.
|
|
2277
|
+
PAIRED_MEMBER_IVARS = Scope::DiscoveryIndex::SIBLINGS.keys.to_h do |member|
|
|
2278
|
+
[member, :"@project_#{member}"]
|
|
2279
|
+
end.freeze
|
|
2280
|
+
private_constant :PAIRED_MEMBER_IVARS
|
|
2281
|
+
|
|
2282
|
+
def seed_paired_tables(tables)
|
|
2283
|
+
PAIRED_MEMBER_IVARS.each do |member, ivar|
|
|
2284
|
+
sibling = Scope::DiscoveryIndex::SIBLINGS.fetch(member)
|
|
2285
|
+
table = instance_variable_get(ivar)
|
|
2286
|
+
own = @project_discovery_siblings.fetch(sibling)
|
|
2287
|
+
next if table.empty? && own.empty?
|
|
2288
|
+
|
|
2289
|
+
tables[member] = table
|
|
2290
|
+
tables[sibling] = own
|
|
2291
|
+
end
|
|
2007
2292
|
end
|
|
2008
2293
|
|
|
2009
|
-
# The tables only check rules read: the issue #
|
|
2010
|
-
#
|
|
2011
|
-
#
|
|
2294
|
+
# The tables only check rules read: the issue #1120 refinements (`call.undefined-method`) and the issue #1367
|
|
2295
|
+
# global-write census (the `global.*` write rules); the issue #992 envelopes ride {#seed_paired_tables}. Split
|
|
2296
|
+
# out of {#project_scope_seed_tables} to keep it under the complexity budget.
|
|
2012
2297
|
def seed_call_surface_tables(tables)
|
|
2013
|
-
unless @project_discovered_parameter_envelopes.empty?
|
|
2014
|
-
tables[:discovered_parameter_envelopes] = @project_discovered_parameter_envelopes
|
|
2015
|
-
end
|
|
2016
2298
|
tables[:discovered_refinements] = @project_discovered_refinements unless @project_discovered_refinements.empty?
|
|
2017
2299
|
# Issue #1367 — read by the `global.*` write rules.
|
|
2018
2300
|
return if @project_discovered_global_write_census.empty?
|
|
@@ -2028,12 +2310,14 @@ module Rigor
|
|
|
2028
2310
|
tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
|
|
2029
2311
|
tables[:discovered_prepends] = @project_discovered_prepends unless @project_discovered_prepends.empty?
|
|
2030
2312
|
tables[:discovered_extends] = @project_discovered_extends unless @project_discovered_extends.empty?
|
|
2313
|
+
tables[:unpositioned_mixins] = @project_unpositioned_mixins unless @project_unpositioned_mixins.empty?
|
|
2031
2314
|
end
|
|
2032
2315
|
|
|
2033
|
-
# ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read
|
|
2316
|
+
# ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read by the recording accessors
|
|
2034
2317
|
# (`Scope#record_class_dependency` for the class-declaration map, `Scope#record_constant_dependency`
|
|
2035
|
-
# for the constant-write map).
|
|
2036
|
-
#
|
|
2318
|
+
# for the constant-write map). The class-declaration map rides every run: an ancestor-order reader also
|
|
2319
|
+
# asks whether a class is declared in more than one file. The constant-write map rides only a recording
|
|
2320
|
+
# run. Extracted to keep {#project_scope_seed_tables} under the complexity budget.
|
|
2037
2321
|
#
|
|
2038
2322
|
# Issue #617 — the census's binding writes, grouped by last segment, ride every run instead: a constant
|
|
2039
2323
|
# compound write whose plain read resolves nothing reads its binding off them, a TYPE rather than an edge.
|
|
@@ -2043,10 +2327,10 @@ module Rigor
|
|
|
2043
2327
|
tables[:constant_writers] = writers unless writers.empty?
|
|
2044
2328
|
shadowers = constant_shadower_index
|
|
2045
2329
|
tables[:constant_shadowers] = shadowers unless shadowers.empty?
|
|
2046
|
-
return unless @record_dependencies
|
|
2047
|
-
|
|
2048
2330
|
tables[:discovered_class_sources] = @project_discovered_class_sources unless
|
|
2049
2331
|
@project_discovered_class_sources.empty?
|
|
2332
|
+
return unless @record_dependencies
|
|
2333
|
+
|
|
2050
2334
|
tables[:constant_sources] = @project_constant_sources unless @project_constant_sources.empty?
|
|
2051
2335
|
end
|
|
2052
2336
|
|
|
@@ -2108,7 +2392,7 @@ module Rigor
|
|
|
2108
2392
|
def memo_files_by_segment
|
|
2109
2393
|
@project_constant_writes.each_with_object({}) do |(name, by_path), files|
|
|
2110
2394
|
set = (files[name.split("::").last] ||= Set.new)
|
|
2111
|
-
by_path.each { |path, descriptor| set << File.
|
|
2395
|
+
by_path.each { |path, descriptor| set << File.absolute_path(path) if memo_descriptor?(descriptor) }
|
|
2112
2396
|
end
|
|
2113
2397
|
end
|
|
2114
2398
|
|
|
@@ -2166,14 +2450,14 @@ module Rigor
|
|
|
2166
2450
|
writers = @project_constant_sources[name]
|
|
2167
2451
|
wildcard = @project_constant_sources["#{Inference::ScopeIndexer::DYNAMIC_TARGET_PREFIX}#{segment}"]
|
|
2168
2452
|
[writers, wildcard].any? do |paths|
|
|
2169
|
-
paths&.any? { |path| !listed.include?(File.
|
|
2453
|
+
paths&.any? { |path| !listed.include?(File.absolute_path(path)) }
|
|
2170
2454
|
end
|
|
2171
2455
|
end
|
|
2172
2456
|
|
|
2173
2457
|
# The listed `pre_eval:` files, expanded so a census path and a configured one compare as the same
|
|
2174
2458
|
# file whichever spelling each arrived in.
|
|
2175
2459
|
def pre_eval_path_set
|
|
2176
|
-
@pre_eval_path_set ||= @configuration.pre_eval.to_set { |path| File.
|
|
2460
|
+
@pre_eval_path_set ||= @configuration.pre_eval.to_set { |path| File.absolute_path(path) }
|
|
2177
2461
|
end
|
|
2178
2462
|
|
|
2179
2463
|
# ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
|
|
@@ -2203,11 +2487,18 @@ module Rigor
|
|
|
2203
2487
|
def analyze_file(path, environment)
|
|
2204
2488
|
return analyze_with_effects(path, environment) unless @record_dependencies
|
|
2205
2489
|
|
|
2490
|
+
# ADR-45 WD2 (#1507) — every plugin read this file's analysis makes is attributed to it, so the
|
|
2491
|
+
# incremental session can validate them on a later run that serves this file's answer from cache.
|
|
2492
|
+
@pre_analysis_plugin_rows ||= plugin_boundary_rows
|
|
2493
|
+
reads = []
|
|
2206
2494
|
diagnostics = nil
|
|
2207
|
-
record =
|
|
2208
|
-
|
|
2495
|
+
record = Plugin::IoBoundary.attributing(reads) do
|
|
2496
|
+
DependencyRecorder.record_for(path) do
|
|
2497
|
+
diagnostics = analyze_with_effects(path, environment)
|
|
2498
|
+
end
|
|
2209
2499
|
end
|
|
2210
2500
|
@file_dependencies[path] = record
|
|
2501
|
+
@analysis_reads[path] = reads unless reads.empty?
|
|
2211
2502
|
diagnostics
|
|
2212
2503
|
end
|
|
2213
2504
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# The `RIGOR_SHADOW_RULE_WALK` switch, read in one place by both halves of the shadow harness: the rule
|
|
6
|
+
# walk's collectors (ADR-53 Track B, {CheckRules}) and the discovery tables (ADR-116 WD5,
|
|
7
|
+
# {Inference::DeclarationWalk::Shadow}, which also says how a run must be made), and by the cache keys that
|
|
8
|
+
# must not let a warm run answer for either. A leaf file, so the ADR-87 boot-slim probe loads it without
|
|
9
|
+
# any engine code.
|
|
10
|
+
module ShadowHarness
|
|
11
|
+
ENV_KEY = "RIGOR_SHADOW_RULE_WALK"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Set to anything, `0` and the empty string included.
|
|
16
|
+
def enabled?
|
|
17
|
+
!ENV[ENV_KEY].nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What the run-result cache and the incremental snapshot key on: nil with the harness off, which adds
|
|
21
|
+
# no key slot, so a run without the variable computes the key it computed before the harness existed.
|
|
22
|
+
def cache_identity
|
|
23
|
+
enabled? ? ENV_KEY : nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|